Introduction
In part 1, we explored how to extract Microsoft Windows Active Directory user data through LDAP. While using LDAP allows to browse all data that is available, it requires LDAP client software for accessing the Domain controllers.
What if we have have no LDAP client at hand? Are there any Windows built-in tools we can use?
Yes, Microsoft continues to provide commandline tools line net that allow us to retrieve a variety of domain data. By using aditional scripting, we can access and retrieve all data through Microsofts AxtiveX Data Objects (ADO), built-in to Microsofts Windows OS.
'net user /Domain' - Retrieving Domain user information
Let's start with a Windows 7 laptop as a domain member, together with a local domain user account.
in a first step we will retrieve all users in our current domain. (Belows data is just exemplary, not real output)
C:\>net user /Domain The request will be processed at a domain controller for domain frank4dd.com. User accounts for \\pdc02.frank4dd.com ------------------------------------------------------------------------------- @standarduser aaAdministrator3 abdolsu abahide abahima abahiro abakeni abakenj abamaki abamasa abamich abamike ... zhowouz zhuangi zunigje zunojes The command completed successfully.
We can also display details of each domain user returned:
C:\>net user /Domain abehide The request will be processed at a domain controller for domain frank4dd.com. User name abahide Full Name Hideki Abakor Comment HR department User's comment Country code 000 (System Default) Account active Yes Account expires Never Password last set 2013/01/23 8:11:36 Password expires 2013/03/09 8:11:36 Password changeable 2013/01/24 8:11:36 Password required Yes User may change password Yes Workstations allowed All Logon script f4ddlogon.bat User profile Home directory Last logon 2013/03/04 9:36:57 Logon hours allowed All Local Group Memberships Global Group memberships *acl_hrdrive_C *acl_f4dddrive_C *acl_f4dd_shared *acl_hrdrive_r *Domain Users The command completed successfully.
'net group /Domain' - Retrieving Domain group information
Same as for domain users above, the command net group /Domain will retrieve all Windows domain groups in Actice Directory. First, lets list up all Domain groups available in Active Directory.
C:\>net group /Domain The request will be processed at a domain controller for domain frank4dd.com. Group Accounts for \\pdc01.frank4dd.com ------------------------------------------------------------------------------ acl_hrdrive_C acl_f4dddrive_C acl_f4dd_shared ... The command completed successfully.
Lets see how we can check Domain group membership.
C:\>net group /Domain "acl_hrdrive_c" The request will be processed at a domain controller for domain frank4dd.com. Group name acl_hrdrive_c Comment created 12/Apr/2009 Members ------------------------------------------------------------------------------- abahide abemike ... The command completed successfully.
Use the 'net' command to display other useful domain Info
The command net accounts /Domain displays the Domain's password policy:
C:\>net accounts /Domain The request will be processed at a domain controller for domain frank4dd.com. Force user logoff how long after time expires?: 0 Minimum password age (days): 1 Maximum password age (days): 45 Minimum password length: 8 Length of password history maintained: 8 Lockout threshold: 3 Lockout duration (minutes): Never Lockout observation window (minutes): 1440 Computer role: BACKUP The command completed successfully.
The command net view /Domain displays all domains visible in the local network:
C:\>net view /Domain Domain ------------------------------------------------------------------------------- FRANK4DD WORKGROUP The command completed successfully.
The command net time /Domain displays all domain controllers current date and time:
C:\>net time /Domain Current time at \\pdc02.frank4dd.com is 2013/03/04 11:24:49 The command completed successfully.
Accessing AD through scripts
In cases when the 'net' command does not return enough information, we can turn our attention to the myriad of scripts that are available when browsing the Internet. Below is a version I created: domain_userlist.vbs. The script downloads the full list of Domain users, together with their password settings into a CSV file for easy user sorting. It works for acount extraction within the local domain, and with trusted domains, common in larger environments.
There are two reasons for using such a script:
- It is a "turnkey" solution, providing the Windows Domain user account data without requiring further, detailed knowledge.
- Account analytics in AD with LDAP clients is cumbersome. Microsoft limits record sets to 1000, and filter rules are cryptic.
- It can return the account data from trusted domains
Below is a example run of the script, followed by an output screenshot of the CSV results in Excel.
D:\Code\VBS>cscript domain_userlist.vbs Microsoft (R) Windows Script Host Version 5.8 Copyright (C) Microsoft Corporation. All rights reserved. Running domain_userlist.vbs for domain DC=frank4dd,DC=com ... =============================================================================== 1 Active IUSR_MCLSSERVER1 Internet Server Anonymous Access 2 Disabled krbtgt Key Distribution Center Service Account ... 6726 Active mizmike Mike Mizonus Hired 2010/01/03 =============================================================================== End of get_domain_userlist.vbs, run on Domain DC=frank4dd,DC=com. Extracted 6726 Records, 3712 disabled, 3014 active into file D:\Domain_Accounts.csv. ===============================================================================

