Tome's Land of IT

IT Notes from the Powertoe – Tome Tanasovski

Attack the AD! – All computers in the domain that have been logged in during the last 60 days

I’ve been scripting for a while.  From batch to VBS to Perl, I’ve often found a need to automate things.  When I was learning Perl, ADSI was the new hotness.  It gave a real way to query and use AD through automation.  Sure there were caveats: You can hit it via LDAP or WINNT, but not both now.  What property did you need?  Oh!  Go find that through the other provider.

My needs for AD scripting have fortunately always been very limited – until now, that is.  Now that my scripting muscle must touch hundreds of thousands of computers I’ve had to be honest with myself that scripting against AD truly does matter.  So let’s look at a question that I was asked during the interview process with my current employer: “How would you get me a list of all of the computers in a domain that are enabled and have been logged in within the past 60 days?”  The answer at the time was, “I don’t know”, but I had enough knowledge to dance around it in a way that was informed – plus I knew a ton of other stuff.  Needless to say I landed the job, but now I had to ask myself, “how would I do it?”

So let’s explore.  I had a trick up my sleeve to connect a DirectoryEntry object to my current domain.  So I started with that:

$de = New-Object DirectoryServices.DirectoryEntry('LDAP://rootDSE')
$root = New-Object DirectoryServices.DirectoryEntry("LDAP://$($de.DefaultNamingContext)")

The next step was to create a DirectorySearcher against my DirectoryEntry:

$searcher = New-Object System.DirectoryServices.DirectorySearcher($root)

With a searcher in hand I could set a filter that would show me all computers and then execute that searcher to get me a list of all computers:

$searcher.Filter = "(&(objectCategory=computer))"
$comps = $searcher.findall()

The first thing I noticed was that it only returned 1000 records when there should have been 50K.  This is due to a limitation with the maximum returned by the server.  To get around it you need to tell your searcher to have a page value less than the maximum of 1000:

$searcher.PageSize = 500

After doing this there were two obvious problems:

  1. It was returning all computers without constraints
  2. It was slow as s*#t

The speed wound up being very simple.  By adding constraints and telling it to only return the values I cared about the speed increased dramatically.  I’ll get to the constraints in a moment. The syntax to force it to only return the value (name) I wanted was:

$searcher.PropertiesToLoad.AddRange(@("name")) #obviously this can be a list of properties if you need more than the name

Now the constraints – the first of which was to make sure I only returned enabled accounts:

$searcher.Filter = "(&(objectCategory=computer)(!(userAccountControl:1.2.840.11356.1.4.803:=2)))"

While it seems bizarre it’s doing some bitwise operators to determine if the account is disabled.  You should read here to learn more about this technique.  It can also be used to determine if a user is a member of a group.  You can find more info specifically about the useraccountcontrol property here.

Tackling the date last logged in was a bit trickier.  After some digging I found an attribute in AD called lastlogontimestamp.  When inspecting it on the computer I had just logged into I found the following value: 129195723370097473.  Interesting – my first thought was that it was a Unix time, but I soon found that I was mistaken.  Of course it’s not the number of seconds since 1970, it’s the number of 100 nanoseconds since January 1st, 1601.  Duh!

The question was how could I get today’s date into this format to compare it against the number before me.  I knew I could write a function to do this, but I couldn’t believe that it was not built-in to the .net DateTime class.  Fortunately, Microsoft proved their worth with two functions to convert to and from this value:

  1. Convert a date to this number:
  2. Convert the number to a DateTime:

The strange thing was that I found that the time converted was not the actual last time logged in on the computer.  It winds up that this is by design.  The lastlogontime attribute only gets updated when the logon took place after a predefined threshold in AD called msDS-LogonTimeSyncInterval.  In my case this is 14 days.  If the logon happens within 14 days of what’s written to the attribute it will not update and replicate throughout AD.  This ensures that unnecessary replication does not take place.  So to account for this I could add 14 days to my 60 day limit and rewrite my filter with the following:

$logondate = (get-date).adddays(-74).tofiletime()
$searcher.Filter = "(&(objectCategory=computer)(lastlogontimestamp>=$logondate)(!(userAccountcontrol:1.2.840.113556.1.4.803:=2)))"

The final script was as follows:

$de = New-Object DirectoryServices.DirectoryEntry('LDAP://rootDSE')
$root = New-Object DirectoryServices.DirectoryEntry("LDAP://$($de.DefaultNamingContext)")
$searcher = New-Object System.DirectoryServices.DirectorySearcher($root)
$searcher.PageSize = 500
$searcher.Filter = "(&(objectCategory=computer)(lastlogontimestamp>=$logondate)(!(userAccountcontrol:1.2.840.113556.1.4.803:=2)))"
$comps = $searcher.findall()

Mission accomplished – until I found out that the domain I needed to run it against was a Windows 2000 domain that did not have this attribute.  Back to the drawing board.  Fortunately, I found another attribute called lastlogon in Windows 2000 that looked identical.  However, once I started using it I realized that it was impossible for it to be accurate.  After a discussion with my AD guru I learned that this attribute does not replicate.  The lastlogontimestamp was added to 2k3 in order to ensure that this data was replicated so that people could run reports like the one I was trying to run.  He told me the only thing I could do would be to query every domain controller.  This task sounded impossible – until I tried it.

If you need to do this in 2k it’s actually quite simple.  Using System.DirectoryServices.ActiveDirectory.Domain you can get all of the DCs in your domain and query each of them in turn.  I wrote the code below to do this.  Mind you it would require some error checking in production since the data must be from all DCs in order to be valid.  It also would require extra thought if you are so far from a DC that your query will timeout.  With that in mind here is the quick roundabout:

$logondate = (get-date).adddays(-30).tofiletime()
$comps = @()
([System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()).DomainControllers|select name | foreach {
    "Connecting to $($"
    $Root = New-Object directoryservices.directoryentry("LDAP://$($")
    $searcher = new-object System.DirectoryServices.DirectorySearcher($root)
    $searcher.PageSize = 500
    $searcher.findall()|foreach {
        if ($comps -notcontains $ {
            $comps += $

Funny – a long post over something I considered putting as a powerbit just to remind myself about the two datetime functions for working with the nanoseconds from 1601.


One response to “Attack the AD! – All computers in the domain that have been logged in during the last 60 days

  1. January 16, 2013 at 7:01 pm

    “Attack the AD! – All computers in the domain that have been logged in during the last 60 days
    Tome’s Land of IT” was indeed a superb blog post. If solely there was significantly more personal blogs like this specific one on the net. Regardless, thanks for your time, Carson

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )


Connecting to %s

%d bloggers like this: