From c8d935819b4ba7873b717f33381ff8c74efaa5c9 Mon Sep 17 00:00:00 2001 From: Marc-Antoine Dubois <56685045+MADUBOIS0@users.noreply.github.com> Date: Tue, 28 Jan 2025 15:01:28 -0500 Subject: [PATCH] Change AccountUserNameDomain regex to allow subdomains Current regex: '^\\w+\\.\\w+$') New regex: '^\\w+(\\.\\w+)+$') This will detect subdomains such as ad.mydomain.local instead of only mydomain.local --- Propagation-Scripts/windows_service/template.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Propagation-Scripts/windows_service/template.json b/Propagation-Scripts/windows_service/template.json index 49e1ff8..a8c10d8 100644 --- a/Propagation-Scripts/windows_service/template.json +++ b/Propagation-Scripts/windows_service/template.json @@ -1,7 +1,7 @@ { "version": 1, "template": { - "command": "#requires -Version 7\n\n<#\n.SYNOPSIS\nUpdates the password for a specified service account and optionally restarts the service on a remote system.\n\n.DESCRIPTION\nThis script updates the service account password on a specified endpoint. It supports both domain and local user accounts and can handle multiple services. If specified, it can also restart the services after updating the password.\n\n.PARAMETER Endpoint\nSpecifies the target endpoint where the service(s) are running. This should be the hostname or IP address of the remote system.\n\n.PARAMETER EndpointUserName\nSpecifies the username used to authenticate against the endpoint. This should be an account with permissions to modify service settings.\n\n.PARAMETER EndpointPassword\nSpecifies the password for the EndpointUserName as a secure string.\n\n.PARAMETER AccountUserName\nSpecifies the service account username whose password needs updating. This parameter must be a flat username without domain information.\n\n.PARAMETER NewPassword\nSpecifies the new password for the service account as a secure string.\n\n.PARAMETER AccountUserNameDomain\nOptional. Specifies the domain of the service account in FQDN format if the account is a domain account.\n\n.PARAMETER ServiceName\nOptional. Specifies the name of the service that needs the service account password updated. If not provided, all services running under the specified account will be updated.\n\n.PARAMETER RestartService\nOptional. Specifies whether to restart the service after updating the password. Acceptable values are 'yes' to restart the service, or an empty string to leave the service running without restarting.\n\n.EXAMPLE\nPS C:\\> .\\script.ps1 -Endpoint 'server01' -EndpointUserName 'administrator' -EndpointPassword (ConvertTo-SecureString 'Passw0rd!' -AsPlainText -Force) -AccountUserName 'svc_account' -NewPassword (ConvertTo-SecureString 'N3wPassw0rd!' -AsPlainText -Force) -AccountUserNameDomain 'domain.local' -ServiceName 'MyService' -RestartService 'yes'\n\nThis example updates the password for the 'svc_account' user on the 'domain.local' domain, specifically for 'MyService' on 'server01', and restarts the service after the update.\n\n.NOTES\nThis script requires PowerShell Remoting to be enabled and configured on the target endpoint. Ensure credentials provided have the necessary administrative rights on the remote system.\n#>\n[CmdletBinding()]\nparam(\n [Parameter(Mandatory)]\n [ValidateNotNullOrEmpty()]\n [string]$Endpoint,\n \n [Parameter(Mandatory)]\n [ValidateNotNullOrEmpty()]\n [string]$EndpointUserName,\n\n [Parameter(Mandatory)]\n [ValidateNotNullOrEmpty()]\n [securestring]$EndpointPassword,\n\n [Parameter(Mandatory)]\n [ValidateNotNullOrEmpty()]\n [ValidatePattern(\n '^\\w+$',\n ErrorMessage = 'You must provide the AccountUserName parameter as a flat name like \"user\"; not domain\\user, et al. To specify a domain, use the AccountUserNameDomain parameter.'\n )]\n [string]$AccountUserName,\n\n [Parameter(Mandatory)]\n [ValidateNotNullOrEmpty()]\n [securestring]$NewPassword,\n\n [Parameter()]\n [string]$AccountUserNameDomain,\n\n [Parameter()]\n [string]$ServiceName,\n\n [Parameter()]\n [ValidateSet('yes', '')]\n [string]$RestartService\n)\n\n# Output the script parameters and the current user running the script\nWrite-Output -InputObject \"Running script with parameters: $($PSBoundParameters | Out-String) as [$(whoami)]\"\n\nif ($AccountUserNameDomain -and $AccountUserNameDomain -notmatch '^\\w+\\.\\w+$') {\n throw 'When using the AccountUserNameDomain parameter, you must provide the domain as an FQDN (domain.local)'\n}\n\n#region Functions\nfunction newCredential([string]$UserName, [securestring]$Password) {\n New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $UserName, $Password\n}\n\nfunction testIsOnDomain {\n (Get-CimInstance -ClassName win32_computersystem).PartOfDomain\n}\n\nfunction decryptPassword {\n param(\n [securestring]$Password\n )\n try {\n $ptr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Password)\n [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($ptr)\n } finally {\n ## Clear the decrypted password from memory\n [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ptr)\n }\n}\n\nfunction updateServiceUserPassword {\n param(\n $ServiceInstance,\n [string]$UserName,\n [securestring]$Password\n ) \n\n Invoke-CimMethod -InputObject $ServiceInstance -MethodName Change -Arguments @{\n StartName = $UserName\n StartPassword = decryptPassword($Password)\n }\n}\n\nfunction GetServiceAccountNames {\n param(\n $UserName,\n $Domain = '.'\n )\n\n if (!$PSBoundParameters.ContainsKey('Domain')) {\n ## local account\n @(\n \"$Domain\\$Username\" ## domain\\user\n )\n } else {\n ## Domain account with domain as FQDN\n @(\n \"$Username@$Domain\", ## user@domain.local\n \"$($Domain.split('.')[0])\\$Username\" ## domain\\user\n )\n }\n}\n\nfunction ValidateUserAccountPassword {\n [CmdletBinding()]\n param(\n [string]$UserName,\n [string]$Domain,\n [securestring]$Password\n )\n\n try {\n Add-Type -AssemblyName System.DirectoryServices.AccountManagement\n\n if ($Domain) {\n $context = New-Object System.DirectoryServices.AccountManagement.PrincipalContext([System.DirectoryServices.AccountManagement.ContextType]::Domain, $Domain)\n } else {\n $context = New-Object System.DirectoryServices.AccountManagement.PrincipalContext([System.DirectoryServices.AccountManagement.ContextType]::Machine)\n }\n \n $context.ValidateCredentials($UserName, (decryptPassword($Password)))\n } catch {\n Write-Error \"An error occurred: $_\"\n } finally {\n if ($context) {\n $context.Dispose()\n }\n }\n}\n#endregion\n\n# Create a new PSCredential object using the provided EndpointUserName and EndpointPassword\n$credential = newCredential $EndpointUserName $EndpointPassword\n\n# Define a script block to be executed remotely on the Windows server\n$scriptBlock = {\n\n $ErrorActionPreference = 'Stop'\n\n ## Assigning to variables inside the scriptblock allows mocking of args with Pester\n $userName = $args[0]\n $userDomain = $args[2]\n $pw = $args[1]\n $serviceNames = $args[3]\n $restartService = $args[4]\n\n if ($userDomain -and !(testIsOnDomain)) {\n throw \"The AccountUserNameDomain parameter was used and the host is not on a domain. For local accounts, do not use the AccountUserNameDomain parameter.\"\n }\n\n ## Ensure the password is valid\n $valUserAcctParams = @{\n UserName = $userName\n Password = $pw\n }\n if ($userDomain) {\n $valUserAcctParams.Domain = $userDomain\n }\n $validatePwResult = ValidateUserAccountPassword @valUserAcctParams\n if (!$validatePwResult) {\n throw \"The password for user account [$($UserName)] is invalid. Did you mean to provide a domain account? If so, use the AccountUserNameDomain parameter.\"\n }\n\n $getSrvAccountNamesParams = @{\n UserName = $userName\n }\n if ($userDomain) {\n $getSrvAccountNamesParams.Domain = $userDomain\n }\n [array]$serviceAccountNames = GetServiceAccountNames @getSrvAccountNamesParams\n $startNameCimQuery = \"(StartName = '{0}')\" -f ($serviceAccountNames -join \"' OR StartName = '\") ## (StartName = 'user@domain.local' OR StartName = 'domain\\user')\n\n if (-not $serviceNames) {\n $cimFilter = $startNameCimQuery\n } else {\n $cimFilter = \"(Name='{0}') AND {1}\" -f ($serviceNames -join \"' OR Name='\"), $startNameCimQuery\n }\n $cimFilter = $cimFilter.replace('\\', '\\\\')\n \n $serviceInstances = Get-CimInstance -ClassName Win32_Service -Filter $cimFilter\n if (-not $serviceInstances) {\n throw \"No services found on [{0}] running as [{1}] could be found.\" -f (hostname), $serviceAccountNames[0]\n }\n\n $results = foreach ($servInst in $serviceInstances) {\n try {\n $updateResult = updateServiceUserPassword -ServiceInstance $servInst -Username $serviceAccountNames[0] -Password $pw\n if ($updateResult.ReturnValue -ne 0) {\n throw \"Password update for service [{0}] failed with return value [{1}]\" -f $servInst.Name, $updateResult.ReturnValue\n }\n if ($restartService -eq 'yes' -and $servInst.State -eq 'Running') {\n Restart-Service -Name $servInst.Name\n }\n $true\n } catch {\n Write-Error -Message $_.Exception.Message\n }\n }\n @($results).Count -eq @($serviceInstances).Count\n}\n\n## Ensures args passed to Invoke-Command always have the same count to check inside of the scriptblock\n$icmArgsList = $AccountUserName, $NewPassword\n@('AccountUserNameDomain', 'ServiceName', 'RestartService') | ForEach-Object {\n if ($PSBoundParameters.ContainsKey($_) -and $_ -ne 'ServiceName') {\n $icmArgsList += $PSBoundParameters[$_]\n } elseif ($_ -eq 'ServiceName') {\n ## To process multiple services at once. This approach must be done because DVLS will not allow you to pass an array\n ## of strings via a parameter.\n $icmArgsList += $ServiceName -split ','\n } else {\n $icmArgsList += $null\n }\n}\n\n#region Create a new PSSession\n$sessParams = @{\n ComputerName = $Endpoint\n Credential = $credential\n}\n$session = New-PSSession @sessParams\n#endregion\n\n#region Load all of the local functions into the PSsession\nForEach ($func in @('decryptPassword','updateServiceUserPassword','GetServiceAccountNames','ValidateUserAccountPassword','testIsOnDomain')) {\n\t$lfunctions += \"function $((Get-Item Function:\\$func).Name) {$((Get-Item Function:\\$func).ScriptBlock)};\"\n}\n\n$loadFunctionsBlock = {\n\t. ([scriptblock]::Create($args[0]))\n}\n\nInvoke-Command -Session $session -ScriptBlock $loadFunctionsBlock -ArgumentList $lfunctions\n#endregion\n\n## Invoke the main code execution\n$invParams = @{\n Session = $session\n ScriptBlock = $scriptBlock\n ArgumentList = $icmArgsList\n}\nInvoke-Command @invParams\n\n$session | Remove-PSSession", + "command": "#requires -Version 7\n\n<#\n.SYNOPSIS\nUpdates the password for a specified service account and optionally restarts the service on a remote system.\n\n.DESCRIPTION\nThis script updates the service account password on a specified endpoint. It supports both domain and local user accounts and can handle multiple services. If specified, it can also restart the services after updating the password.\n\n.PARAMETER Endpoint\nSpecifies the target endpoint where the service(s) are running. This should be the hostname or IP address of the remote system.\n\n.PARAMETER EndpointUserName\nSpecifies the username used to authenticate against the endpoint. This should be an account with permissions to modify service settings.\n\n.PARAMETER EndpointPassword\nSpecifies the password for the EndpointUserName as a secure string.\n\n.PARAMETER AccountUserName\nSpecifies the service account username whose password needs updating. This parameter must be a flat username without domain information.\n\n.PARAMETER NewPassword\nSpecifies the new password for the service account as a secure string.\n\n.PARAMETER AccountUserNameDomain\nOptional. Specifies the domain of the service account in FQDN format if the account is a domain account.\n\n.PARAMETER ServiceName\nOptional. Specifies the name of the service that needs the service account password updated. If not provided, all services running under the specified account will be updated.\n\n.PARAMETER RestartService\nOptional. Specifies whether to restart the service after updating the password. Acceptable values are 'yes' to restart the service, or an empty string to leave the service running without restarting.\n\n.EXAMPLE\nPS C:\\> .\\script.ps1 -Endpoint 'server01' -EndpointUserName 'administrator' -EndpointPassword (ConvertTo-SecureString 'Passw0rd!' -AsPlainText -Force) -AccountUserName 'svc_account' -NewPassword (ConvertTo-SecureString 'N3wPassw0rd!' -AsPlainText -Force) -AccountUserNameDomain 'domain.local' -ServiceName 'MyService' -RestartService 'yes'\n\nThis example updates the password for the 'svc_account' user on the 'domain.local' domain, specifically for 'MyService' on 'server01', and restarts the service after the update.\n\n.NOTES\nThis script requires PowerShell Remoting to be enabled and configured on the target endpoint. Ensure credentials provided have the necessary administrative rights on the remote system.\n#>\n[CmdletBinding()]\nparam(\n [Parameter(Mandatory)]\n [ValidateNotNullOrEmpty()]\n [string]$Endpoint,\n \n [Parameter(Mandatory)]\n [ValidateNotNullOrEmpty()]\n [string]$EndpointUserName,\n\n [Parameter(Mandatory)]\n [ValidateNotNullOrEmpty()]\n [securestring]$EndpointPassword,\n\n [Parameter(Mandatory)]\n [ValidateNotNullOrEmpty()]\n [ValidatePattern(\n '^\\w+$',\n ErrorMessage = 'You must provide the AccountUserName parameter as a flat name like \"user\"; not domain\\user, et al. To specify a domain, use the AccountUserNameDomain parameter.'\n )]\n [string]$AccountUserName,\n\n [Parameter(Mandatory)]\n [ValidateNotNullOrEmpty()]\n [securestring]$NewPassword,\n\n [Parameter()]\n [string]$AccountUserNameDomain,\n\n [Parameter()]\n [string]$ServiceName,\n\n [Parameter()]\n [ValidateSet('yes', '')]\n [string]$RestartService\n)\n\n# Output the script parameters and the current user running the script\nWrite-Output -InputObject \"Running script with parameters: $($PSBoundParameters | Out-String) as [$(whoami)]\"\n\nif ($AccountUserNameDomain -and $AccountUserNameDomain -notmatch '^\\w+(\\.\\w+)+$') {\n throw 'When using the AccountUserNameDomain parameter, you must provide the domain as an FQDN (domain.local)'\n}\n\n#region Functions\nfunction newCredential([string]$UserName, [securestring]$Password) {\n New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $UserName, $Password\n}\n\nfunction testIsOnDomain {\n (Get-CimInstance -ClassName win32_computersystem).PartOfDomain\n}\n\nfunction decryptPassword {\n param(\n [securestring]$Password\n )\n try {\n $ptr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Password)\n [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($ptr)\n } finally {\n ## Clear the decrypted password from memory\n [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ptr)\n }\n}\n\nfunction updateServiceUserPassword {\n param(\n $ServiceInstance,\n [string]$UserName,\n [securestring]$Password\n ) \n\n Invoke-CimMethod -InputObject $ServiceInstance -MethodName Change -Arguments @{\n StartName = $UserName\n StartPassword = decryptPassword($Password)\n }\n}\n\nfunction GetServiceAccountNames {\n param(\n $UserName,\n $Domain = '.'\n )\n\n if (!$PSBoundParameters.ContainsKey('Domain')) {\n ## local account\n @(\n \"$Domain\\$Username\" ## domain\\user\n )\n } else {\n ## Domain account with domain as FQDN\n @(\n \"$Username@$Domain\", ## user@domain.local\n \"$($Domain.split('.')[0])\\$Username\" ## domain\\user\n )\n }\n}\n\nfunction ValidateUserAccountPassword {\n [CmdletBinding()]\n param(\n [string]$UserName,\n [string]$Domain,\n [securestring]$Password\n )\n\n try {\n Add-Type -AssemblyName System.DirectoryServices.AccountManagement\n\n if ($Domain) {\n $context = New-Object System.DirectoryServices.AccountManagement.PrincipalContext([System.DirectoryServices.AccountManagement.ContextType]::Domain, $Domain)\n } else {\n $context = New-Object System.DirectoryServices.AccountManagement.PrincipalContext([System.DirectoryServices.AccountManagement.ContextType]::Machine)\n }\n \n $context.ValidateCredentials($UserName, (decryptPassword($Password)))\n } catch {\n Write-Error \"An error occurred: $_\"\n } finally {\n if ($context) {\n $context.Dispose()\n }\n }\n}\n#endregion\n\n# Create a new PSCredential object using the provided EndpointUserName and EndpointPassword\n$credential = newCredential $EndpointUserName $EndpointPassword\n\n# Define a script block to be executed remotely on the Windows server\n$scriptBlock = {\n\n $ErrorActionPreference = 'Stop'\n\n ## Assigning to variables inside the scriptblock allows mocking of args with Pester\n $userName = $args[0]\n $userDomain = $args[2]\n $pw = $args[1]\n $serviceNames = $args[3]\n $restartService = $args[4]\n\n if ($userDomain -and !(testIsOnDomain)) {\n throw \"The AccountUserNameDomain parameter was used and the host is not on a domain. For local accounts, do not use the AccountUserNameDomain parameter.\"\n }\n\n ## Ensure the password is valid\n $valUserAcctParams = @{\n UserName = $userName\n Password = $pw\n }\n if ($userDomain) {\n $valUserAcctParams.Domain = $userDomain\n }\n $validatePwResult = ValidateUserAccountPassword @valUserAcctParams\n if (!$validatePwResult) {\n throw \"The password for user account [$($UserName)] is invalid. Did you mean to provide a domain account? If so, use the AccountUserNameDomain parameter.\"\n }\n\n $getSrvAccountNamesParams = @{\n UserName = $userName\n }\n if ($userDomain) {\n $getSrvAccountNamesParams.Domain = $userDomain\n }\n [array]$serviceAccountNames = GetServiceAccountNames @getSrvAccountNamesParams\n $startNameCimQuery = \"(StartName = '{0}')\" -f ($serviceAccountNames -join \"' OR StartName = '\") ## (StartName = 'user@domain.local' OR StartName = 'domain\\user')\n\n if (-not $serviceNames) {\n $cimFilter = $startNameCimQuery\n } else {\n $cimFilter = \"(Name='{0}') AND {1}\" -f ($serviceNames -join \"' OR Name='\"), $startNameCimQuery\n }\n $cimFilter = $cimFilter.replace('\\', '\\\\')\n \n $serviceInstances = Get-CimInstance -ClassName Win32_Service -Filter $cimFilter\n if (-not $serviceInstances) {\n throw \"No services found on [{0}] running as [{1}] could be found.\" -f (hostname), $serviceAccountNames[0]\n }\n\n $results = foreach ($servInst in $serviceInstances) {\n try {\n $updateResult = updateServiceUserPassword -ServiceInstance $servInst -Username $serviceAccountNames[0] -Password $pw\n if ($updateResult.ReturnValue -ne 0) {\n throw \"Password update for service [{0}] failed with return value [{1}]\" -f $servInst.Name, $updateResult.ReturnValue\n }\n if ($restartService -eq 'yes' -and $servInst.State -eq 'Running') {\n Restart-Service -Name $servInst.Name\n }\n $true\n } catch {\n Write-Error -Message $_.Exception.Message\n }\n }\n @($results).Count -eq @($serviceInstances).Count\n}\n\n## Ensures args passed to Invoke-Command always have the same count to check inside of the scriptblock\n$icmArgsList = $AccountUserName, $NewPassword\n@('AccountUserNameDomain', 'ServiceName', 'RestartService') | ForEach-Object {\n if ($PSBoundParameters.ContainsKey($_) -and $_ -ne 'ServiceName') {\n $icmArgsList += $PSBoundParameters[$_]\n } elseif ($_ -eq 'ServiceName') {\n ## To process multiple services at once. This approach must be done because DVLS will not allow you to pass an array\n ## of strings via a parameter.\n $icmArgsList += $ServiceName -split ','\n } else {\n $icmArgsList += $null\n }\n}\n\n#region Create a new PSSession\n$sessParams = @{\n ComputerName = $Endpoint\n Credential = $credential\n}\n$session = New-PSSession @sessParams\n#endregion\n\n#region Load all of the local functions into the PSsession\nForEach ($func in @('decryptPassword','updateServiceUserPassword','GetServiceAccountNames','ValidateUserAccountPassword','testIsOnDomain')) {\n\t$lfunctions += \"function $((Get-Item Function:\\$func).Name) {$((Get-Item Function:\\$func).ScriptBlock)};\"\n}\n\n$loadFunctionsBlock = {\n\t. ([scriptblock]::Create($args[0]))\n}\n\nInvoke-Command -Session $session -ScriptBlock $loadFunctionsBlock -ArgumentList $lfunctions\n#endregion\n\n## Invoke the main code execution\n$invParams = @{\n Session = $session\n ScriptBlock = $scriptBlock\n ArgumentList = $icmArgsList\n}\nInvoke-Command @invParams\n\n$session | Remove-PSSession", "configurationProperties": [ { "id": "359ac21c-9c0f-4be2-9210-03635220a2b6", @@ -45,4 +45,4 @@ "imageName": "SampleToolsBlue", "name": "Windows Service" } - } \ No newline at end of file + }