Integrating LAPS with Citrix VDI

Overview

The Local Administrator Password Solution is a wonderful tool, however due to the non-persistant nature of some environments, such as Citrix VDI, it can become a security vulnerability.

After reading an interesting article on Password Wars: Randomizing Local Admin Passwords in Non-persistent Environments I realized the importance of keeping LAPS passwords randomized in a Citrix VDI environment.

This was a good article, but the link to the script is invalid. I did, however, find a .vbs version of the script here. But, this VBSB script is antiquated, so I recreated the logic in PowerShell.

Antiquated VBS LAPS Shutdown Script


Set objSysInfo = CreateObject("ADSystemInfo")
Set objComputer = GetObject("LDAP://" & objSysInfo.ComputerName)
' Change ms-Mcs-AdmPwdExpirationTime attribute to 0 
objComputer.Put "ms-Mcs-AdmPwdExpirationTime", "0"
' Write change to AD
objComputer.SetInfo
' Sleep 3 minutes
WScript.Sleep(180000)
Set WshShell = CreateObject("Wscript.Shell")
' Run GPUpdate force and target only the computer policies
Result = WshShell.Run("cmd /c echo n | gpupdate /target:computer /force",0,true)
' Exit with code
Wscript.Quit(Result)

My Solution: Reset-LAPSPasswordExpiration

I created a script, Reset-LAPSPasswordExpiration.ps1, attached below which aims to solve this problem by randomly generating a password upon either VDI startup or shutdown and synchronizing those results with the LAPS password store in Active Directory. This requires RSAT to be on the Citrix server because it uses Set-ADComputer.

If run it locally on the Citrix server at startup:


  Reset-LAPSPasswordExpiration -RunOnLocalHost -logic Startup 

If run local on the Citrix server at shutdown:


  Reset-LAPSPasswordExpiration -RunOnLocalHost -logic Shutdown 

To run again a batch of Citrix servers remotely:


  Reset-LAPSPasswordExpiration -CSVFile Reset-LAPSPasswordExpiration-Import.csv -Logic Shutdown 

To run against a single server remotely:


  Reset-LAPSPasswordExpiration -Hostname CITRIXSERVER001 -Logic Shutdown 

Sample Output:

Logging Output

The script has built in logging functionality which is fully compatible with CMTrace.exe

PowerShell Function: Reset-LAPSPasswordExpiration


<#
.Synopsis
   Reset the LAPS Password Expiration Date
.DESCRIPTION
   The tool, Reset-LAPSPasswordExpiration.ps1 was written by David Maiolo which will reset the LAPS Password expiration date. Useful on non-persistent VDIs
.EXAMPLE
   Reset-LAPSPasswordExpiration -CSVFile laps_computers_import.csv -Logic Startup
.EXAMPLE
   Reset-LAPSPasswordExpiration -Hostname LT061222 -Logic Shutdown
.EXAMPLE
   Reset-LAPSPasswordExpiration -RunOnLocalHost -logic Startup
#>


function New-DGMCMTraceLog
{
  param (
  [Parameter(Mandatory=$true)]
  $message,
  [Parameter(Mandatory=$true)]
  $component,
  [Parameter(Mandatory=$true)]
  $type )

  switch ($type)
  {
    1 { $type = "Info" }
    2 { $type = "Warning" }
    3 { $type = "Error" }
    4 { $type = "Verbose" }
  }

  if (($type -eq "Verbose") -and ($Global:Verbose))
  {
    $toLog = "{0} `$$<{1}><{2} {3}><thread={4}>" -f ($type + ":" + $message), ($Global:ScriptName + ":" + $component), (Get-Date -Format "MM-dd-yyyy"), (Get-Date -Format "HH:mm:ss.ffffff"), $pid
    $toLog | Out-File -Append -Encoding UTF8 -FilePath ("filesystem::{0}" -f $Global:LogFile)
    Write-Host $message
  }
  elseif ($type -ne "Verbose")
  {
    $toLog = "{0} `$$<{1}><{2} {3}><thread={4}>" -f ($type + ":" + $message), ($Global:ScriptName + ":" + $component), (Get-Date -Format "MM-dd-yyyy"), (Get-Date -Format "HH:mm:ss.ffffff"), $pid
    $toLog | Out-File -Append -Encoding UTF8 -FilePath ("filesystem::{0}" -f $Global:LogFile)
    if ($type -eq 'Info') { Write-Host $message }
    if ($type -eq 'Warning') { Write-Host $message -ForegroundColor Yellow}
    if ($type -eq 'Error') { Write-Host $message -ForegroundColor Red}
    

  }
  if (($type -eq 'Warning') -and ($Global:ScriptStatus -ne 'Error')) { $Global:ScriptStatus = $type }
  if ($type -eq 'Error') { $Global:ScriptStatus = $type }

  if ((Get-Item $Global:LogFile).Length/1KB -gt $Global:MaxLogSizeInKB)
  {
    $log = $Global:LogFile
    Remove-Item ($log.Replace(".log", ".lo_"))
    Rename-Item $Global:LogFile ($log.Replace(".log", ".lo_")) -Force
  }
} 

function GetScriptDirectory
{
  $invocation = (Get-Variable MyInvocation -Scope 1).Value
  Split-Path $invocation.MyCommand.Path
} 

