Windows 10, Windows Server

Account Password Rotation | Randomly Generated



INTRO

User account password rotation can be randomly generated with PowerShell and implemented with RMM tools. When working in the IT field it feels like you never hear the end about passwords. Security regarding passwords, age of passwords, password history, lockouts, complexity, character length, multi-factor, zero-trust, and the list goes on. With the use these days of RMM tools, things have gotten a little easier to manage. Best practices regarding passwords are all over the place but how many people actually follow them?

CONSIDERATIONS

When thinking about creating scripts to accomplish password rotation, here are some considerations:

  • Have a password change on a schedule.
  • Have a password be random and complex.
  • Have access to the password set.
  • Verify password set is not stored on the endpoint for attackers to potentially find.

RMM USAGE

I spent some time researching how to do this and put together and tested some scripts. Below you will find them. This example will be for Datto RMM. With Datto RMM we can schedule the job for whatever we want. When the job runs the PowerShell script that is running against an endpoint, the attempt is shown in the Security event log. The event log shows Datto copies the script down to the endpoint and runs it from the following location:

  • C:\ProgramData\CentraStage\Packages\d0b49e6e-9eb6-48bf-8e28-99271b329f93#\command.ps1

The “d0b49e6e-9eb6-48bf-8e28-99271b329f93#” part being the random unique identifier of the script path. The output of the command containing the password is not logged and stored on the endpoint. Once the job is completed, you can click the “StdOut” link next to the job in Datto RMM to view the output which contains the password that was generated that you can copy and paste, and when using Splashtop or Web remote to the endpoint and you can past clipboard as keystrokes to the password fields remotely.

The output was monitored and captured when it ran in memory but again is not stored on the endpoint itself. The nice thing about this is that you don’t need to store the password anywhere else. Someone might say, well what if malware was present on the machine monitoring this in memory? My answer would then be you’re already compromised and the malware obtaining the password doesn’t really matter anymore.

PASSWORD ROTATION

Here are some PowerShell scripts below that simply just rotate the password for either a domain or local account called “Administrator”. So, for DC servers, it would rotate the Domain Administrator and client endpoints, the Local Built-in Administrator account.

Domain User Account:

PowerShell
Get-ADUser -Identity "Administrator" -Properties PasswordLastSet,LastLogon,LastLogonDate
 
Function Create-String([Int]$Size = 16, [Char[]]$CharSets = "ULNS", [Char[]]$Exclude) {
    $Chars = @(); $TokenSet = @()
    If (!$TokenSets) {$Global:TokenSets = @{
        U = [Char[]]'ABCDEFGHIJKLMNOPQRSTUVWXYZ'                                #Upper case
        L = [Char[]]'abcdefghijklmnopqrstuvwxyz'                                #Lower case
        N = [Char[]]'0123456789'                                                #Numerals
        S = [Char[]]'!"#$%&''()*+,-./:;<=>?@[\]^_`{|}~'                         #Symbols
    }}
    $CharSets | ForEach {
        $Tokens = $TokenSets."$_" | ForEach {If ($Exclude -cNotContains $_) {$_}}
        If ($Tokens) {
            $TokensSet += $Tokens
            If ($_ -cle [Char]"Z") {$Chars += $Tokens | Get-Random}             #Character sets defined in upper case are mandatory
        }
    }
    While ($Chars.Count -lt $Size) {$Chars += $TokensSet | Get-Random}
    ($Chars | Sort-Object {Get-Random}) -Join ""                                #Mix the (mandatory) characters and output string
}; Set-Alias Create-Password Create-String -Description "Generate a random string (password)"; Write-Output ""
 
$Passwd = Create-Password 25 UNLS; Write-Output "This is the new randomly generated password for the Built-in Domain/Local Server Administrator account:"; Write-Output "";"";""; $Passwd 
 
$PasswdSecStr = ConvertTo-SecureString $Passwd -AsPlainText -Force
Set-ADAccountPassword -Identity "Administrator" -Reset -NewPassword $PasswdSecStr
 
Get-ADUser -Identity "Administrator" -Properties PasswordLastSet | Format-Table PasswordLastSet

Local User Account:

