Update Local Administrator Password (Unmanaged + Offline Scenario)

 


<#
.SYNOPSIS
    This script allows for the automation of updating the Local Admin Password via a scheduled task.
.DESCRIPTION
    This script allows for the automation of updating the Local Admin Password via a scheduled task.
    The script will:
        1 - Update the local admin password using a scheme of your choosing
        2 - Create the scheduled task that will call the script on a set schedule
        3 - Copy the script locally into the specified directory, required for the scheduled task.
    This can be modified to update the password for any other local user not just te local admin.
    Update the scheduled task XML manually in the script OR
    Create your scheduled task via Task Scheduler then export to XML and paste the XML in the script.
.NOTES
    File Name  : Update-LocalAdminPasswordScheduledTask.ps1
    Author     : Julius Perkins - https://itinlegal.wordpress.com
.LINK
    https://itinlegal.wordpress.com/update-local-administrator-password-unmanaged-offline-scenario/
.EXAMPLE
    .\Update-LocalAdminPasswordScheduledTask.ps1
.EXAMPLE
    .\Update-LocalAdminPasswordScheduledTask.ps1 -ScheduledTaskName "UpdateLAP"
.EXAMPLE
    .\Update-LocalAdminPasswordScheduledTask.ps1 -ScriptHome "C:\ProgramData\ULAP"
.EXAMPLE
    .\Update-LocalAdminPasswordScheduledTask.ps1 -Force
.PARAMETER ScheduledTaskName
    Specify the name of the scheduled task you want to create
.PARAMETER ScriptHome
    Specify the location where the script will be stored
.PARAMETER Force
    Instructs the script to re-create the Scheduled task if it already exists and copy the script locally if it already exists.
#>
[CmdletBinding()]
Param
    (
        # This is the name of the scheduled task you want to create
        [Parameter(Mandatory=$false)]
            [string]$ScheduledTaskName = "Update Local Admin Password",

        # This is where you want to store the script locally
        [Parameter(Mandatory=$false)]
            [string]$ScriptHome = $env:WinDir + '\ULAP',

        # Do you want to force:
        #    Creation of the scheduled task even if it already exists?
        #    Copying the script locally even if it already exists?
        [Parameter(Mandatory=$false)]
            [switch]$Force = $false
    )

##*=============================================
##* VARIABLE DECLARATION
##*=============================================
#region VariableDeclaration

# Local Administrator Username
$LocalAdminUser = 'Administrator'


# Password Configuration Section
[string]$Prefix = 'ABCD'
[string]$CurrentMonth = Get-Date -Format MMM
[string]$CurrentYear = Get-Date -Format yyyy
[string]$Suffix = '&'
[string]$Separator = '-'
[string]$UpdatedPassword = $Prefix + $Separator + $CurrentMonth + $Separator + $CurrentYear + $Suffix


# Logging Section
$LogDir = Join-Path $env:WinDir Logs
$LogFile = 'UpdateLAP.log'
$Log = Join-Path $LogDir $LogFile
If(!(Test-Path -Path $LogDir -PathType Container)) { New-Item -Path $LogDir -ItemType directory | Out-Null }


# DO NOT CHANGE THIS : This is calculated by the script at run time
$ComputerName = $env:COMPUTERNAME

# DO NOT CHANGE THIS : This is calculated by the script at run time
$ScriptExecutionDirectory = split-path $MyInvocation.MyCommand.Definition -Parent

# DO NOT CHANGE THIS : This is calculated by the script at run time
$ScriptName = split-path $MyInvocation.MyCommand.Definition -Leaf

#endregion VariableDeclaration
##*=============================================
##* END VARIABLE DECLARATION
##*=============================================


##*=============================================
##* FUNCTION LISTINGS
##*=============================================
#region FunctionListings