function Reset-LAPSPasswordExpiration
{
    [CmdletBinding()]
    [Alias()]
    [OutputType([int])]
    Param
    (
        # Param1 help description
        [Parameter(Mandatory=$true,
                   ValueFromPipeline=$true,
                   Position=0,
                   ParameterSetName='CSV File')]
                   [ValidateScript({(Test-Path $_)})]
                   $CSVFile,
        # Param2 help description
        [Parameter(Mandatory=$true,
                   ValueFromPipeline=$true,
                   Position=1,
                   ParameterSetName='Single Computer')]
                   [ValidateScript({(Get-ADComputer -Identity $_).objectclass -eq 'computer' })]
                   [String]$Hostname,
        # Param3 help description
        [Parameter(Mandatory=$true,
                   ValueFromPipeline=$true,
                   Position=2,
                   ParameterSetName='Local Host')]
                   [Switch]$RunOnLocalHost,
        # Param4 help description
        [Parameter(Mandatory=$true,
                   ValueFromPipeline=$true,
                   Position=3)]
                   [ValidateSet("Shutdown","Startup")]
                   [String]$Logic
    )

    Begin
    {
        
        $path = (get-item -Path .).FullName 
        
        if ($RunOnLocalHost){
            $localhostname = $env:computername
            $csv = [PSCustomObject]@{
                Hostname = $localhostname}
        }
        elseif ($CSVFile -ne $null){
            Write-Host "Importing $CSVFile..."
            $csv = import-csv "$CSVFile"
        }else{
            $csv = [PSCustomObject]@{
                Hostname = $Hostname}
        }
        Write-Host "=========================================="
        Write-Host "LAPS Password Expiration Date Reset Tool"
        Write-Host "=========================================="
        Write-Host "v0.5 (2017-12-26) by dmaiolo"
        New-DGMCMTraceLog -message ("Starting Logging for Reset-LAPSPasswordExpiration") -component "Main()" -type 1 
    }
    Process
    {
    
    $computers = @();
        
    $csv | foreach-object {
        $h = $_.Hostname
    

        if(Test-Connection -ComputerName $h -Count 1 -Quiet){
            
            $comp = Get-ADComputer $h -Properties ms-MCS-AdmPwdExpirationTime
            
            try{
                $currentexpirationtime = $([datetime]::FromFileTime([convert]::ToInt64($comp.'ms-MCS-AdmPwdExpirationTime',10)))
                New-DGMCMTraceLog -message ("$h`: The current LAPS password expiration is $currentexpirationtime.") -component "Main()" -type 1
            }catch{
                New-DGMCMTraceLog -message ("$h`: The current LAPS password has an unknown expiration or is already clear.") -component "Main()" -type 2
            }

            Write-Host "$h`: Resetting ms-MCS-AdmPwdExpirationTime..."
            try{
                Set-ADComputer $h -Clear "ms-MCS-AdmPwdExpirationTime"
                New-DGMCMTraceLog -message ("$h`: ms-MCS-AdmPwdExpirationTime has been cleared succesfully.") -component "Main()" -type 1
            }catch{
                New-DGMCMTraceLog -message ("$h`: ms-MCS-AdmPwdExpirationTime could not be cleared.") -component "Main()" -type 3
            }
            if ($Logic -eq "Startup"){
                New-DGMCMTraceLog -message ("$h`: Startup Sequence Logic Was Initiated.") -component "Main()" -type 1
                Write-Host "$h`: Sleeping 3 seconds..."
                sleep 3

                Write-Host "$h`: Running GPUpdate /Force on $h ..."
                if ($RunOnLocalHost){
                    try{
                        $command = "GPUPdate /Target:Computer /Force"
                        Invoke-Expression -Command:"$command"
                        New-DGMCMTraceLog -message ("$h`: Group Policy Was Succesfully Updated.") -component "Main()" -type 1
                    }catch{
                        New-DGMCMTraceLog -message ("$h`: Group Policy Could Not Update.") -component "Main()" -type 3
                    }
                }else{
                    try{
                        Invoke-GPUpdate -Computer $h -Force
                        New-DGMCMTraceLog -message ("$h`: Group Policy Was Succesfully Updated.") -component "Main()" -type 1
                    }catch{
                        New-DGMCMTraceLog -message ("$h`: Group Policy Could Not Update.") -component "Main()" -type 3
                    }
                }
            }elseif ($Logic -eq "Shutdown"){
                New-DGMCMTraceLog -message ("$h`: Shutdown Sequence Logic Was Initiated. Skipping GPUpdate") -component "Main()" -type 1
            }
            Write-Host "$h`: Sleeping 5 Seconds..."
            sleep 5
            try{
                $currentexpirationtime = $([datetime]::FromFileTime([convert]::ToInt64($comp.'ms-MCS-AdmPwdExpirationTime',10)))
                New-DGMCMTraceLog -message ("$h`: The updated LAPS password expiration is $currentexpirationtime.") -component "Main()" -type 1
            }catch{
                New-DGMCMTraceLog -message ("$h`: The updated LAPS password has an unknown expiration.") -component "Main()" -type 3
            }
        }
        else{
            New-DGMCMTraceLog -message ("$h`: is offline.") -component "Main()" -type 2
        }
       }
    }
    End
    {
       Write-Host "==============================================================="
       Write-Host "Log File of Results Generated at $Global:LogFile VIEW WITH CMTRACE.EXE"
       New-DGMCMTraceLog -message ("Ending Logging for Reset-LAPSPasswordExpiration") -component "Main()" -type 1
    }
}

$VerboseLogging = "true"
[bool]$Global:Verbose = [System.Convert]::ToBoolean($VerboseLogging)
#$Global:LogFile = Join-Path (GetScriptDirectory) "Reset-LAPSPasswordExpiration_$(Get-Date -Format dd-MM-yyyy).log"
$Global:LogFile = "\\vendscr001prd\Scripts\Reset-LAPSPasswordExpiration\Reset-LAPSPasswordExpiration.log"
$Global:MaxLogSizeInKB = 10240
$Global:ScriptName = 'Reset-LAPSPasswordExpiration.ps1' 
$Global:ScriptStatus = 'Success'

Leave a Comment

Your email address will not be published.