Using PowerShell to notify users that their password is going to expire

Posted by: Andrew Jefferies

I do very little programming in my normal day-to-day activities. However, I do believe that understanding programming concepts is great at helping me understand application security issues.

I had a bit of free time on my hands this week so I thought I would delve into Microsoft's new (ok maybe not really new anymore) fang-dangled scripting language PowerShell.  I figure that they aren't about to start shipping Perl or Python with Windows so I had better get on the bandwagon.

It just so happens that we have a need for a mechanism to alert users when their password is going to expire.  This is for users that aren't connected to the domain regularly. I'll explain the actual change mechanism in a post soon...

 

So what did I learn about PowerShell?

  1. It has some wonky syntax
  2. It is poorly documented (although general .Net documentation usually applies)
  3. I'm not a fan of its error messages
  4. Generally... not too bad. I'll probably try to use it some more.

This script is what I came up with. Please keep in mind that I am not a programmer! ...just a security guy that likes to dabble. I hope someone finds it useful. If you have comments on improvements I would love to hear them. Leave a comment.

One last note, PLEASE test this in a controlled manner. I don't want you sending out emails to all of your users unexpectedly! Line 53 has a commented out test line that you can use to specify your name for testing. In tools like:

$strFilter = "(&(objectCategory=User)(name=YourNameForTesting*))"

My plan is to set this to run nightly as a Scheduled Task.

#########################################
# Find accounts that are expiring and email the user                                 #Author: Andrew Jefferies, Bulletproof Solutions
#September 2009                                                                                                 ##########################################

#################### Set parameters ###############

$warnInterval = -7 #the number of days warning (use negative number)
$serviceName = "yourdomain.com"
$changeURL = "http://www.someURL.com"
$domainLdapString = "LDAP://dc=yourdomain,dc=com" #Domain connection info for retrieving domain level info
$ouLdapString = "LDAP://OU=SomeOU,dc=yourdomain,dc=com" #specific OU connection info
$accountDisqualifiers = "8389120","528","514" #These are account status numbers stored in userAccountControl in AD
$accountValid="512"

#Email Settings
$SmtpClient = new-object system.net.mail.smtpClient
$SmtpServer = "10.1.1.1" #Your email server address. Must allow relaying from the script host
$SmtpClient.host = # Doesn't need to be defined for this script
$SmtpServer $From = "Support "
$Title = "Password expiring: $serviceName"
######################################################################

# Set up logging to the eventlog
$evt=new-object System.Diagnostics.EventLog("Application")
$evt.Source="Account Expiration Script"
$infoEvent=[System.Diagnostics.EventLogEntryType]::Information
$warnEvent=[System.Diagnostics.EventLogEntryType]::Warning
$evt.WriteEntry("Starting Account Expiration Notification Script",$infoEvent)

#Function: Check for valid email address function isEmailAddress {param($address)
($address -as [System.Net.Mail.MailAddress]).Address -eq $address -and $address -ne $null } ##########################
# Get General domain information
#Create domain searcher object with attribs
$domObj = New-Object System.DirectoryServices.DirectoryEntry($domainLdapString) #The main domain connection object
$domStrFilter = "(objectclass=domainDNS)" #Filter for domain search
$domObjSearcher = New-Object System.DirectoryServices.DirectorySearcher
$domObjSearcher.PageSize = 1000 #set the max number of results that will be returned
$domObjSearcher.SearchRoot = $domObj
$domObjSearcher.Filter = $domStrFilter
$domResult = $domObjSearcher.FindOne() $maxpwdage = [System.Math]::ABS($domResult.properties["maxpwdage"][0]) #result is returned as negative ticks - make positive with ABS

#########################
# Get user specific information
#Create searcher object with attribs
$OUObj = New-Object System.DirectoryServices.DirectoryEntry($ouLdapString) #The main OU connection object
#$strFilter = "(&(objectCategory=User)(name=YourNameForTesting*))" #For test (* is for wildcard search)
$strFilter = "(&(objectCategory=User))" #For user search
$OUObjSearcher = New-Object System.DirectoryServices.DirectorySearcher
$OUObjSearcher.PageSize = 1000 #set the max number of results that will be returned
$OUObjSearcher.SearchRoot = $domObj
$OUObjSearcher.Filter = $domStrFilter
$OUObjSearcher.Filter = $strFilter
$OUObjSearcher.SearchScope = "Subtree" #search whole tree
$colProplist = "name","mail" ,"pwdLastSet","useraccountcontrol" #This is what we want returned from the search

foreach ($i in $colPropList){$OUObjSearcher.PropertiesToLoad.Add($i)} # load resulting attributes into the search class

# Do the search
$colResults = $OUObjSearcher.FindAll()

# Figure out which passwords are set to expire in [$warnInterval] days $messagesSent = 0 #tracks the number of messages sent in the next loop
foreach ($objResult in $colResults){
trap [Exception] {
write-host "Trap (user:"  $objItem.name "): $($_.Exception.Message)"
continue
}
$objItem = $objResult.Properties #holds the properties
$name = [string]$objItem.name
$emailAddress = [string]$objItem.mail
$pwdlastset = $objItem.pwdlastset[0] #last date the password was set (in FileTime format --> ticks)
$userAccountControl = $objItem.useraccountcontrol #stores account status
$maxPwdDate = [Datetime]::FromFileTime($pwdlastset + $maxpwdage)
$warnDate = $maxPwdDate.AddDays($warnInterval)
$today = [DateTime]::Today
$expireDays = New-Timespan -Start $today -End $maxPwdDate #number of days until the account expires
if (($userAccountControl -eq $accountValid) -and ($today -ge $warnDate) -and ($expireDays.Days -gt 0)){ #test password dates and valid account
$verifyAddress = isEmailAddress $emailAddress
if (!$verifyAddress){
$evt.WriteEntry("No valid email address for: $name",$warnEvent)
}
elseif ($accountDisqualifiers -notcontains $userAccountControl){ #check account against list of statuses for disabled accounts
# Send warning message
$body = $name + " your " + $servicename + " account is set to expire in " + $expireDays.Days + " days. Please log into the domain to change your password or visit " + $changeURL + " ."
$name + ":" + $expireDays.Days
$SmtpClient.Send($from,$emailAddress,$title,$body) #Send message
$evt.WriteEntry("Sent account expiration notification to: $name  $emailAddress ",$infoEvent)
$messagesSent += 1 #increment message counter
}
}
}

$evt.WriteEntry("Completed Account Expiration Notification Script. Sent $messagesSent messages.",$infoEvent)

Comments (0)Add Comment

Write comment

busy