PowerShell
Get-LocalUser | Where-Object {$_.Name -eq 'Administrator' -and $_.Name -NotLike '\Administrator'} | Select-Object PSComputername, Caption, Name, FullName, Description, Status, Disabled, Domain, LocalAccount, AccountType, Lockout, PasswordRequired, PasswordChangeable, PasswordExpires, SID, SIDType, Lastlogon, PasswordLastSet | Format-List
 
Function Create-String([Int]$Size = 16, [Char[]]$CharSets = "ULNS", [Char[]]$Exclude) {
    $Chars = @(); $TokenSet = @()
    If (!$TokenSets) {$Global:TokenSets = @{
        U = [Char[]]'ABCDEFGHIJKLMNOPQRSTUVWXYZ'                                #Upper case
        L = [Char[]]'abcdefghijklmnopqrstuvwxyz'                                #Lower case
        N = [Char[]]'0123456789'                                                #Numerals
        S = [Char[]]'!"#$%&''()*+,-./:;<=>?@[\]^_`{|}~'                         #Symbols
    }}
    $CharSets | ForEach {
        $Tokens = $TokenSets."$_" | ForEach {If ($Exclude -cNotContains $_) {$_}}
        If ($Tokens) {
            $TokensSet += $Tokens
            If ($_ -cle [Char]"Z") {$Chars += $Tokens | Get-Random}             #Character sets defined in upper case are mandatory
        }
    }
    While ($Chars.Count -lt $Size) {$Chars += $TokensSet | Get-Random}
    ($Chars | Sort-Object {Get-Random}) -Join ""                                #Mix the (mandatory) characters and output string
}; Set-Alias Create-Password Create-String -Description "Generate a random string (password)"; Write-Output ""
 
$Passwd = Create-Password 25 UNLS; Write-Output "This is the new randomly generated password for the local Built-in Administrator account:"; Write-Output "";"";""; $Passwd 
 
$PasswdSecStr = ConvertTo-SecureString $Passwd -AsPlainText -Force
Set-LocalUser -Name "Administrator" -Password $PasswdSecStr
 
Get-LocalUser | Where-Object {$_.Name -eq 'Administrator' -and $_.Name -NotLike '\Administrator'} | Select-Object PasswordLastSet | Format-List

ACCOUNT CREATION & PASSWORD ROTATION

The password rotation scripts work well, but I find the best use of password rotation is when combined with account creation. So, if I take over a site with many endpoints, I want to have a local Administrator account for administrative purposes that is only for my use and the use of others in my MSP organization. I could use the local built-in Administrator account in Windows, but many cybersecurity organizations combined with insurance agencies, and other cybersecurity compliance standards such as PCI-DSS want the built-in Administrator account disabled and it makes sense.

So, what I do is create a custom one called Support. So below are scripts that will check for an account called Support and rotate the password, if the account is not found it’s created, and then the password is rotated.

Domain User Account:

PowerShell
$ErrorActionPreference = 'Stop'
$VerbosePreference = 'Continue'
 
$USERNAME = "Support"
 
Function Create-String([Int]$Size = 16, [Char[]]$CharSets = "ULNS", [Char[]]$Exclude) {
    $Chars = @(); $TokenSet = @()
    If (!$TokenSets) {$Global:TokenSets = @{
        U = [Char[]]'ABCDEFGHIJKLMNOPQRSTUVWXYZ'                                #Upper case
        L = [Char[]]'abcdefghijklmnopqrstuvwxyz'                                #Lower case
        N = [Char[]]'0123456789'                                                #Numerals
        S = [Char[]]'!"#$%&''()*+,-./:;<=>?@[\]^_`{|}~'                         #Symbols
    }}
    $CharSets | ForEach {
        $Tokens = $TokenSets."$_" | ForEach {If ($Exclude -cNotContains $_) {$_}}
        If ($Tokens) {
            $TokensSet += $Tokens
            If ($_ -cle [Char]"Z") {$Chars += $Tokens | Get-Random}             #Character sets defined in upper case are mandatory
        }
    }
    While ($Chars.Count -lt $Size) {$Chars += $TokensSet | Get-Random}
    ($Chars | Sort-Object {Get-Random}) -Join ""                                #Mix the (mandatory) characters and output string
}; Set-Alias Create-Password Create-String -Description "Generate a random string (password)"; Write-Output ""
 
