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.
Comments
- Anonymous
January 01, 2003
The comment has been removed - Anonymous
January 01, 2003
Judd - Where would you want to log it to? I would imagine you could write a function that would be called when a user clicks Submit and the function evaluates to be a properly formatted and found computer name.
You could probably do it between lines 176 and 181, where its displaying the actual computer name.
Interesting idea though. You could write it to the event logs on a system or if you knew enough PowerShell, send it to SIEM or SNMP traps, things like that. I suppose its all up to you and what requirements you have.
Also - PlatformsPFE Folks - Great script. Does this require PS v3.0 or PS v2.0? One thing to mention would be that it would be ideal to follow the standard PS practices and use Verb-Noun for your functions, but that's just being picky :-) - Anonymous
August 12, 2014
Overview
In this multi part series I will walk you through how to manage the local admin password - Anonymous
August 12, 2014
Hi,
Thanks for an excellent series... When I try your script it finds the computer but it gives me the error "Properly formatted username/password not found"
The field (LocalAdminPWD) in AD looks like this: (Username:administrator)|(Password:*******************)
What am I missing?
/Johan - Anonymous
August 20, 2014
What about attestation? Can you log who ran the tool and to what machine they requested the password. This is so needed but to conform to the whole CIA from a security view, that is what is missing.