Part 8 (Final): Managing Local Administrator Passwords

This is Part 8 and the final part of a multi-part series on managing local admin passwords. In this part I will provide PWDViewer which is a XAML secure password viewer that will allow authorized users to securely retrieve the username and password stored in the password confidential attribute.  In case you missed it:

Here is Part 1 - Overview

Here is Part 2 - Random Password Generation

Here is Part 3 - Secure Active Directory Attribute Update

Here is Part 4 - Update Local Account's Password

Here is Part 5 - Logging The Update Process

Here is Part 6 - Extending The Active Directory Schema

Here is Part 7 - Controlling The Password Change Script Functions

If you want to skip straight to the script you can download the script which is attached to this post as a text file. The attached script MUST be edited in PowerShell ISE to display properly. If you open the attached script in Notepad with word wrap enabled or any text editor with word wrap enabled you may be unable to properly run the script afterwards.

The Problem

So if you have followed this series or if you jumped straight to Part 7 and downloaded the completed script you now have a place to securely store the workstation's local admin password, a way to securely change each workstation's local admin password, and a way to securely transmit that password to its storage location (Active Directory). The final problem remaining is how to securely retrieve that local admin password when needed.

The Solution

The following script is a secure PowerShell driven XAML GUI that will securely retrieve the username and password for a computer object and display them on the screen so that system administrators can use the credentials to log into the workstation or server using the local admin credentials. If you followed all of the steps in this series and created a confidential attribute as recommended then ensure you edit the value of $CustomAttribute variable prior to running the script since its default value is "description".

If you read my blog post titled Integrating XAML into PowerShell then some of the following code will look familiar to you. The script starts off by creating the XAML form, then it loads the form elements into PowerShell variables, contains some code to securely connect to Active Directory using Kerberos, then displays the form.

#===========================================================================
# Edit the Name of the Custom Attribute Below (Default = description)
#===========================================================================
$CustomAttribute = "description"