#region Function Check-AdminRights
Function Check-AdminRights
    {
	    If (!([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator"))
            {
                Write-Output "ERROR: $ScriptName requires Administrator rights to function. Please re-run the script as an Administrator."
                Exit-Script 4005
		    }
    }
#endregion Function Check-AdminRights

#region Function Exit-Script
Function Exit-Script
    {
        [CmdletBinding()]
	    Param (
		    [Parameter(Mandatory=$false)]
		    [ValidateNotNullorEmpty()]
		        $ExitCode = 0
        )
	
	    ## Get the name of this function

        ## Determine action based on exit code
	    Switch ($ExitCode)
            {
                0 
                    {
                        Write-Output "Script completed successfully [$ExitCode]!"
                        [int32]$ExitCode = 0
                    }
    
                1
                    {
                        Write-Output "ERROR [$ExitCode] - SCRIPT FAILED"
                        [int32]$ExitCode = 1
                    }

                4005
                    {
                        Write-Output "ERROR: Script requires Administrator rights to function."
                        Write-Output "ERROR: Please re-run the script as an Administrator."
                        [int32]$ExitCode = 1
                    }

		        Default
                    {
                        Write-Output "ERROR: UNKNOWN ERROR OCCURRED: [$ExitCode]"
                        [int32]$ExitCode = -1
                    }
	        }

        Write-Output "Script exiting with exit code $ExitCode"
        Write-Output "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"

        Start-Transcript -Path $Log -Append -Force
        Exit $ExitCode
    }
#endregion


#endregion FunctionListings
##*=============================================
##* END FUNCTION LISTINGS
##*=============================================


##*=============================================
##* SCRIPT BODY
##*=============================================
#region ScriptBody

Start-Transcript -Path $Log -Append -Force

Write-Output "$(Get-Date -Format s) - >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"
Write-Output "$(Get-Date -Format s) - Running on [$env:computername]"
Write-Output "$(Get-Date -Format s) - Running as [$env:username]"

Check-AdminRights


#region Step 1 - Change the Local Administrator Password
$OriginalEAP = $ErrorActionPreference
$ErrorActionPreference = 'Stop'

$adsiComputer = [ADSI]"WinNT://$ComputerName,computer"

# Get local users list
$LocalUserList = $adsiComputer.PSBase.Children | ? { ($_.PSBase.SchemaClassName -eq 'User') -and ($_.Name -eq $LocalAdminUser) }

foreach ($LocalUser in $LocalUserList)
    {
        # Skip accounts that don't match - This should never happen though because of the logic above
        if($LocalUser.Name -ne $LocalAdminUser) { Continue }

        # Last time the password was Set: 
        Write-Output "$(Get-Date -Format s) - Password last updated for [$($LocalUser.Name)]: $((Get-Date).AddSeconds(-($LocalUser.PasswordAge.Value)))"
        
        Write-Output "$(Get-Date -Format s) - Updating Password for [$($LocalUser.Name)]"
        Try
            {
                #$LocalUser.SetPassword($NewPassword)
                Write-Output "$(Get-Date -Format s) - Successfully reset password for [$($LocalUser.Name)]: $((Get-Date).AddSeconds(-($LocalUser.PasswordAge.Value)))"
            }
        Catch [System.UnauthorizedAccessException]
            {
                Write-Output "$(Get-Date -Format s) - ERROR: ACCESS DENIED SETTING THE PASSWORD FOR [$($LocalUser.Name)]: $_"
            }
        Catch
            {
                Write-Output "$(Get-Date -Format s) - ERROR: AN UNKNOWN ERROR OCCURRED SETTING THE PASSWORD FOR [$($LocalUser.Name)]: $_"
            }
    }
$ErrorActionPreference = $OriginalEAP
#endregion Step 1 - Change the Local Administrator Password


#region Step 2 - Create the Scheduled Task
# Flag that determines whether the Scheduled Task already exists.
# We assume it doesn't.
[bool]$boolAlreadyExists = $false

# Flag that determines if we need to create the Scheduled Task
# We assume we do.
[bool]$boolCreate = $true

# Scheduled Tasks EXE
$exeSchTasks = "$env:WinDir\System32\schtasks.exe"

# Check if task already exists
if(Get-ScheduledTask -TaskName $ScheduledTaskName -ErrorAction SilentlyContinue)
    {
        # If it does we toggle the flags accordingly
        [bool]$boolAlreadyExists = $true
        [bool]$boolCreate = $false
    }

# If the Force switch was specified then we override the boolCreate flag so that the Scheduled Task is created.
if($Force -eq $true) { [bool]$boolCreate = $true }

# If the boolCreate flag is true, then we start the process.
if($boolCreate -eq $true)
    {
        If($boolAlreadyExists)
            {
                # If the task already exists, we need to delete it before reimporting.
                $DeleteTaskResult = &$exeSchTasks /Delete /TN "\$ScheduledTaskName" /F
                if($LASTEXITCODE -ne 0)
                    {
                        Write-Output "$(Get-Date -Format s) - ERROR [$LASTEXITCODE]: Failed to delete existing scheduled task: $DeleteTaskResult"
                        Exit-Script $LASTEXITCODE
                    }
            }

        # Note the formatting below MUST stay the way it is.  Do NOT indent!
        $TaskXML = @"
<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
  <RegistrationInfo>
    <Date>$(Get-Date -Format s)</Date>
    <Author>IT</Author>
    <URI>\$ScheduledTaskName</URI>
  </RegistrationInfo>
  <Triggers>
    <CalendarTrigger>
      <StartBoundary>2018-01-02T10:45:00</StartBoundary>
      <ExecutionTimeLimit>PT30M</ExecutionTimeLimit>
      <Enabled>true</Enabled>
      <ScheduleByMonthDayOfWeek>
        <Weeks>
          <Week>1</Week>
        </Weeks>
        <DaysOfWeek>
          <Tuesday />
        </DaysOfWeek>
        <Months>
          <January />
          <February />
          <March />
          <April />
          <May />
          <June />
          <July />
          <August />
          <September />
          <October />
          <November />
          <December />
        </Months>
      </ScheduleByMonthDayOfWeek>
    </CalendarTrigger>
  </Triggers>
  <Principals>
    <Principal id="Author">
      <UserId>S-1-5-18</UserId>
      <RunLevel>HighestAvailable</RunLevel>
    </Principal>
  </Principals>
  <Settings>
    <MultipleInstancesPolicy>StopExisting</MultipleInstancesPolicy>
    <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
    <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
    <AllowHardTerminate>true</AllowHardTerminate>
    <StartWhenAvailable>true</StartWhenAvailable>
    <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
    <IdleSettings>
      <StopOnIdleEnd>false</StopOnIdleEnd>
      <RestartOnIdle>false</RestartOnIdle>
    </IdleSettings>
    <AllowStartOnDemand>true</AllowStartOnDemand>
    <Enabled>true</Enabled>
    <Hidden>true</Hidden>
    <RunOnlyIfIdle>false</RunOnlyIfIdle>
    <DisallowStartOnRemoteAppSession>false</DisallowStartOnRemoteAppSession>
    <UseUnifiedSchedulingEngine>true</UseUnifiedSchedulingEngine>
    <WakeToRun>true</WakeToRun>
    <ExecutionTimeLimit>PT1H</ExecutionTimeLimit>
    <Priority>7</Priority>
    <RestartOnFailure>
      <Interval>PT5M</Interval>
      <Count>10</Count>
    </RestartOnFailure>
  </Settings>
  <Actions Context="Author">
    <Exec>
      <Command>powershell.exe</Command>
      <Arguments>-ExecutionPolicy Bypass -NoProfile -WindowStyle Hidden -File "$(Join-Path $ScriptHome $ScriptName)"</Arguments>
    </Exec>
  </Actions>
</Task>
"@

        # This is the path (directory + filename) of the XML file we're going to create
        $XMLFileName = Join-Path $env:WinDir "Temp\$ScheduledTaskName.xml"

        # If an existing XML file is found, delete it first
        If(Test-Path -Path $XMLFileName -PathType Leaf) { Remove-Item -Path $XMLFileName -Force }

        # Create the new XML file for import
        $CreateXMLReturn = New-Item $XMLFileName -Type File -Force -Value $TaskXML

        # Create the Scheduled Task by importing the XML
        $CreateResult = &$exeSchTasks /Create /XML "$XMLFileName" /TN "\$ScheduledTaskName"
        if($LASTEXITCODE -ne 0)
            {
                Write-Output "$(Get-Date -Format s) - ERROR [$LASTEXITCODE] Failed to create scheduled task [$ScheduledTaskName]: $CreateResult"
                Exit-Script $LASTEXITCODE
            }


        # I believe in houskeeping, so I want to cleaup the .XML file we created above.
        # However, the task hasn't been ingested by the time the script reaches this step :`(
        # So we wait a while before deleting the file.
        # This is optional so feel free to remove.
        Start-Sleep -Seconds 5
        If(Test-Path -Path $XMLFileName -PathType Leaf) { Remove-Item -Path $XMLFileName -Force -ErrorAction SilentlyContinue }
    }
#endregion Step 2 - Create the Scheduled Task


#region Step 3 - Copy the Script Locally
# Copy the script locally if
#     it doesn't exist locally OR
#     its being executed from elsewhere OR
#     if the Force flag is true
if(($ScriptExecutionDirectory -ne $ScriptHome) -or ($Force) -or !(Test-Path -Path "$(Join-path $ScriptHome $ScriptName)" -PathType Leaf))
    {
        if(Test-Path $($MyInvocation.MyCommand.Definition))
            {
                Write-Output "$(Get-Date -Format s) - Copying Script Locally"
                Write-Output "$(Get-Date -Format s) - FROM: [$($MyInvocation.MyCommand.Definition)]"
                Write-Output "$(Get-Date -Format s) - TO  : [$(Join-path $ScriptHome $ScriptName)]"
                If(!(Test-Path -Path $ScriptHome -PathType Container)) { New-Item -Path $ScriptHome -ItemType directory | Out-Null }
                Try { Copy-Item -Path $($MyInvocation.MyCommand.Definition) -Destination $(Join-path $ScriptHome $ScriptName) -Force -ErrorAction Stop }
                Catch { Write-Output "$(Get-Date -Format s) - ERROR Failed to copy [$($MyInvocation.MyCommand.Definition)] to [$(Join-path $ScriptHome $ScriptName)]: $_" }
            }
        Else { Write-Output "$(Get-Date -Format s) - ERROR Can't copy script to [$ScriptHome] because source path is NOT accessible: [$($MyInvocation.MyCommand.Definition)]" }
    }
else { Write-Output "$(Get-Date -Format s) - Script running from expected location: [$($MyInvocation.MyCommand.Definition) == $(Join-path $ScriptHome $ScriptName)]" }
#endregion Step 3 - Copy the Script Locally

Exit-Script -ExitCode 0

#endregion ScriptBody
##*=============================================
##* END SCRIPT BODY
##*=============================================