Large organisations may easily return 50 thousand domain account records. Typically, 50-60% are disabled as organisations often deprovision by only disabling the terminated employee's account in Windows AD, plus move it into a non-production OU container.
Opening the resulting domain account CSV file in Excel will show the user account list similar to the example below:


Domain account analytics - Example verification of password policy compliance
The obvious benefit of the account record extraction to CSV is that we are now able to use various MS Excel filtering functions to check conditions are matching our corporate account and password policies. Most companies have the policy the require users to change their password in a defined period. If we filter the account list by "Status=Active" and "PW Expiry=Never", we can see which accounts are exempted from password change. Normally, we should see by account name and account description that only special system accounts, such as used for batch transfer, monitoring or firecall usage - are excempted.
In real live, above policy verification may discover that a large number of corporate senior executives excempt themselves from such bothering tasks like changing passwords. It is a sad fact that different rules exist for common employees, and that executive PC's and access may not appear worth to be strongly protected. A problem of ethics, and leadership by example that can be a minefield to navigate.
Conclusion
Windows comes with builtin tools that allows us to retrieve Windows Domain information about users, groups, and other settings. With additional scripting, we can obtain almost all information available in Active Directory. Simply downloading and running the script will do.
Because the script programming techniques in extracting windows domain account data could help others, below is the full VBS code listing with comments. If you have enhancements or corrections, it would be great to share.
'===========================================================================
' domain_userlist.vbs v1.1 @2013 by Frank4dd http://www.frank4dd.com/howto
'
' Description: This script collects all active and disabled accounts from a
' selected active directory domain. The results are saved into
' a local CSV file for further data mining in MS Excel.
'
' Run script: C:\> cscript domain_userlist.vbs
'
' This script exists thanks to the research and publishing of other authors.
' It is free for use and enhancements. Although care has been taken during
' development, there is no warranty that it will work in all circumstances.
'
' Troubleshooting: Remember that AD records can always contain strange and
' unexpected values (i.e. from operator typo's, etc) that could prevent a
' correct displayed CVS format. In that case, a good approach is to open the
' CSV results file with a editor and validate the problematic line(s)
' through a direct record lookup using a LDAP client against AD.
'===========================================================================
On Error Resume Next
'===========================================================================
' This script is intended to run on the Windows commandline under cscript.
' If run under wscript (user doubleclick), show a warning message and exit.
'===========================================================================
strScriptHost = LCase(Wscript.FullName)
If Right(strScriptHost, 11) = "wscript.exe" Then
Wscript.Echo WScript.ScriptName & " should run only under cscript. Exiting..."
Wscript.Quit
End If
'===========================================================================
' Global variables
'===========================================================================
Dim fso, myFile, selectedDomain, selectedFields, csvResultFile, csvHeaderLine
Dim objConnection, objCommand, objRecordSet
'===========================================================================
' Global constants: Active Directory userAccountControl flag values, see
' http://support.microsoft.com/kb/305144
'===========================================================================
Const ADS_UF_PASSWD_CANT_CHANGE = &H40
Const ADS_UF_ACCOUNTDISABLE = &H0002
Const ADS_UF_DONT_EXPIRE_PASSWD = &H10000
Const ADS_SCOPE_SUBTREE = 2
'===========================================================================
' Here we set the domain to query. For queries within the local domain, the
' domain controller is not necessary, we can set domainController = "local".
' If it is a trusted domain in our environment we need to set the Controllers
' DNS name or IP address. Example:
' domainController = "mypdc01"
' domainDnsName = "frank4dd.com"
' selectedDomain = "DC=frank4dd,DC=com"
'===========================================================================
domainController = "192.168.10.21"
domainDnsName = "frank4dd.com"
selectedDomain = "DC=frank4dd,DC=com"
'===========================================================================
' The name (and optional path) for the CSV result file.
'===========================================================================
csvResultFile = "Domain_Accounts.csv"
'===========================================================================
' Definition of the AD user account fields to retrieve, and our Name for it.
' For a more complete listing of possible fields and their description, see
' http://fm4dd.com/programming/check-id-active-directory.htm#query
' The number of csvHeaderNames should match the value output at script end.
'===========================================================================
selectedFields = "CN,sAMAccountName,displayName,userPrincipalName," & _
"userAccountControl,whenCreated,whenChanged," & _
"pwdLastSet, accountExpires,description"
csvHeaderLine = "#,Status,User Name (CN),Account Name,Display Name," & _
"Principal Name,Creation Date,Last Update," & _
"Account Flags,PW Last Change,PW Expiry," & _
"Account Expiry,Description"
'===========================================================================
' Create the file handles: stdout, stderr and the CSV file for the results.
'===========================================================================
Set fso = CreateObject("Scripting.FileSystemObject")
Set stdout = fso.GetStandardStream (1)
Set stderr = fso.GetStandardStream (2)
Set myFile = fso.CreateTextFile(csvResultFile, True)
localPath = left(WScript.ScriptFullName,_
(Len(WScript.ScriptFullName))-(len(WScript.ScriptName)))
'===========================================================================
' Start Console and file output: Create the CSV text file header line
'===========================================================================
stdout.WriteLine "Running " & WScript.ScriptName & " for domain " & _
selectedDomain & " ..."
stdout.WriteLine "=========================================================="
stdout.WriteLine
myFile.WriteLine csvHeaderLine
'===========================================================================
' Get domain's password age settings through the WinNT provider. See also
' http://www.rlmueller.net/WinNT_Binding.htm
'===========================================================================
If (domainController = "local") Then
domainConnect = domainDnsName
Else
domainConnect = domainController & "/" & selectedDomain
End If
Set objDomain = GetObject("LDAP://" & domainConnect)
' debug output connection string
stdout.WriteLine "LDAP://" & domainConnect
Set objMaxPwdAge = objDomain.Get("maxPwdAge")
' Convert the received Integer8 64bit value into days.
' Account for bug in IADslargeInteger property methods.
lngHighAge = objMaxPwdAge.HighPart
lngLowAge = objMaxPwdAge.LowPart
If (lngLowAge < 0) Then
lngHighAge = lngHighAge + 1
End If
intMaxPwdAge = -((lngHighAge * 2^32) _
+ lngLowAge)/(600000000 * 1440)
' debug output for intMaxPwdAge
stdout.WriteLine "Domain max password age: " & intMaxPwdAge
'===========================================================================
' Configure the LDAP query through the ADO provider. See also
' http://support.microsoft.com/kb/187529
'===========================================================================
Set objConnection = CreateObject("ADODB.Connection")
objConnection.ConnectionTimeout = 5 '-- default is 15secs --
objConnection.Provider = ("ADsDSOObject")
objConnection.Open "Active Directory Provider"
Set objCommand = CreateObject("ADODB.Command")
objCommand.CommandTimeout = 10 '-- default is 30secs --
objCommand.ActiveConnection = objConnection
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE
objCommand.Properties("Page Size") = 3000
objCommand.CommandText = "SELECT " & selectedFields & _
" FROM 'LDAP://" & domainConnect & _
"' WHERE objectCategory='user'"
' Debug line: write query command to console
stdout.WriteLine objCommand.CommandText
'===========================================================================
' Execute the Query
'===========================================================================
Set objRecordSet = objCommand.Execute
'===========================================================================
' Loop through all records found in AD. Write shortened record values for
' progress control to console, and write the results into the CSV file.
'===========================================================================
recordCount = 0
activeCount = 0
disabledCount = 0
Do Until objRecordSet.EOF
Dim sStatus, dName, cnName, uDesc, UacFlag, pCantChg, pLastChg, pExpiry
recordCount = recordCount + 1
'=========================================================================
' Get the date of last password set, skip if it is zero
'=========================================================================
pLastChgDate = Integer8Date(objRecordSet.Fields("pwdLastSet").Value)
If (pLastChgDate = #1/1/1601#) Then
pLastChgDate = "Unset"
End If
'=========================================================================
' Check the userAccountControl various flag settings here
'=========================================================================
UacFlag = objRecordSet.Fields("userAccountControl").Value
' Account disabled?
If (UacFlag AND ADS_UF_ACCOUNTDISABLE) <> 0 Then
sStatus = "Disabled"
disabledCount = disabledCount+1
Else
sStatus = "Active"
activeCount = activeCount+1
End If
pExpiry = "Unset"
' PW never expires?
If (UacFlag AND ADS_UF_DONT_EXPIRE_PASSWD) <> 0 Then
pExpiry = "Never"
End If
' pLastChgDate = 0 and DONT_EXPIRE flag does not exist?
If (pExpiry = "Unset") AND (pLastChgDate = "Unset") Then
pExpiry = "ChgNextLogin"
End If
' None of the cases above, calculate expiration
If (pExpiry = "Unset") Then
' Calculate the PW expiry: last change date + PW policy MaxPwdAge
pExpiry = DateValue(pLastChgDate + intMaxPwdAge)
' debug line
' stdout.WriteLine "pExpiry = " & pLastChgDate & "+" & intMaxPwdAge
End If
' PW cannot change?
If (UacFlag AND ADS_UF_PASSWD_CANT_CHANGE) <> 0 Then
pCantchg = "Set"
Else
pCantChg = " "
End If
'=========================================================================
' The displayName may have Commas troubling Excel, replace with space
'=========================================================================
dName = ""
dName = Replace(cStr(objRecordSet.Fields("displayName").Value), ",", " ")
'=========================================================================
' The CN may have strange values (duplicates) as below:
'mahonkh
'CNF:5f77a3e7-1fa1-4b02-a37f-cd9ac14b5ed1
' It may also have commas, we replace them with space for valid CSV
'=========================================================================
cnName = ""
cnName = Replace(cStr(objRecordSet.Fields("CN").Value), vblf, " ")
cnName = Replace(cnName, ",", " ")
'=========================================================================
' The Description may have a commas or ", replace commas and " with space.
' Because this field has the type "advariant", we need to loop through it.
'=========================================================================
uDesc = ""
For Each SubValue in objRecordSet.Fields("description").Value
uDesc = uDesc & Replace(SubValue, ",", " ")
uDesc = Replace(uDesc, """", " ")
Next
'=========================================================================
' If the account expiration is disabled, We set a readable name for '0'
'=========================================================================
If objRecordSet.Fields("accountExpiry").Value = "0" Then
sExpiry = "Never"
Else
sExpiry = objRecordSet.Fields("accountExpiry").Value
End If
'=========================================================================
' Generate the query output to console, select fields to be included
'=========================================================================
stdout.WriteLine recordCount & " " & sStatus & " " & _
objRecordSet.Fields("sAMAccountname").Value & _
" " & dName & " " & " " & uDesc
'=========================================================================
' Write the individual field and processed values to the CSV file.
' The number of fields should match the header line descriptions above.
'=========================================================================
myFile.WriteLine recordCount & "," & _
sStatus & "," & _
cnName & "," & _
objRecordSet.Fields("sAMAccountName").Value & "," & _
dName & "," & _
objRecordSet.Fields("userPrincipalName") & "," & _
objRecordSet.Fields("whenCreated") & "," & _
objRecordSet.Fields("whenChanged") & "," & _
objRecordSet.Fields("userAccountControl").Value & "," & _
pLastChgDate & "," & _
pExpiry & "," & _
sExpiry & "," & _
uDesc
objRecordSet.MoveNext
Loop
objRecordSet.Close
myFile.Close
stdout.WriteLine
stdout.WriteLine "=========================================================="
stdout.WriteLine "End of " & WScript.ScriptName & ", run on Domain" & selectedDomain & "."
stdout.WriteLine "Extracted " & recordCount & " Records, " & _
disabledCount & " disabled, " & _
activeCount & " active into file " & _
localPath & csvResultFile & "."
stdout.WriteLine "=========================================================="
stdout.Close
stderr.Close
'===========================================================================
' End of Main
'===========================================================================
'===========================================================================
' Function to convert Integer8 (64-bit) value to a date, adjusted for local
' time zone bias. This function is necessary because the object "pwdLastSet"
' is stored as a Windows internal time object. See also
' http://msdn.microsoft.com/en-us/library/windows/desktop/ms679430(v=vs.85).aspx
'===========================================================================
Function Integer8Date(objDate)
Dim lngAdjust, lngDate, lngHigh, lngLow
lngHigh = objDate.HighPart
lngLow = objdate.LowPart
' Obtain local Time Zone bias from machine registry.
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
lngAdjust = lngBias
' Account for error in IADslargeInteger property methods.
If lngLow < 0 Then
lngHigh = lngHigh + 1
End If
If (lngHigh = 0) And (lngLow = 0) Then
lngAdjust = 0
End If
lngDate = #1/1/1601# + (((lngHigh * (2 ^ 32)) _
+ lngLow) / 600000000 - lngAdjust) / 1440
Integer8Date = CDate(lngDate)
End Function
Other solutions and links
Since accessing and extracting data from active directory is a frequent and helpful task, numerous tools, both free and commercial exist. The link below I found particularly helpful.