[void][System.Reflection.Assembly]::LoadWithPartialName('presentationframework')
[xml]$XAML = @'
<Window
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    Title="Computer Object Attribute Viewer" Height="480" Width="640" WindowStartupLocation="CenterScreen" ResizeMode="NoResize" WindowStyle='None'>
    <Grid>
        <TextBlock TextAlignment="Center" HorizontalAlignment="Center" TextWrapping="Wrap" VerticalAlignment="Top" Width="176" Height="443" FontSize="24" Margin="0,37,464,0">
            <TextBlock.Background>
                <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                    <GradientStop Color="White" Offset="0"/>
                    <GradientStop Color="#FFB8F1A8" Offset="1"/>
                </LinearGradientBrush>
            </TextBlock.Background></TextBlock>
        <Button Name="btnExit" Content="EXIT" HorizontalAlignment="Left" Margin="0,440,0,0" VerticalAlignment="Top" Width="176" BorderBrush="{x:Null}" Height="40" FontSize="16"/>
        <TextBlock TextAlignment="Center" HorizontalAlignment="Center" TextWrapping="Wrap" Text="Active Directory Computer Object Attribute Viewer" VerticalAlignment="Top" Width="640" Height="37" FontSize="24">
            <TextBlock.Background>
                <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                    <GradientStop Color="#FFB8F1A8" Offset="0"/>
                    <GradientStop Color="White" Offset="1"/>
                </LinearGradientBrush>
            </TextBlock.Background>
        </TextBlock>
        <Button Name="btnSubmit" Content="Submit" HorizontalAlignment="Left" Margin="122,40,0,0" VerticalAlignment="Top" Width="51" Height="26" BorderBrush="{x:Null}" Padding="0" BorderThickness="0"/>
        <TextBox Name="txtComputerName" TextAlignment="Center" VerticalContentAlignment="Center"  HorizontalAlignment="Left" Height="26" Margin="2,40,0,0" TextWrapping="Wrap" MaxLength="15" Text="Computer Name" VerticalAlignment="Top" Width="120"/>
        <TextBlock TextAlignment="Center" TextWrapping="Wrap" Margin="176,40,0,414" FontSize="16"><Run Text="Results"/></TextBlock>
        <Label HorizontalAlignment="Left" Margin="181,68,0,0" VerticalAlignment="Top" Width="449" Background="#FFE6F8E6" Height="25" Content="SAMAccountName"/>
        <Label Content="Description" HorizontalAlignment="Left" Margin="181,99,0,0" VerticalAlignment="Top" Width="449" Background="#FFE6F8E6" Height="25"/>
        <Label Content="Distinguished Name" HorizontalAlignment="Left" Margin="181,130,0,0" VerticalAlignment="Top" Width="449" Background="#FFE6F8E6" Height="25" Padding="5,5,5,3"/>
        <Label Content="Operating System" HorizontalAlignment="Left" Margin="181,159,0,0" VerticalAlignment="Top" Width="449" Background="#FFE6F8E6" Height="25"/>
        <Label Content="Local Admin Username" HorizontalAlignment="Left" Margin="181,188,0,0" VerticalAlignment="Top" Width="449" Background="#FFE6F8E6" Height="25"/>
        <TextBox Name="txtSAMAccountName" TextWrapping="NoWrap" Margin="327,69,11,388" IsReadOnly="True"/>
        <TextBox Name="txtDescription" TextWrapping="NoWrap" Margin="327,100,11,357" IsReadOnly="True"/>
        <TextBox Name="txtDistinguishedName" TextWrapping="NoWrap" Margin="327,131,11,326" IsReadOnly="True"/>
        <TextBox Name="txtOperatingSystem" TextWrapping="NoWrap" Margin="327,160,11,297" IsReadOnly="True"/>
        <TextBox Name="txtLocalAdminUserName" TextWrapping="NoWrap" Margin="327,189,11,268" IsReadOnly="True"/>
        <Label Content="Local Admin Password" HorizontalAlignment="Left" Margin="181,218,0,0" VerticalAlignment="Top" Width="449" Background="#FFE6F8E6" Height="25"/>
        <TextBox Name="txtLocalAdminPassword" TextWrapping="NoWrap" Margin="327,219,11,238" IsReadOnly="True"/>
        <TextBlock Name="txtFeedback" TextWrapping="Wrap" Margin="2,68,467,380" Foreground="#FFFF0006"/>
        <TextBlock Name="txtResultsFeedback" TextWrapping="NoWrap" Margin="327,247,11,201" Foreground="#FFFF0006"/>
    </Grid>
</Window>
'@
#Read XAML
$reader=(New-Object System.Xml.XmlNodeReader $xaml)
#$Form=[Windows.Markup.XamlReader]::Load( $reader )
try{$Form=[Windows.Markup.XamlReader]::Load( $reader )}
catch{Write-Host "Unable to load Windows.Markup.XamlReader. An Unhandled Exception Occured"; exit}
#===========================================================================
# Global Variable Store
#===========================================================================
$CustomAttribute = $CustomAttribute.ToLower()

#===========================================================================
# Store Form Objects In PowerShell
#===========================================================================
$xaml.SelectNodes("//*[@Name]") | %{Set-Variable -Name ($_.Name) -Value $Form.FindName($_.Name)}

#===========================================================================
# Current Build Number
#===========================================================================
$version = "v1.0.103113" #Initial Release

#===========================================================================
# Add events to Form Objects
#===========================================================================

#Configures FOrm Actions
$btnExit.Add_Click({$form.Close()})
$txtComputerName.Add_GotKeyboardFocus({if($txtComputerName.Text -eq "Computer Name"){$txtComputerName.Text = ""}})
$btnSubmit.Add_Click({fnClear;fnAttributeSearch -ComputerName $txtComputerName.Text})