# Generating a new random password.
$Passwd = Create-Password 25 UNLS
 
# Converting random generated password to a secure string for insertion.
$PasswdSecStr = ConvertTo-SecureString $Passwd -AsPlainText -Force
 
# Declare LocalUser Object
$ObjADUser = $null
 
try {
    Write-Verbose "Searching for the AD user account named $($USERNAME) in Active Directory."
    $ObjADUser = Get-ADUser -Identity $USERNAME
    Write-Verbose "The domain user account named $($USERNAME) was found."
 
    Write-Verbose "Gathering details on the $USERNAME account."
    Get-ADUser -Identity $USERNAME -Properties * | Select-Object Name, DisplayName, Description, Enabled, LockedOut, PasswordNotRequired, PasswordNeverExpires, LastBadPasswordAttempt, SID, LastLogonDate, PasswordLastSet | Format-List
 
    Write-Verbose "Setting the new randomly generated password."
    Set-ADAccountPassword -Identity $USERNAME -Reset -NewPassword $PasswdSecStr
    
    Write-Verbose "This is the newly random generated password for the AD user account named $($USERNAME):"; Write-Output ""; $Passwd
    Get-ADUser -Identity $USERNAME -Properties * | Select-Object PasswordLastSet | Format-List
}
catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] {
    "The AD user account named $($USERNAME) was not found." | Write-Warning
}
catch {
    "An unspecifed error has occurred." | Write-Error
    Exit # Stop PowerShell! 
}
 
# Create the AD user account if it was not found.
if (!$ObjADUser) {
    Write-Verbose "Creating an AD user account named $($USERNAME) and setting the randomly generated password."
    New-ADUser -Name $USERNAME -AccountPassword $PasswdSecStr -PasswordNeverExpires $True -Enabled $True
 
    Write-Verbose "Adding the $($USERNAME) AD account to the Administrators Group."
    Add-ADGroupMember -Identity Administrators -Members $USERNAME
 
    Write-Verbose "Gathering details on the $USERNAME AD account."
    Get-ADUser -Identity $USERNAME -Properties * | Select-Object Name, DisplayName, Description, Enabled, LockedOut, PasswordNotRequired, PasswordNeverExpires, LastBadPasswordAttempt, SID, LastLogonDate, PasswordLastSet | Format-List
 
    Write-Verbose "This is the newly random generated password for the AD user account named $($USERNAME):"; Write-Output ""; $Passwd
    Get-ADUser -Identity $USERNAME -Properties * | Select-Object PasswordLastSet | Format-List
}

Local User Account:

PowerShell
$ErrorActionPreference = 'Stop'
$VerbosePreference = 'Continue'
 
$USERNAME = "Support"
 
Function Create-String([Int]$Size = 16, [Char[]]$CharSets = "ULNS", [Char[]]$Exclude) {
    $Chars = @(); $TokenSet = @()
    If (!$TokenSets) {$Global:TokenSets = @{
        U = [Char[]]'ABCDEFGHIJKLMNOPQRSTUVWXYZ'                                #Upper case
        L = [Char[]]'abcdefghijklmnopqrstuvwxyz'                                #Lower case
        N = [Char[]]'0123456789'                                                #Numerals
        S = [Char[]]'!"#$%&''()*+,-./:;<=>?@[\]^_`{|}~'                         #Symbols
    }}
    $CharSets | ForEach {
        $Tokens = $TokenSets."$_" | ForEach {If ($Exclude -cNotContains $_) {$_}}
        If ($Tokens) {
            $TokensSet += $Tokens
            If ($_ -cle [Char]"Z") {$Chars += $Tokens | Get-Random}             #Character sets defined in upper case are mandatory
        }
    }
    While ($Chars.Count -lt $Size) {$Chars += $TokensSet | Get-Random}
    ($Chars | Sort-Object {Get-Random}) -Join ""                                #Mix the (mandatory) characters and output string
}; Set-Alias Create-Password Create-String -Description "Generate a random string (password)"; Write-Output ""
 
