Last active
February 22, 2023 15:33
-
-
Save aaronpmiller/f7ddf3e539c1d964201e to your computer and use it in GitHub Desktop.
Powershell WPF Form for setting a remote password via netapi32.netuserchangepassword
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
PARAM ( | |
$domainFQDN = $((Get-WmiObject Win32_ComputerSystem).Domain) | |
) | |
#The XAML Form | |
[string]$formXAML = @" | |
<Window x:Class="MainWindow" | |
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" | |
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" | |
Title="Remote Password Reset" Height="300" Width="500" ResizeMode="NoResize" SizeToContent="WidthAndHeight" MinWidth="500" WindowStartupLocation="CenterScreen"> | |
<Grid> | |
<Grid.RowDefinitions> | |
<RowDefinition Height="Auto"/> | |
<RowDefinition Height="Auto"/> | |
<RowDefinition Height="Auto"/> | |
<RowDefinition Height="Auto"/> | |
<RowDefinition Height="Auto"/> | |
<RowDefinition Height="Auto"/> | |
<RowDefinition/> | |
</Grid.RowDefinitions> | |
<Grid.ColumnDefinitions> | |
<ColumnDefinition Width="Auto"/> | |
<ColumnDefinition SharedSizeGroup="SharedButtons"/> | |
<ColumnDefinition SharedSizeGroup="SharedButtons" Width="Auto"/> | |
</Grid.ColumnDefinitions> | |
<Label x:Name="lblInformation" Content="Please fill in a user name." Grid.Row="0" Grid.ColumnSpan="3" Margin="5,5,10,5" Cursor=""/> | |
<Label Content="Domain Name:" Grid.Row="1" Margin="5" Cursor=""/> | |
<Label Content="User Name:" Grid.Row="2" Padding="5" Margin="5"/> | |
<Label Content="Existing Password" Grid.Row="3" Padding="5" Margin="5"/> | |
<Label Content="New password:" Grid.Row="4" Padding="5" Margin="5"/> | |
<Label Content="Retype New Password" Grid.Row="5" Padding="5" Margin="5"/> | |
<TextBox x:Name="txtDomainName" Grid.Column="1" Grid.Row="1" TextWrapping="Wrap" Grid.ColumnSpan="2" Padding="5" Margin="5,5,10,5" TabIndex="1"/> | |
<TextBox x:Name="txtUserName" Grid.Column="1" Grid.Row="2" TextWrapping="Wrap" Grid.ColumnSpan="2" Padding="5" Margin="5,5,10,5" TabIndex="1"/> | |
<Button x:Name="btnChange" Content="Change" Grid.Column="1" Grid.Row="6" Grid.IsSharedSizeScope="True" Padding="3" Margin="5,5,10,10" Width="95" Height="24" HorizontalContentAlignment="Center" HorizontalAlignment="Right" VerticalAlignment="Top" IsEnabled="False" TabIndex="5"/> | |
<Button x:Name="btnCancel" Content="Cancel" Grid.Column="2" Grid.Row="6" Grid.IsSharedSizeScope="True" Padding="3" Margin="5,5,10,10" Width="95" Height="24" HorizontalContentAlignment="Center" HorizontalAlignment="Right" VerticalAlignment="Top" IsCancel="True" TabIndex="6"/> | |
<PasswordBox x:Name="pwdCurrentPassword" Grid.Column="1" Grid.Row="3" Grid.ColumnSpan="2" Margin="5,5,10,5" Padding="5" TabIndex="2"/> | |
<PasswordBox x:Name="pwdNewPassword" Grid.Column="1" Grid.Row="4" Grid.ColumnSpan="2" Margin="5,5,10,5" Padding="5" TabIndex="3"/> | |
<PasswordBox x:Name="pwdRetypeNewPassword" Grid.Column="1" Grid.Row="5" Grid.ColumnSpan="2" Margin="5,5,10,5" Padding="5" TabIndex="4"/> | |
</Grid> | |
</Window> | |
"@ | |
# Set to true to get the error code from NetUserChangePassword to display in the form | |
$testing = $false | |
# There's no built-in Powershell method to do a remote password reset so we have to import a .NET class, below is the deffinition and some helpful links | |
#http://www.pinvoke.net/default.aspx/netapi32.netuserchangepassword | |
#http://msdn.microsoft.com/en-us/library/windows/desktop/aa370650(v=vs.85).aspx | |
$MethodDefinition = @" | |
[DllImport("netapi32.dll", CharSet=CharSet.Unicode, CallingConvention=CallingConvention.StdCall, | |
SetLastError=true )] | |
public static extern uint NetUserChangePassword ( | |
[MarshalAs(UnmanagedType.LPWStr)] string domainname, | |
[MarshalAs(UnmanagedType.LPWStr)] string username, | |
[MarshalAs(UnmanagedType.LPWStr)] string oldpassword, | |
[MarshalAs(UnmanagedType.LPWStr)] string newpassword | |
); | |
"@ | |
# We want to help the user setting the password understand what the requirements are, below is the message we'll send them when their password hasn't met complexity requirements. | |
$complexPWMessage = @" | |
Your new password must meet a few complexity requirements. | |
Your new password must not contain your user name, or full name. | |
Your new password must contain characters from at least three of the following five categories: | |
- Uppercase characters (A-Z) | |
- Lowercase characters (a-z) | |
- Base 10 digits (0-9) | |
- Nonalphanumeric characters (~!@#$%^&*_-+=``|\(){}[]:;`"'<>,.?/) | |
- Any Unicode character that is categorized as an alphabetic character but is not uppercase or lowercase. | |
See the following webpage for details: http://technet.microsoft.com/en-us/library/cc786468(v=ws.10).aspx | |
"@ | |
# Define what the minimum password length should be. | |
$minPasswordLength = 8 | |
# Holding text to let the user know we're ready (also using a variable so we can ensure label comparison when enabeling the change process) | |
$readyMessage = "Click Change when you are ready to change your password." | |
#region Custom Functions | |
# We want to validate current and new credentials against the domain to rule out potentional problems, below is a function that allows us to do that | |
Function Test-ADCredentials { | |
Param($username, $password, $domain) | |
Add-Type -AssemblyName System.DirectoryServices.AccountManagement | |
$ct = [System.DirectoryServices.AccountManagement.ContextType]::Domain | |
$pc = New-Object System.DirectoryServices.AccountManagement.PrincipalContext($ct, $domain) | |
$pc.ValidateCredentials($username, $password).ToString() | |
} | |
# Attempting to connect to a server that's not available is painfully slow so we test the connection to a specific port and define a timeout to wait. | |
Function Test-TCPPort { | |
[CmdletBinding()] | |
[OutputType([System.boolean])] | |
PARAM ( | |
[Parameter(Mandatory = $True)][string]$computer, | |
[Parameter(Mandatory = $True)][int]$port, | |
[int]$timeout=500 | |
) | |
try { | |
$TcpClient = New-Object system.Net.Sockets.TcpClient | |
#Connect to remote machine's port | |
$connection = $TcpClient.BeginConnect($computer,$port,$null,$null) | |
#Configure a timeout before quitting | |
$Connected = $connection.AsyncWaitHandle.WaitOne($timeout,$false) | |
if ($Connected) { | |
$TcpClient.EndConnect($connection) | |
return $True | |
} else { | |
$TcpClient.Close() | |
return $false | |
} | |
} catch { | |
#throw $_ | |
return $false | |
} | |
} | |
#endregion | |
try { | |
#region Prep the Form | |
# Remove the elements the powershell xml parser doesn't agree with (so we don't have to manually do it every time) | |
$formXAML = $formXAML.Replace('x:Name=','Name=').Replace('x:Class="MainWindow"','') | |
# Convert it to xml | |
[xml]$XML = $formXAML | |
# Add WPF and Windows Forms assemblies | |
Add-Type -AssemblyName PresentationCore,PresentationFramework,WindowsBase,system.windows.forms | |
# Create the XAML reader using a new XML node reader | |
$GUI = [Windows.Markup.XamlReader]::Load((new-object System.Xml.XmlNodeReader $XML)) | |
# Create hooks to each named object in the XAML | |
$XML.SelectNodes("//*[@Name]") | % {Set-Variable -Name ($_.Name) -Value $GUI.FindName($_.Name) -Scope Global} | |
#endregion | |
#region Action Deffinitions | |
$disableInputs = { | |
$txtUserName.IsEnabled = $false | |
$pwdCurrentPassword.IsEnabled = $false | |
$pwdNewPassword.IsEnabled = $false | |
$pwdRetypeNewPassword.IsEnabled = $false | |
} | |
$enableInputs = { | |
$txtUserName.IsEnabled = $true | |
$pwdCurrentPassword.IsEnabled = $true | |
$pwdNewPassword.IsEnabled = $true | |
$pwdRetypeNewPassword.IsEnabled = $true | |
} | |
# Actions to take everytime text is changed in any input field. | |
$textChanged = { | |
# By default we want to make sure the change button is disabled whenever a change occurs to the input text until it's been validated again. | |
$btnChange.IsEnabled = $false | |
# Test Domain Availability | |
[bool]$emptyDomain = (-not (($txtDomainName.Text -ne $null) -and ($txtDomainName.Text -ne '') -and ($txtDomainName.Text.trim() -ne ''))) | |
if (-not $emptyDomain) { | |
$domainConnected = Test-TCPPort -computer $($txtDomainName.Text.trim()) -port 389 -timeout 50 | |
} else { | |
$domainConnected = $false | |
} | |
if (-not $domainConnected) { | |
&$disableInputs | |
} else { | |
&$enableInputs | |
} | |
# Begin validation testings | |
# (Re)set the passed tests to 0. 3 or more tests need to be passed for the password to be considered complex. | |
$passedComplexityTests = 0 | |
# Test if it contains numberic characters | |
if ($pwdNewPassword.Password -match "[0-9]") {$passedComplexityTests++} | |
# Test if it contains lower case characters | |
if ($pwdNewPassword.Password -cmatch "[a-z]") {$passedComplexityTests++} | |
# Test if it contains upper case chararacters | |
if ($pwdNewPassword.Password -cmatch "[A-Z]") {$passedComplexityTests++} | |
# Test if it contains alternative characters | |
if ($pwdNewPassword.Password -cmatch "[^a-zA-Z0-9]") {$passedComplexityTests++} | |
# Do some basic form validation | |
$lblInformation.Content = ` | |
if ($emptyDomain) {"Please specify a domain."} | |
elseif (-not $domainConnected) {"Could not contact to the domain specified."} | |
elseif ($txtUserName.Text -eq '') {"Please fill in a user name."} | |
elseif ($pwdCurrentPassword.Password -eq '') {"Please enter your existing password."} | |
elseif ($pwdNewPassword.Password -eq '') {"Please enter your new password."} | |
elseif ($pwdNewPassword.Password.Length -lt $minPasswordLength) {"Your password does not meet the length requirements [$minPasswordLength characters]."} | |
elseif ($pwdNewPassword.Password -eq $pwdCurrentPassword.Password) {"You new password cannot be the same as your existing password."} | |
elseif ($pwdNewPassword.Password -match $txtUserName.Text) {"You new password cannot contain your user name."} | |
elseif ($passedComplexityTests -lt 3) {$complexPWMessage} | |
elseif ($pwdRetypeNewPassword.Password -ne $pwdNewPassword.Password) {"Please retype your new password. Your new and retyped passwords do not match (yet)."} | |
else {$readyMessage} | |
# Enable the change button if our lable is displaying the ready message. | |
if ($lblInformation.Content -eq $readyMessage) {$btnChange.IsEnabled = $true} | |
} | |
# What to do when btnChange is clicked | |
$clickbtnChange = { | |
# Once we start the change process we want to prevent the user from click the change button again and causing errors. | |
$btnChange.IsEnabled = $false | |
# If the user put in domain\userID or [email protected] let's clean up the text so we only have the userID | |
$name = $txtUserName.Text.Split('\')[-1].Split('@')[0] | |
# User hardly ever sees this because the test occurs so quickly but it's a decent message to let the user know in the event the test doesn't go as quickly as expected. | |
$lblInformation.Content = "Validating the current user name & password..." | |
# Test the current credentials before we proceed to attempt the reset. | |
if (Test-ADCredentials -username $txtUserName.Text -password $pwdCurrentPassword.Password -domain $domainFQDN) { | |
$lblInformation.Content = "The current user name & password have been validated." | |
# Import the .NET MethodDefinition we specified up top | |
$NetAPI32 = Add-Type -MemberDefinition $MethodDefinition -Name 'NetAPI32' -Namespace 'Win32' -PassThru | |
# Call the NetUserChangePassword of the MethodDefinition with the required info. | |
$returnValue = $NetAPI32::NetUserChangePassword($domainFQDN, $name, $pwdCurrentPassword.Password, $pwdNewPassword.Password) | |
if ($returnValue -eq 0) { | |
#success | |
# Verify that the new creds work. | |
if (Test-ADCredentials -username $txtUserName.Text -password $pwdNewPassword.Password -domain $domainFQDN) { | |
# Clear out the input fields | |
$pwdCurrentPassword.Password = $null | |
$pwdNewPassword.Password = $null | |
$pwdRetypeNewPassword.Password = $null | |
# Let the user know it worked! | |
$lblInformation.Content = "Your password has been updated successfully." | |
} else { | |
# This should not occurr.. but it'd be good to know where things went wrong if it did... | |
$lblInformation.Content = "Your password was updated, but there was an issue verifying it was successful." | |
} | |
} else { | |
#fail | |
# Let the user know there was a problem updating the password | |
$lblInformation.Content = "Your password failed to update. " | |
# .. and why | |
$lblInformation.Content += switch ($returnValue) { | |
{@(86,2221) -contains $_} {"The user name or password are incorrect."} | |
2245 {"`n$complexPWMessage"} | |
5 {"Access is denied."} | |
default {"An unknown error occurred."} | |
} | |
# If we're doing testing return the error code to the form. | |
if ($testing) {$lblInformation.Content += "`nThe error code was: $returnValue."} | |
} | |
} else { | |
# The current password did not validate, let the user know. | |
$lblInformation.Content = "The current user name or password is incorrect." | |
} | |
} | |
#endregion | |
#region Verify we can connect to the intended domain | |
if ($domainFQDN) { | |
$txtDomainName.Text = "$domainFQDN" | |
$domainConnected = Test-TCPPort -computer $($txtDomainName.Text.trim()) -port 389 -timeout 100 | |
if (-not $domainConnected) { | |
#throw "Could not contact to the domain [$domainFQDN]." | |
$lblInformation.Content ="Could not contact to the domain specified." | |
&$disableInputs | |
} | |
} | |
#endregion | |
#region register events | |
# Set each input field to execue the $textChanged actions when text is changed in the field. | |
$txtDomainName.Add_TextChanged($textChanged) | |
$txtUserName.Add_TextChanged($textChanged) | |
$pwdCurrentPassword.Add_PasswordChanged($textChanged) | |
$pwdNewPassword.Add_PasswordChanged($textChanged) | |
$pwdRetypeNewPassword.Add_PasswordChanged($textChanged) | |
# Defining what happens when the $btnChange is clicked. | |
$btnChange.add_Click($clickbtnChange) | |
#endregion | |
#Launch the window | |
$GUI.ShowDialog() | out-null | |
} | |
catch { | |
throw $_ | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment