Auditing Active Directory User Creation: A Simple Approach

Hello again. Since websites like reddit, Wikipedia and plenty others are blacked out today in protest of the Internet censorship bills SOPA and PIPA, it gives me plenty of time that I would have otherwise wasted surfing the web to contact my representatives and tell them that I, as a constituent, strongly urge them to reconsider their support of these bills... and then to write a blog post about Active Directory change auditing.

Recently, someone explained to me how in their company, they had some third-party software foisted upon them that automatically generated new user accounts. I don't know what the software was for, but understandably, this made him feel a little uncomfortable. We administrators don't particularly enjoy giving HAL-9000 the keys to manipulate our Active Directories with little insight into what it's actually doing.

So with that in mind, he asked me if there was a way to audit new user account creation, and then to go a step further and actually perform some action whenever a new user account was created.

There are lots of third-party Active Directory auditing tools that companies would love to sell you, but let's put on our engineer hats and bang something out using only built-in Windows tools. Let's pretend that our boss just told us there's no budget for buying new software and this task must be completed by lunch, or else you're fired. There are undoubtedly many different ways of going about auditing Active Directory changes, and this is but one way. It may or may not be the best way, but perhaps it will give you some ideas. This information is written specifically using Windows 2008 R2.

When a new user account is created, a slew of events are recorded in the Security event log on the domain controller on which the user account was created. In order of occurrence:

  • 4720 - A user account was created.
  • 4724 - An attempt was made to reset an account's password.
  • 4738 - A user account was changed. (Repeated 4x)
  • 4722 - A user account was enabled.

If you only have one domain controller in your domain, you can pretty much stop right here - your work is done.  Simply right-click the event in Event Viewer, select "Attach Task To This Event," and insert the name of your Powershell script or executable or email address you want to send notification to, etc.

But most of us have more than one domain controller, and those aforementioned Security events are not logged on every domain controller - only the DC on which the user was initially created, and there's no practical way to ensure that user accounts are only created on one DC. I was hoping that since the PDC Emulator is involved in every password reset, that I would at least get an event on my PDCe that implied user account creation had taken place on another DC, but I found no such events on the PDCe. There was only a generic Logon event originating from the auxiliary DC at the exact moment that the user account was created. Furthermore, even if I had found an event 4724 on the PDCe, there probably would have been no way to distinguish between that event and one that accompanied an existing user's routine password change anyway.

So to solve for this, let's set up event subscriptions! (I suppose you could just go around and set up identical tasks on each DC... but I want to do event subscriptions!) On the server that you want to collect events from other sources, just click "Subscriptions" in the left pane of Event Viewer:

*Do it!*

I just happened to choose my main DC as the event subscriber for this test. It should also be noted that at the command line, you can use wecutil.exe and its brother wevtutil.exe to accomplish these same goals, but we're going to use the GUI.

Now right-click on Subscriptions and Create Subscription:

Fill out the information. You're going to want your subscriber to go get events from your other DC. When you select the computers from which you want to collect events, you can test them before you commit the changes, which is nice. You're going to want to make sure that the Windows Remote Management (WS-Management) service, also known as WinRM, is running... and also that it is configured. To do this, simply run winrm quickconfig on all the machines involved. This can also be done via GPO so that your new machines will be configured automatically as they're deployed.

Now the connectivity test from your subscriber should succeed, and you'll be ready to subscribe to events from the other machine. If the test is still failing, double check Windows Firewall, any other firewalls in the way, that the WinRM service is running and configured on the remote machine, and name resolution. Now back on our event collector machine, make sure and set up your filter to only get Security event 4720's.

Alright you're done! Now at this point, events from DC02 will pop up in the "Forwarded Events" log on DC01. If you have any problems with your forwarded events not showing up, right-click on the subscription and choose "Runtime Status". This will alert you to any additional problems. In my case, I was still getting an "Access Denied" when trying to read the logs on DC02. The reason was that the subscription was configured to run under the Machine Account. I switched it to a user account that had the correct permissions to read the logs on DC02, and it worked just fine. If you get just an EventID 111 in the Forwarded Events log on your collector, remember that you need to run winrm quickconfig on both machines - the forwarder and the forwardee.

You can now attach a custom task to either these forwarded events, or the entire Forwarded Events log as a whole.

Using Powershell to Monitor Windows Reliability Data

There's always a lot of talk about monitoring when stuff gets installed and uninstalled on a Windows machine, or when "configuration changes" take place on a system, or even when unplanned reboots (crashes) take place... how do we audit that? As awesome as the Windows event logs are, they can be a bit unwieldy to sift through all the noise and cryptic messages.

There are lots of third-party tools for auditing software changes. Those tools can cost a lot of money. But did you know Windows already does this for you? If you run perfmon /rel on your Vista/7/2008/R2 machine, you will be greeted with this pretty picture:

Notice that you can even export all the data as a nice little XML file. So that's pretty neat. You can see all the application crashes, system crashes, when software was installed and uninstalled, etc... but that's all GUI stuff. I know what really you want is something more programmatic, customizable and automatable. It just so happens that there's a WMI class called Win32_ReliabilityRecords. Let's use Powershell to take a peek:

# Looking at Windows software installations and uninstallations and other reliability data
# Ryan Ries, Jan 5 2012
#
# Usage: .\ReliabilityData.ps1 <argument>
# Valid arguments are "ShowAll", "ShowSystemCrashes", "ShowWhateverYourImaginationIsTheLimit", ...
# Arguments are not case sensitive.

param([parameter(Mandatory=$true)]
      [string]$Argument)

Function WMIDateStringToDateTime([String] $strWmiDate) 
{ 
    $strWmiDate.Trim() > $null 
    $iYear   = [Int32]::Parse($strWmiDate.SubString( 0, 4)) 
    $iMonth  = [Int32]::Parse($strWmiDate.SubString( 4, 2)) 
    $iDay    = [Int32]::Parse($strWmiDate.SubString( 6, 2)) 
    $iHour   = [Int32]::Parse($strWmiDate.SubString( 8, 2)) 
    $iMinute = [Int32]::Parse($strWmiDate.SubString(10, 2)) 
    $iSecond = [Int32]::Parse($strWmiDate.SubString(12, 2)) 
    $iMicroseconds = [Int32]::Parse($strWmiDate.Substring(15, 6)) 
    $iMilliseconds = $iMicroseconds / 1000 
    $iUtcOffsetMinutes = [Int32]::Parse($strWmiDate.Substring(21, 4)) 
    if ( $iUtcOffsetMinutes -ne 0 ) 
    { 
        $dtkind = [DateTimeKind]::Local 
    } 
    else 
    { 
        $dtkind = [DateTimeKind]::Utc 
    } 
    return New-Object -TypeName DateTime -ArgumentList $iYear, $iMonth, $iDay, $iHour, $iMinute, $iSecond, $iMilliseconds, $dtkind 
} 

If($Argument -eq "ShowAll")
{
       $reliabilityData = Get-WmiObject Win32_ReliabilityRecords
       ForEach ($entry in $reliabilityData)
       {
              Write-Host "Computer Name: " $entry.ComputerName
              Write-Host "Event ID:      " $entry.EventIdentifier
              Write-Host "Record Number: " $entry.RecordNumber
              Write-Host "Date and Time: " $(WMIDateStringToDateTime($entry.TimeGenerated))
              Write-Host "Source:        " $entry.SourceName
              Write-Host "Product Name:  " $entry.ProductName
              Write-Host "User:          " $entry.User
              Write-Host "Message:       " $entry.Message
              Write-Host " "
       }
}

If($Argument -eq "ShowSystemCrashes")
{
       $reliabilityData = Get-WmiObject Win32_ReliabilityRecords
       ForEach ($entry in $reliabilityData)
       {
              If($entry.Message.StartsWith("The previous system shutdown") -And $entry.Message.EndsWith("was unexpected."))
              {
                     Write-Host "Computer Name: " $entry.ComputerName
                     Write-Host "Event ID:      " $entry.EventIdentifier
                     Write-Host "Record Number: " $entry.RecordNumber
                     Write-Host "Date and Time: " $(WMIDateStringToDateTime($entry.TimeGenerated))
                     Write-Host "Source:        " $entry.SourceName
                     Write-Host "Product Name:  " $entry.ProductName
                     Write-Host "User:          " $entry.User
                     Write-Host "Message:       " $entry.Message
                     Write-Host " "             
              }
       }
}

If($Argument -eq "ShowApplicationInstalls")
{
       $reliabilityData = Get-WmiObject Win32_ReliabilityRecords
       ForEach ($entry in $reliabilityData)
       {
              If($entry.Message.StartsWith("Windows Installer installed the product."))
              {
                     Write-Host "Computer Name: " $entry.ComputerName
                     Write-Host "Event ID:      " $entry.EventIdentifier
                     Write-Host "Record Number: " $entry.RecordNumber
                     Write-Host "Date and Time: " $(WMIDateStringToDateTime($entry.TimeGenerated))
                     Write-Host "Source:        " $entry.SourceName
                     Write-Host "Product Name:  " $entry.ProductName
                     Write-Host "User:          " $entry.User
                     Write-Host "Message:       " $entry.Message
                     Write-Host " "             
              }
       }
}

So those are just some ideas that I threw together, but is by no means a complete solution. Use that as a starting point, play with the script, expand on it and make it even better! And one last thing, ideally I should not be using Write-Host here but instead be preserving the objects, that way I could combine this script with other commandlets on the pipeline, etc. I'll put that in as an enhancement request...