#==================================================================================
# Form Clear
#==================================================================================
function fnClear{
    $txtSAMAccountName.Text = ""
    $txtDescription.Text = ""
    $txtDistinguishedName.Text = ""
    $txtOperatingSystem.Text = ""
    $txtLocalAdminUserName.Text = ""
    $txtLocalAdminPassword.Text = ""
    $txtResultsFeedback.Text = ""
}
#==================================================================================
# ADSI Attribute Lookup
#==================================================================================
function fnAttributeSearch
{param([string]$ComputerName)

    #LDAP Search Filter
    $filter = "(&(objectCategory=computer)(objectClass=computer)(cn=$ComputerName))"

    #Store Computer Object
    $oComputerObject = ([adsisearcher]$filter).FindOne()
    
    #Checks Results
    if($oComputerObject -ne $null){
        
        #Display Identifying Attributes
        $txtSAMAccountName.Text = $oComputerObject.Properties.Item("SAMAccountName")
        $txtDescription.Text = $oComputerObject.Properties.Item("description")
        $txtDistinguishedName.Text = $oComputerObject.Properties.Item("distinguishedname")
        $txtOperatingSystem.Text = $oComputerObject.Properties.Item("operatingsystem")
        
    #============================
    #Display Computer Password
    #============================
    
    #Store Computer Object
    $oComputerObject = ([adsisearcher]$filter).FindOne()  
    $sDN = $oComputerObject.Properties.Item("distinguishedname")
        
    #Import Assembly
    Add-Type -AssemblyName System.DirectoryServices.Protocols
    
    #Store Domain
    $sDomain = (gwmi Win32_ComputerSystem).Domain
    
    #Create Connection
    $connection=New-Object System.DirectoryServices.Protocols.LDAPConnection($sDomain)
        
    #Enable Kerberos Encryption
    $connection.SessionOptions.Sealing=$true  
    $connection.SessionOptions.Signing=$true
    
    #Search Base
    $sSearchRoot="DC=" + $sDomain.replace(".",",DC=")
    
    #Search Request
    $req = New-Object System.DirectoryServices.Protocols.SearchRequest($sSearchRoot,$filter,"Subtree",$null)
    $rsp = $connection.SendRequest($req)
    
    #Checks for null attribute value
    if($rsp.Entries.Item(0).Attributes.$CustomAttribute -ne $null){
    
        #Store Current Attribute Value
        $sValue = $rsp.Entries.Item(0).Attributes.$CustomAttribute[0]
    
        #Separate Username from Password
        $aValue = $sValue -split ')|(Password:',0,"SimpleMatch"
    }
    
    #Tests for Properly Formatted Username / Password Combination
    if($aValue.Count -eq 2){
        #Format / Display Username
        $sUserName = $aValue[0] -Replace("\(Username:","");$txtLocalAdminUserName.text = $sUserName
        
        #Format / Display Password
        $txtLocalAdminPassword.Text = $aValue[1].Remove($aValue[1].Length -1)
    }
    else{
        #Best Effort Display Of Attribute Value
        $txtLocalAdminPassword.Text = $sValue
        
        #Feedback
        $txtResultsFeedback.Text = "Properly formatted username/password not found"
    }
    $txtFeedback.Text = ""
    }
    else{$txtFeedback.Text = "Computer Not Found"}
}
#===========================================================================
# Shows the form
#===========================================================================
$Form.ShowDialog() | out-null

Using The Form

So what exactly does all of that put together look like? As you can see from the following Figure, the tool is very simple to use. The only input field is the "Computer Name" input field.

After inputting a computer name click Submit to view the local admin username and password for the computer. In the following Figure I retrieved the username and password for a computer object called WKSTA1.

Troubleshooting

If the username or password is not displayed for a particular computer object check to ensure the following:

  • Make sure the computer is in an OU that receives the GPO that applies the Local Admin Password script as a startup script
  • Ensure that the account used to run the viewer has the rights needed to view the confidential attribute where the password is stored
  • Ensure that the workstation or server has been rebooted at least once to run the startup script
  • Ensure that the computer object is in an OU where the computer objects have the rights necessary to update the confidential attribute.
  • Ensure that you have edited the $CustomAttribute variable within the viewer to match the name of the custom attribute where you are storing the password. The following line
     needs to be edited prior to use unless you are in a lab environment and have chosen to use the "description" attribute to store the password:

#===========================================================================
# Edit the Name of the Custom Attribute Below (Default = description)
#===========================================================================
$CustomAttribute = "description"

Conclusion

In this series complete local administrator password management was demonstrated using PowerShell, XAML, and Active Directory. The secure password viewer is attached to this blog post. As always, the extension must be changed to *.ps1 prior to using.

PWDViewer.txt