Skip to content

Instantly share code, notes, and snippets.

@aaronpmiller
Last active February 22, 2023 15:33
Show Gist options
  • Save aaronpmiller/f7ddf3e539c1d964201e to your computer and use it in GitHub Desktop.
Save aaronpmiller/f7ddf3e539c1d964201e to your computer and use it in GitHub Desktop.
Powershell WPF Form for setting a remote password via netapi32.netuserchangepassword
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 userID@domain.com 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