# Generating a new random password.
$Passwd = Create-Password 25 UNLS
 
# Converting random generated password to a secure string for insertion.
$PasswdSecStr = ConvertTo-SecureString $Passwd -AsPlainText -Force
 
# Declare LocalUser Object
$ObjLocalUser = $null
 
try {
    Write-Verbose "Searching for the local user account named $($USERNAME) in the LocalUser database."
    $ObjLocalUser = Get-LocalUser $USERNAME
    Write-Verbose "The local user account named $($USERNAME) was found."
 
    Write-Verbose "Gathering details on the $USERNAME account."
    Get-LocalUser | Where-Object {$_.Name -eq $USERNAME -and $_.Name -NotLike '\$USERNAME'} | Select-Object Name, FullName, Description, Status, Disabled, Domain, LocalAccount, AccountType, Lockout, PasswordRequired, PasswordChangeable, PasswordExpires, SID, Lastlogon, PasswordLastSet | Format-List
 
    Write-Verbose "Setting the new randomly generated password."
    Set-LocalUser -Name $USERNAME -Password $PasswdSecStr
 
    
    Write-Verbose "This is the newly random generated password for the local user account named $($USERNAME):"; Write-Output ""; $Passwd
    Write-Output "";
    Get-LocalUser | Where-Object {$_.Name -eq $USERNAME -and $_.Name -NotLike '\$USERNAME'} | Select-Object PasswordLastSet | Format-List
}
catch [Microsoft.PowerShell.Commands.UserNotFoundException] {
    "The local user account named $($USERNAME) was not found." | Write-Warning
}
catch {
    "An unspecifed error has occurred." | Write-Error
    Exit # Stop PowerShell! 
}
 
# Create the user account if it was not found.
if (!$ObjLocalUser) {
    Write-Verbose "Creating a local user account named $($USERNAME) and setting the randomly generated password."
    New-LocalUser $USERNAME -Password $PasswdSecStr -FullName $USERNAME -PasswordNeverExpires;
 
    Write-Verbose "Adding the $($USERNAME) account to the Local Administrators Group."
    Add-LocalGroupMember -Group Administrators -Member $USERNAME
 
    Write-Verbose "Gathering details on the $USERNAME account."
    Get-LocalUser | Where-Object {$_.Name -eq $USERNAME -and $_.Name -NotLike '\$USERNAME'} | Select-Object Name, FullName, Description, Status, Disabled, Domain, LocalAccount, AccountType, Lockout, PasswordRequired, PasswordChangeable, PasswordExpires, SID, Lastlogon, PasswordLastSet | Format-List
 
    Write-Verbose "This is the newly random generated password for the local user account named $($USERNAME):"; Write-Output ""; $Passwd
    Write-Output "";
    Get-LocalUser | Where-Object {$_.Name -eq $USERNAME -and $_.Name -NotLike '\$USERNAME'} | Select-Object PasswordLastSet | Format-List
}

END RESULT

Datto RMM has removed the ability to view job output in Legacy UI under each device, it can still be viewed in legacy UI under the actual job itself, but then you must scroll for a while so below is the end result visible under activities in New UI. Personally, I like legacy UI better.

Datto RMM Local User Account Creation and Password Rotation Script Schedule Example

TROUBLESHOTING

I found that some Windows 8.1 and Windows Server 2012 R2 clients were not able to process these PowerShell scripts. After spending and hour or two researching, I discovered it’s because they need to have Windows Management Framework 5.1 installed. Once installed the scripts ran as expected.

Here is some untested code to install the patch silently, will need to check prerequisites first, have yet to confirm it works but it’s a start.

BAT (Batchfile)
wusa.exe Win8.1AndW2K12R2-KB3191564-x64.msu /quiet /norestart

CONCLUSION

Well, there you go, give it a try and let me know what you think. The nice part about having something like this on a schedule is that if someone leaves the MSP, or you’re away, the password will be changed without any effort on your part. You could have this run every hour if you wanted to, and if any funny business goes down, the offender won’t have much time to act until the next rotation.

Do you know a better way to do this? If so, let me know!

0 replies

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply

Your email address will not be published. Required fields are marked *