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:

  1. It is a "turnkey" solution, providing the Windows Domain user account data without requiring further, detailed knowledge.
  2. Account analytics in AD with LDAP clients is cumbersome. Microsoft limits record sets to 1000, and filter rules are cryptic.
  3. 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.
===============================================================================
CSV file icon

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:

CSV script output example 1 for domain_userlist.vbs
CSV script output example 2 for domain_userlist.vbs

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

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.

Index:

Source Code:

See Also: