Archive

Archive for the ‘Active Directory’ Category

VBScript: Query multiple servers on any domain from AD or a text file.

March 16, 2011 Leave a comment

I had a requirement to query AD for all servers then gather their network config.  I then found that there were servers in AD that had been shut down but not cleaned up, so the script failed on the first unreachable server, so I had to output a list from AD to a text file then re-work the script to read from that text file.  In this article I’ll show both methods, the worker code and how I output the list of servers.

Enumerate all servers in the domain and query their NIC settings:

Option Explicit

Dim objRootDSE, strDNSDomain, adoConnection, adoCommand, strQuery
Dim adoRecordset, strComputerName, strBase, strFilter, strAttributes

' Determine DNS domain name from RootDSE object.
Set objRootDSE = GetObject("LDAP://RootDSE")
strDNSDomain = objRootDSE.Get("defaultNamingContext")

' Use ADO to search Active Directory for all computers.
Set adoCommand = CreateObject("ADODB.Command")
Set adoConnection = CreateObject("ADODB.Connection")
adoConnection.Provider = "ADsDSOObject"
adoConnection.Open "Active Directory Provider"
adoCommand.ActiveConnection = adoConnection

' Search entire domain.
strBase = "<LDAP://" & strDNSDomain & ">"

' Filter on computer objects with server operating system.
strFilter = "(&(objectCategory=computer)(operatingSystem=*server*))"

' Comma delimited list of attribute values to retrieve.
strAttributes = "cn"

' Construct the LDAP syntax query.
strQuery = strBase & ";" & strFilter & ";" & strAttributes & ";subtree"

adoCommand.CommandText = strQuery
adoCommand.Properties("Page Size") = 100
adoCommand.Properties("Timeout") = 30
adoCommand.Properties("Cache Results") = False

Set adoRecordset = adoCommand.Execute

' Enumerate computer objects with server operating systems.
Do Until adoRecordset.EOF

    ' Worker Code: Enumerate computer objects with server operating systems
    strComputerName = adoRecordset.Fields("cn").Value
    Wscript.Echo "================================="
    Wscript.Echo "Computer Name: " & strComputerName
    Wscript.Echo "================================="
    ' Bind to CIMV2 on each Server
    Set objWMIService = GetObject("winmgmts:" _
        & "{impersonationLevel=impersonate}!\\" & strComputerName & "\root\cimv2")
    ' Gather Network Adapter Configuration
    Set colAdapters = objWMIService.ExecQuery _
        ("SELECT * FROM Win32_NetworkAdapterConfiguration WHERE IPEnabled = True")
    n = 1
    WScript.Echo
    For Each objAdapter in colAdapters
        WScript.Echo
        ' Display NIC Name
        WScript.Echo "Network Adapter " & n
        WScript.Echo "================="
        WScript.Echo "  Description: " & objAdapter.Description
        ' Display MAC address
        WScript.Echo "  Physical (MAC) address: " & objAdapter.MACAddress
        ' Display IP address
        If Not IsNull(objAdapter.IPAddress) Then
            For i = 0 To UBound(objAdapter.IPAddress)
                WScript.Echo "  IP address:             " & objAdapter.IPAddress(i)
            Next
        End If
        ' Display Subnet Mask
        If Not IsNull(objAdapter.IPSubnet) Then
            For i = 0 To UBound(objAdapter.IPSubnet)
                WScript.Echo "  Subnet:                 " & objAdapter.IPSubnet(i)
            Next
        End If
        ' Display Default Gateway
        If Not IsNull(objAdapter.DefaultIPGateway) Then
            For i = 0 To UBound(objAdapter.DefaultIPGateway)
                WScript.Echo "  Default gateway:        " & _
            objAdapter.DefaultIPGateway(i)
        Next
        End If
        ' Display DNS Server Search Order
        WScript.Echo
        WScript.Echo "  DNS"
        WScript.Echo "  ---"
        WScript.Echo "    DNS servers in search order:"
        If Not IsNull(objAdapter.DNSServerSearchOrder) Then
            For i = 0 To UBound(objAdapter.DNSServerSearchOrder)
                WScript.Echo "      " & objAdapter.DNSServerSearchOrder(i)
            Next
        End If
        WScript.Echo "    DNS domain: " & objAdapter.DNSDomain
        ' Display Suffix Search List
        If Not IsNull(objAdapter.DNSDomainSuffixSearchOrder) Then
            For i = 0 To UBound(objAdapter.DNSDomainSuffixSearchOrder)
                WScript.Echo "    DNS suffix search list: " & _
                objAdapter.DNSDomainSuffixSearchOrder(i)
            Next
        End If
        n = n + 1
    Next
adoRecordset.MoveNext
Loop
' Clean up.
adoRecordset.Close
adoConnection.Close
Function WMIDateStringToDate(utcDate)
WMIDateStringToDate = CDate(Mid(utcDate, 5, 2)  & "/" & _
Mid(utcDate, 7, 2)  & "/" & _
Left(utcDate, 4)    & " " & _
Mid (utcDate, 9, 2) & ":" & _
Mid(utcDate, 11, 2) & ":" & _
Mid(utcDate, 13, 2))
End Function

Query AD for a list of servers by saving the following script as QueryADServers.vbs:

Option Explicit

Dim objRootDSE, strDNSDomain, adoConnection, adoCommand, strQuery
Dim adoRecordset, strComputerName, strBase, strFilter, strAttributes

' Determine DNS domain name from RootDSE object.
Set objRootDSE = GetObject("LDAP://RootDSE")
strDNSDomain = objRootDSE.Get("defaultNamingContext")

' Use ADO to search Active Directory for all computers.
Set adoCommand = CreateObject("ADODB.Command")
Set adoConnection = CreateObject("ADODB.Connection")
adoConnection.Provider = "ADsDSOObject"
adoConnection.Open "Active Directory Provider"
adoCommand.ActiveConnection = adoConnection

' Search entire domain.
strBase = "<LDAP://" & strDNSDomain & ">"

' Filter on computer objects with server operating system.
strFilter = "(&(objectCategory=computer)(operatingSystem=*server*))"

' Comma delimited list of attribute values to retrieve.
strAttributes = "cn"

' Construct the LDAP syntax query.
strQuery = strBase & ";" & strFilter & ";" & strAttributes & ";subtree"

adoCommand.CommandText = strQuery
adoCommand.Properties("Page Size") = 100
adoCommand.Properties("Timeout") = 30
adoCommand.Properties("Cache Results") = False

Set adoRecordset = adoCommand.Execute

' Enumerate computer objects with server operating systems.

Do Until adoRecordset.EOF

    ' Worker Code: Enumerate computer objects with server operating systems & display their names
    strComputerName = adoRecordset.Fields("cn").Value
        Wscript.Echo "Computer Name: " & strComputerName
adoRecordset.MoveNext
Loop
' Clean up.
adoRecordset.Close
adoConnection.Close
Function WMIDateStringToDate(utcDate)
WMIDateStringToDate = CDate(Mid(utcDate, 5, 2)  & "/" & _
  Mid(utcDate, 7, 2)  & "/" & _
    Left(utcDate, 4)    & " " & _
      Mid (utcDate, 9, 2) & ":" & _
        Mid(utcDate, 11, 2) & ":" & _
          Mid(utcDate, 13, 2))
End Function

Output a list of servers to a text file by running the script thusly:

C:\>cscript //nologo QueryADServers.vbs > ServerList.txt

NB: Once I output the list of servers I fed the list into a batch file that ran an nslookup against them and removed the ones that didn’t have an entry in DNS.  The resulting ServerList.txt file is the one I used for the following script.

Query the ServerList.txt text file by running the following script from the same directory that the ServerList.txt file is in as Administrator:

Option Explicit

Dim objFSO, objFile, strLine, objWMIService, colAdapters, n, objAdapter, i

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("ServerList.txt", 1)

Do Until objFile.AtEndOfStream
strLine = objFile.ReadLine

    ' Worker Code: Enumerate computer objects with server operating systems
    strComputerName = adoRecordset.Fields("cn").Value
    Wscript.Echo "================================="
    Wscript.Echo "Computer Name: " & strLine
    Wscript.Echo "================================="
    ' Bind to CIMV2 on each Server
    Set objWMIService = GetObject("winmgmts:" _
        & "{impersonationLevel=impersonate}!\\" & strComputerName & "\root\cimv2")
    ' Gather Network Adapter Configuration
    Set colAdapters = objWMIService.ExecQuery _
        ("SELECT * FROM Win32_NetworkAdapterConfiguration WHERE IPEnabled = True")
    n = 1
    WScript.Echo
    For Each objAdapter in colAdapters
        WScript.Echo
        ' Display NIC Name
        WScript.Echo "Network Adapter " & n
        WScript.Echo "================="
        WScript.Echo "  Description: " & objAdapter.Description
        ' Display MAC address
        WScript.Echo "  Physical (MAC) address: " & objAdapter.MACAddress
        ' Display IP address
        If Not IsNull(objAdapter.IPAddress) Then
            For i = 0 To UBound(objAdapter.IPAddress)
                WScript.Echo "  IP address:             " & objAdapter.IPAddress(i)
            Next
        End If
        ' Display Subnet Mask
        If Not IsNull(objAdapter.IPSubnet) Then
            For i = 0 To UBound(objAdapter.IPSubnet)
                WScript.Echo "  Subnet:                 " & objAdapter.IPSubnet(i)
            Next
        End If
        ' Display Default Gateway
        If Not IsNull(objAdapter.DefaultIPGateway) Then
            For i = 0 To UBound(objAdapter.DefaultIPGateway)
                WScript.Echo "  Default gateway:        " & _
            objAdapter.DefaultIPGateway(i)
        Next
        End If
        ' Display DNS Server Search Order
        WScript.Echo
        WScript.Echo "  DNS"
        WScript.Echo "  ---"
        WScript.Echo "    DNS servers in search order:"
        If Not IsNull(objAdapter.DNSServerSearchOrder) Then
            For i = 0 To UBound(objAdapter.DNSServerSearchOrder)
                WScript.Echo "      " & objAdapter.DNSServerSearchOrder(i)
            Next
        End If
        WScript.Echo "    DNS domain: " & objAdapter.DNSDomain
        ' Display Suffix Search List
        If Not IsNull(objAdapter.DNSDomainSuffixSearchOrder) Then
            For i = 0 To UBound(objAdapter.DNSDomainSuffixSearchOrder)
                WScript.Echo "    DNS suffix search list: " & _
                objAdapter.DNSDomainSuffixSearchOrder(i)
            Next
        End If
        n = n + 1
    Next
adoRecordset.MoveNext
Loop
' Clean up.
adoRecordset.Close
adoConnection.Close
Function WMIDateStringToDate(utcDate)
WMIDateStringToDate = CDate(Mid(utcDate, 5, 2)  & "/" & _
Mid(utcDate, 7, 2)  & "/" & _
Left(utcDate, 4)    & " " & _
Mid (utcDate, 9, 2) & ":" & _
Mid(utcDate, 11, 2) & ":" & _
Mid(utcDate, 13, 2))
End Function

If you save this script as QueryServersFromTextFile.vbs you can run it as follows to output the servers NIC configs to a text file:

C:\>cscript //nologo QueryServersFromTextFile.vbs > ServerNICs.txt

Sample output:

=================================
Computer Name: DC1.yourdomain.com
=================================

Network Adapter 1
=================
Description: HP Network Team #1
Physical (MAC) address: 00:11:22:33:44:55
IP address:             192.168.77.100
Subnet:                 255.255.255.0
Default gateway:        192.168.77.1

DNS
---
DNS servers in search order:
    192.168.77.10
    192.168.77.11
DNS domain: yourdomain.com
DNS suffix search list: theirdomain.com
DNS suffix search list: our-domain.com

Once again, thanks to Richard Mueller and the writers of WMI Script-O-Matic & EZ AD Script-O-Matic for enabling me to make my job so much easier.

Advertisements

Fun with Exchange 2000 in a legacy domain

January 26, 2011 Leave a comment

I had an issue where multiple users were unable to access Outlook.  They were receiving a password prompt upon launching the application.  I needed to find out which Global Catalog server the Exchange server was binding to, which you can do with the trusty old netstat command piped to findstr (which just isn’t a patch on grep, sorry Microsoft):

C:\>netstat -a | findstr ":3268"  
TCP    exch-srv01:1170          DC13.your.domain.com:3268  CLOSE_WAIT 
TCP    exch-srv01:1171          DC13.your.domain.com:3268  ESTABLISHED  
TCP    exch-srv01:1174          DC04.your.domain.com:3268  ESTABLISHED  
TCP    exch-srv01:1175          DC05.your.domain.com:3268  ESTABLISHED  
TCP    exch-srv01:1186          DC05.your.domain.com:3268  CLOSE_WAIT  
TCP    exch-srv01:1187          DC05.your.domain.com:3268  CLOSE_WAIT  
TCP    exch-srv01:1189          DC05.your.domain.com:3268  CLOSE_WAIT  
TCP    exch-srv01:1221          DC04.your.domain.com:3268  ESTABLISHED  
TCP    exch-srv01:1223          DC05.your.domain.com:3268  ESTABLISHED  
TCP    exch-srv01:1237          DC04.your.domain.com:3268  ESTABLISHED  
TCP    exch-srv01:1238          DC05.your.domain.com:3268  ESTABLISHED  
TCP    exch-srv01:1429          DC05.your.domain.com:3268  ESTABLISHED  
TCP    exch-srv01:1431          DC04.your.domain.com:3268  ESTABLISHED  
TCP    exch-srv01:1910          DC04.your.domain.com:3268  ESTABLISHED  
TCP    exch-srv01:1911          DC05.your.domain.com:3268  ESTABLISHED  
TCP    exch-srv01:5452          DC04.your.domain.com:3268  CLOSE_WAIT  
TCP    exch-srv01:10412         DC04.your.domain.com:3268  ESTABLISHED  
TCP    exch-srv01:10414         DC05.your.domain.com:3268  ESTABLISHED

C:\>

The output of the netstat command was a little worrying as the Exchange server was only set to bind to DC04 & DC05 because DC13 was to be decommissioned as it was very sick.

The resolution was easy enough; you just need to uncheck Automatically discover servers and remove all but one GC in the Directory Access tab of the Exchange servers properties dialog in the System manager to force it to bind to a specific DC.  Once it works add the other GC(s) back in and enable automatically discover again.

NB: This is an excellent troubleshooting method if you’re having authentication problems with Exchange &  don’t know which DC is causing the problem; just connect to each one in turn until you find the culprit.

Bind to specific GC

Now comes the fun of removing the meta-data for DC13 as it wasn’t cleanly removed (DCPROMO failed to de-promote).  :-/

Too Many Passwords? NPS/RADIUS on Windows Server 2008

January 24, 2011 Leave a comment

I had a requirement to add Network Policy Services to allow Active Directory Authentication on Cisco devices:

Network Policy Server Setup for AD:

  • Create AD global security group in domain.
  • Install NPS components from the Roles console in Windows Server 2008.
  • Install Network Policy Server.
  • Install Routing and Remote Access Service.
    • Install Remote Access Service component.
    • Install Routing component.
  • Launch NPS Console.
  • Add Remote Access Clients (Radius Clients and Servers > Radius Clients > New):
    • Friendly Name:  Router1
    • IP Address:  192.168.1.254
    • Shared Secret:  CiscoRocks
    • Advanced > Vendor:  Cisco
    • Added remote access policy(Policies > Network Policies > New):
      • Name:  CiscoAuth
      • Access Permission:  Granted
      • Conditions: Add > Windows Groups > cisco.admin
      • Constraints: Authentication > Unencrypted Authentication PAP, SPAP
      • Idle Timeout:  10 minutes
      • Settings:
        • Service-Type = Login
        • Framed-Protocol = PPP

RADIUS Setup for Cisco Device:

  • Configure local user.
  • Configure SSH.
  • Configure Loopback
  • aaa new-model
  • radius-server host 192.168.1.10 auth-port 1645 acct-port 1646 key CiscoRocks
  • aaa authentication login AUTH group radius group ADRADIUS local
  • ip radius source-interface Loopback0
  • aaa group server radius ADRADIUS
  • server 192.168.1.10
  • line vty 0 4
  • login authentication AUTH
  • Allow 1723 & GRE in any ACLs between hosts.

Debug Commands:

debug aaa authentication
debug radius authentication
term mon

VBScript for Active Directory SID to Name Resolution

January 21, 2011 Leave a comment

I had a requirement to query a SID from another domain after SID to name resolution failed:

Set objDomain = GetObject("LDAP://dc=corporate,dc=contoso,dc=com")
strComputer = "DC1"
Set objSWbemLocator = CreateObject("WbemScripting.SWbemLocator")
Set objSWbemServices = objSWbemLocator.ConnectServer _
(strComputer, "root\cimv2", "CORPORATE\%USERNAME%", "%PASSWORD%")
objSWbemServices.Security_.ImpersonationLevel = 3

Set objAccount = objSWbemServices.Get _
("Win32_SID.SID='S-1-2-34-567891234-5678912345-678912345-67890'")
Wscript.Echo objAccount.AccountName

Set objDomain = Nothing
Set objSWbemLocator = Nothing
Set objSWbemServices = Nothing
Set objAccount = Nothing

If you want to query the user name for it’s associated SID, just replace:

("Win32_SID.SID='S-1-2-34-567891234-5678912345-678912345-67890'")
Wscript.Echo objAccount.AccountName

with:

("Win32_UserAccount.Name='%USERNAME%',Domain='CORPORATE'")
Wscript.Echo objAccount.SID

VBScript to Bulk Update UPNs in Active Directory

January 20, 2011 Leave a comment

I had a requirement to harvest all the users in a domain into an Excel spreadsheet then update the ones with the wrong UPN after some faceless entity added the wrong UPN to a number of user accounts:

sDomain="testlab"

Set oADconn = CreateObject("NameTranslate")
oADconn.Init 3, ""

Set oExcel= WScript.CreateObject("excel.application")
With oExcel
 .Visible = True
 .Workbooks.Add
 .Range("A1:C1").Select
 .Selection.Font.Bold = True
 .Cells(1,1).Value = "Display Name"
 .Cells(1,2).Value = "sAMAccountName"
 .Cells(1,3).Value = "UPN Suffix"
 .ActiveSheet.range("A2").Activate
End With

Set objDomainUsers = GetObject("WinNT://" & sDomain & ",domain")
objDomainUsers.Filter = Array("User")
On Error Resume Next
For Each oUserAcct In objDomainUsers
 oADconn.Set 3, sDomain & "\" & oUserAcct.Name
 sUserDN = oADconn.Get(1)
 Set oUser = GetObject("LDAP://" & sUserDN)
 sname=oUser.sAMAccountname
 supn=oUser.userprincipalname
 x=instr(supn,"@")
 supnsuffix=mid(supn,x+1)
 oExcel.activecell.Value=oUser.displayname
 oExcel.activecell.offset(0,1).Activate
 oExcel.activecell.Value=oUser.sAMAccountName
 oExcel.activecell.offset(0,1).Activate
 oExcel.activecell.Value=supn
 oExcel.activecell.offset(0,1).Activate
 oExcel.activecell.offset(1,-3).Activate
 if supnsuffix="testlab.priv" then
 oUser.userprincipalname=sname & "@corporate.test.int"
 oUser.SetInfo
 end if
Next
oExcel.Application.Quit

Cross Forest User Lookup LDAP Query Script

January 19, 2011 Leave a comment

I had a requirement to query users over an external trust between two AD forests, so here it is:

Const ADS_SCOPE_SUBTREE = 2
Set rootDSE = GetObject("LDAP://rootDSE")
Set objConnection = CreateObject("ADODB.Connection")
Set objCommand = CreateObject("ADODB.Command")
objConnection.Provider = "ADSDSOObject"
objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE
objCommand.CommandText = "SELECT * FROM 'LDAP://corporate.contoso.com/" _
"dc=corporate,dc=contoso,dc=com' WHERE objectCategory='user'"
Set objRecordSet = objCommand.Execute
objRecordSet.MoveFirst
Do Until objRecordSet.EOF
Set objType = GetObject(objRecordSet.Fields("ADsPath").Value)
strDistinguishedName = objType.distinguishedName
wscript.echo strDistinguishedName
objRecordSet.MoveNext
Loop

Does exactly what it says on the tin.

Query Last Logon for all Active Directory Users in any Domain

January 17, 2011 Leave a comment

I had a requirement to ascertain how many users in a domain hadn’t logged on in the last 3 months:

Option Explicit
Dim objRootDSE, strConfig, adoConnection, adoCommand, strQuery
Dim adoRecordset, objDC
Dim strDNSDomain, objShell, lngBiasKey, lngBias, k, arrstrDCs()
Dim strCN, dtmDate, objDate, objList, strUser
Dim strBase, strFilter, strAttributes, lngHigh, lngLowr

' Use a dictionary object to track latest lastLogon for each user.

Set objList = CreateObject("Scripting.Dictionary")

objList.CompareMode = vbTextCompare

' Obtain local Time Zone bias from machine registry.
' This bias changes with Daylight Savings Time.
Set objShell = CreateObject("Wscript.Shell")
lngBiasKey = objShell.RegRead("HKLM\System\CurrentControlSet\Control\" _
    & "TimeZoneInformation\ActiveTimeBias")
If (UCase(TypeName(lngBiasKey)) = "LONG") Then
    lngBias = lngBiasKey
ElseIf (UCase(TypeName(lngBiasKey)) = "VARIANT()") Then
    lngBias = 0
    For k = 0 To UBound(lngBiasKey)
        lngBias = lngBias + (lngBiasKey(k) * 256^k)
    Next
End If

' Determine configuration context and DNS domain from RootDSE object.

Set objRootDSE = GetObject("LDAP://RootDSE")

strConfig = objRootDSE.Get("configurationNamingContext")
strDNSDomain = objRootDSE.Get("defaultNamingContext")

' Use ADO to search Active Directory for ObjectClass nTDSDSA.
' This will identify all Domain Controllers.
Set adoCommand = CreateObject("ADODB.Command")
Set adoConnection = CreateObject("ADODB.Connection")
adoConnection.Provider = "ADsDSOObject"
adoConnection.Open "Active Directory Provider"
adoCommand.ActiveConnection = adoConnection

strBase = "<LDAP://" & strConfig & ">"
strFilter = "(objectClass=nTDSDSA)"
strAttributes = "AdsPath"
strQuery = strBase & ";" & strFilter & ";" & strAttributes & ";subtree"

adoCommand.CommandText = strQuery
adoCommand.Properties("Page Size") = 100
adoCommand.Properties("Timeout") = 60
adoCommand.Properties("Cache Results") = False

Set adoRecordset = adoCommand.Execute
' Enumerate parent objects of class nTDSDSA. Save Domain Controller
' AdsPaths in dynamic array arrstrDCs.
k = 0
Do Until adoRecordset.EOF
    Set objDC = _
        GetObject(GetObject(adoRecordset.Fields("AdsPath").Value).Parent)
    ReDim Preserve arrstrDCs(k)
    arrstrDCs(k) = objDC.DNSHostName
    k = k + 1
    adoRecordset.MoveNext
Loop
adoRecordset.Close

' Retrieve lastLogon attribute for each user on each Domain Controller.
For k = 0 To Ubound(arrstrDCs)
    strBase = "<LDAP://" & arrstrDCs(k) & "/" & strDNSDomain & ">"
    strFilter = "(&(objectCategory=person)(objectClass=user))"
    strAttributes = "sAMAccountName,lastLogon"
    strQuery = strBase & ";" & strFilter & ";" & strAttributes _
        & ";subtree"
    adoCommand.CommandText = strQuery
    On Error Resume Next
    Set adoRecordset = adoCommand.Execute
    If (Err.Number <> 0) Then
        On Error GoTo 0
        Wscript.Echo "Domain Controller not available: " & arrstrDCs(k)
    Else
        On Error GoTo 0
        Do Until adoRecordset.EOF
            strCN = adoRecordset.Fields("sAMAccountName").Value
            On Error Resume Next
            Set objDate = adoRecordset.Fields("lastLogon").Value
            If (Err.Number <> 0) Then
                On Error GoTo

                dtmDate = #1/1/1601#
            Else
               On Error GoTo 0
                lngHigh = objDate.HighPart
                lngLow = objDate.LowPart
                If (lngLow < 0) Then
                    lngHigh = lngHigh + 1
                End If
                If (lngHigh = 0) And (lngLow = 0) Then
                    dtmDate = #1/1/1601#
                    dtmDate = #1/1/1601# + (((lngHigh * (2 ^ 32)) _
                        + lngLow)/600000000 - lngBias)/1440
                End If
            End If
            If (objList.Exists(strCN) = True) Then
                If (dtmDate > objList(strCN)) Then
                    objList.Item(strCN) = dtmDate
                End If
            Else
                objList.Add strCN, dtmDate
            End If
            adoRecordset.MoveNext
        Loop
        adoRecordset.Close
    End If
Next

' Output latest lastLogon date for each user.
For Each strUser In objList.Keys    
    If (objList.Item(strUser) = #1/1/1601#) Then        
        Wscript.Echo strUser & ",Never"    
    Else        
        Wscript.Echo strUser & "," & objList.Item(strUser)    
    End If
Next

 ' Clean up.ado
Connection.Close

Many thanks to ADSI God: Richard Mueller for this!