Monday, April 21, 2008

Active Directory _msdcs DNS zones

This post contains information on troubleshooting DNS lookup of Domain Controllers in cross-forest trusts, relating primarily to to _msdcs sub-zones, and highlighting differences between Windows 2000 and 2003 DNS zone configuration .

In a Windows Server 2003 domain, the _msdcs zone is a separate zone in DNS to the parent domain. A delegated sub-zone is created in the parent domain, with static NS records indicating the DNS servers that can be used for the _msdcs zone. If these records are incorrect, problems will occur when identifying Active Directory domain information, particularly in cross-forest scenarios when trying to find a DC through DNS.

In Windows 2000 Server Active Directory, the _msdcs is a sub-zone of the parent domain, and replicated as a single entity, without the requirement for static NS records. While this configuration is simpler, it does not suit some environments, where not all DNS servers should have replicated copies of the _mcdcs zones.

In a cross-forest scenario, the 2003 domain was logging Event ID 5719:

This computer was not able to set up a secure session with a domain controller
in domain domain due to the following:
There are currently no logon servers available to service the logon request.
This may lead to authentication problems. Make sure that this computer is connected to the network. If the problem persists, please contact your domain administrator.

If this computer is a domain controller for the specified domain, it
sets up the secure session to the primary domain controller emulator in the
specified domain. Otherwise, this computer sets up the secure session to any
domain controller in the specified domain.

For more information, see
Help and Support Center at
Using netlogon debugging (nltest /dbflag), errors were logged indicating something is not working:

In this example, cached information was being used:
01/01 08:00:00 [MISC] NetpDcGetName: domain.local using cached information
01/01 08:00:00 [MISC] domain: DsGetDcName function returns 0: Dom:domain.local Acct:(null) Flags: IP KDC

But as soon as the cache expired, problems started occurring:
01/01 09:31:17 [SESSION] domain: NETLOGON_CONTROL_TC_QUERY function received.
01/01 09:31:17 [CRITICAL] domain: NetrLogonControl can't find the client structure of the domain domain.local specified.

A test query using DNS directly was then performed, which failed in this case, as the single NS record in the _msdcs delegated sub-zone in was referring to a DC that had been decommissioned:

nslookup -type=srv

*** dc01.domain.local can't find Server failed
Server: dc01.domain.local

Note that using the '-debug' switch in the nslookup command provides detailed information on the request and reply, for example, using the delegated sub-zone querying a secondary copy of

nslookup -debug dc01
Server: dc01.domain.local

Got answer:
opcode = QUERY, id = 2, rcode = NOERROR
header flags: response, auth. answer, recursion avail.
questions = 1, answers = 0, authority records = 1, additional = 0

QUESTIONS:, type = A, class = IN
ttl = 3600 (1 hour)
primary name server =
responsible mail addr = hostmaster
serial = 286
refresh = 900 (15 mins)
retry = 600 (10 mins)
expire = 86400 (1 day)
default TTL = 3600 (1 hour)


Wayne's World of IT (WWoIT), Copyright 2008 Wayne Martin.

Read more!

Unlocking XP/2003 without passwords

The winlogon secure desktop is really just another desktop on winsta0, and I came across a utility - RemoteUnlock.exe - which injects itself as a thread in the console winlogon process of a remote machine and switches from the secure winlogon desktop to the logged on user’s winsta0\default desktop.

This allows you to skip the SAS process requiring the account password of the currently logged on user, and just shows the desktop, allowing full console interaction as the logged on user.

This doesn't really have very many practical uses, but the concept is great and I thought I would share the utility I came across plus some additional thoughts on the topic.

The utility and source code:

I've previously toyed with creating a command shell on a remote winlogon desktop, which can be done with the following command:
- psexec /s \\%computer% cmd /c c:\windows\temp\psexec /accepteula /x /d /s cmd

After another discussion regarding secure passwords with PowerShell, I was curious just what might be possible from the Winlogon desktop.

One thing led to another, and after trying unsuccessfully using a PowerShell script run from a command shell on the Winlogon desktop (using psexec) calling GetProcessWindowStation/UnlockWindowStation (an undocumented API, presumably unlocking a window station) and OpenDesktop/SwitchDesktop , I came across a reference to UnlockWindowStation which mentioned that this would only work when running as part of winlogon.exe, explaining why the PowerShell script did nothing.

I then successfully tested remoteunlock.exe running from one XPSP2 workstation against another. RemoteUnlock uses switchdesktop which doesn’t actually unlock the desktop, I was going to recompile and try unlockwindowstation, but I don’t have Visual Studio. It would also be interesting to modify this to work with TS winlogon processes, rather than only the interactive console (I presume you could similarly ‘unlock’ an in-use TS session).

As an aside, below is the PowerShell script calling APIs through VB.Net embedded code, which doesn’t work in this case but it's still a valid example of how to call APIs from PowerShell (which I essentially just copied from

$provider = new-object Microsoft.VisualBasic.VBCodeProvider
$params = new-object System.CodeDom.Compiler.CompilerParameters
$params.GenerateInMemory = $True
$refs = "System.dll","Microsoft.VisualBasic.dll"

$txtCode = @'
Class FindProcessWinStation
    Declare Auto Function GetProcWinStation Lib “user32.dll” Alias "GetProcessWindowStation" () As Integer
    Declare Auto Function UnlockWinStation Lib “user32.dll” Alias "UnlockWindowStation" (ByVal WinSta As Integer) As Integer
    Declare Auto Function OpenWinStation Lib “user32.dll” Alias "OpenWindowStation" (ByVal lpszWinSta As String, ByVal fInherit as Boolean, ByVal ACCESS_MASK as Integer) As Integer
    Declare Auto Function OpenDesktop Lib “user32.dll” Alias "OpenDesktop" (ByVal lpszDesktop As String, ByVal dwFlags as Integer, ByVal fInherit as Boolean, ByVal ACCESS_MASK as Integer) As Integer
    Declare Auto Function SwitchDesktop Lib “user32.dll” Alias "SwitchDesktop" (ByVal hDesktop As Integer) As Integer
    Function Main()
        main = GetProcWinStation()
'        UnlockWinStation(main)
'        main = OpenWinStation("winsta0\\desktop", True, 895)
        main = OpenDesktop("Default", 0, True, 256)

    End Function
end class

$results = $provider.CompileAssemblyFromSource($params, $txtCode)
$mAssembly = $results.CompiledAssembly
$i = $mAssembly.CreateInstance("FindProcessWinStation")
$r = $i.main()

write-host $r

Wayne's World of IT (WWoIT), Copyright 2008 Wayne Martin.

Read more!

Wednesday, April 16, 2008

2003 Cluster-enabled scheduled tasks

Creating a cluster-aware scheduled task has several benefits and has historically been quite difficult. The volume shadow copy service task resource type in Windows Server 2003 clusters provides a mechanism to allow scheduled task capability as a cluster resource. Despite the name, this resource type seems to be a generic cluster resource that provides access to the standard task scheduler interface to schedule and run any command within a resource group.

This post provides information on creating the cluster resource using the cluster.exe command-line interface, some best practices (in my opinion) - preventing this resource from affecting the group, network and disk dependencies, using local scripts and some background in the LooksAlive/IsAlive functions provided by this resource type.

  1. Create the scheduled task cluster resource:

    cluster /cluster:%Cluster% res "TaskName" /create /group:"BNE-VFP03-CL4" /type:"Volume Shadow Copy Service Task"
    cluster /cluster:%Cluster% res "%TaskName%" /priv ApplicationName="cmd.exe"
    cluster /cluster:%Cluster% res "%TaskName%" /priv ApplicationParams="/c Command-Batch-Or-Script"
    cluster /cluster:%Cluster% res "%TaskName%" /priv CurrentDirectory=""
    cluster /cluster:%Cluster% res "%TaskName%" /prop Description="Task Description"
    cluster /cluster:%Cluster% res "%TaskName%" /AddDep:"%NetworkName%"
    cluster /cluster:%Cluster% res "%TaskName%" /AddDep:"%PhysicalDisk%"
    cluster /cluster:%Cluster% res "%TaskName%" /prop RestartAction=1
    cluster /cluster:%Cluster% res "%TaskName%" /On

  2. Set the schedule for the cluster resource:

    Use the cluster administrator GUI, this cannot currently be set with cluster.exe in Windows Server 2003 clusters.

  3. Restart the resource to pickup the schedule change:

    cluster /cluster:%Cluster% res "%TaskName%" /Off
    cluster /cluster:%Cluster% res "%TaskName%" /On


  1. The cluster resource providing scheduled task capability is the 'Volume Shadow Copy Service Task' resource. This is a recommended solution from Microsoft for providing scheduled ask capability on a cluster. See the 'Volume Shadow Copy Service resource type' reference.
  2. The LooksAlive and IsAlive functions for the VSSTask.dll simply check that the scheduled task is known to the local task scheduler. However, to further reduce the impact of resource failure, this resource has been marked as not affecting the cluster, preventing potential failover if this task were to fail more than the default of three times.
  3. This causes the creation of a schedule job using the %TaskName% you have chosen in (by default) the %systemroot%\tasks folder. Any existing local computer task with the same name would be overwritten.
  4. When specifying a command to run, it is safer to run a command from local disk. If something in the command were to cause the cluster resource to fail, you would have to modify the task resource before bringing the disk online. Having the commands on local disk allows easy changes. The drawback of this approach is that you'll have to either manually copy or have a process that copies the commands to each node of your cluster.


Cluster resource

Volume Shadow Copy Service resource type

With the Volume Shadow Copy Service Task resource type, you can create jobs in
the Scheduled Task folder that must be run on the node that is currently hosting
a particular resource group. In this way, you can define a scheduled task that
can failover from one cluster node to another. However, in the Microsoft®
Windows Server 2003 family of products, the Volume Shadow Copy Service Task
resource type has limited capabilities for scheduling tasks and serves primarily
to support Shadow Copies of Shared Folders in a server cluster. If you need to
extend the capabilities of this resource type, consider using the Generic Script
resource type

Using Shadow Copies of Shared Folders in a server cluster

Scheduled task does not run after you push the task to another computer

Scheduled Task for the Shadow Copies of Shared Folders Feature May Not Run on a Windows Server 2003 Cluster

Behavior of the LooksAlive and IsAlive functions for the resources that are included in the Windows Server Clustering component of Windows Server 2003

Wayne's World of IT (WWoIT), Copyright 2008 Wayne Martin.

Read more!

Purging aged files from the filesystem

This post provides information on a simple batch file to purge files based on a specified age. There are many more clever ways to achieve this - VBScript, PowerShell, cleanmgr job, but this is a very simple method that provides all the functionality with verbose logging, with next to no effort.

To use this, copy the text below to a batch file, and then schedule it to run every day/week. There are only two lines required - using Robocopy to move the files, and then another call to delete the files. It would be even easier if Robocopy supported a NUL destination, but there are advantages to moving and then deleting, for example you could move files every day, and then delete them weekly/monthly.

Note that the target below is the %temp% directory, usually on C: drive. Therefore if you are purging large quantities of data from a disk other than C:, using the system/boot disk is not really appropriate, perhaps change to a directory on the disk you are purging (which would therefore not require any extra space as the files are being moved locally).


Set FileAge=30

Set Directory=c:\Dir\To\Purge
Set PurgeDir=%Temp%\Purge_%Random%

for /f "tokens=1-8 delims=/:. " %%i in ('echo %date%') do Set DateFlat=%%l%%k%%j
Set LogFile=c:\logs\%~n0_%DateFlat%.log

Echo %Date% %Time%: Purging files from %Directory% older than %FileAge% days, logfile: %LogFile% >> %LogFile%
robocopy %Directory% "%PurgeDir%" *.* /minage:%FileAge% /v /fp /ts /mov /e /r:1 /w:1 /log+:%LogFile%

If Exist "%PurgeDir%" echo Deleting files moved with robocopy: 'rd /s /q "%PurgeDir%"' >> %LogFile%
If Exist "%PurgeDir%" rd /s /q "%PurgeDir%"


how to create your own registration of an existing handler (such as the disk cleanup handler).

Wayne's World of IT (WWoIT), Copyright 2008 Wayne Martin.

Read more!

Tuesday, April 8, 2008

Finding customised ADM templates in AD

Customised ADM files can be used to manage registry stamping, controlled through standard Group Policy Objects. Due to the nature of how these template values are stored, and combined with standard sysvol Group Policy structure, it's not always to find templates, policies and what settings will be applied. This post provides several command-line methods.

Find customised policies. These ADM files are replicated throughout the domain and are considered authoritative:

  1. dir \\%fqdn%\sysvol\%fqdn%\*.adm /s
  2. dir \\\sysvol\\*.adm /s/b find /i /v "wuau.adm" find /i /v "system.adm" find /i /v "wmplayer.adm" find /i /v "inetres.adm" find /i /v "conf.adm"
Query AD to match the GPO GUID to the display name:

  1. dsquery * "CN=Policies,CN=System,DC=domainRoot" -filter '&(objectCategory=groupPolicyContainer)(cn=%GUID%)' -attr Name displayName
  2. dsquery * "CN=Policies,CN=System,DC=domainRoot" -filter "&(objectCategory=groupPolicyContainer)(cn={F0A33B85-963E-4dF5-A425-E6E0894732DB})" -attr Name displayName
Add a custom ADM using GPMC:
  1. Use GPMC to edit the GPO
  2. Select the Administrative Templates, right-click, select 'Add/Remove Templates'
  3. Select the local copy of the customised ADM file
  4. This copies the adm file to the GUID GPO in the replicated sysvol area. (eg. file://
  5. Clear the 'Only show policy settings that can be fully managed' setting in View - filtering
  6. Navigate to the hierarchy added through the ADM
  7. Enable settings as appropriate
View the registry settings that will be applied as part of a GPO:
  1. Find the registry.pol for the GPO, see the commands above.
  2. regview Registry.pol
  1. This should first be performed in a test-lab environment, before adding the ADM to the production sysvol area.
  2. These settings are not visible by default, as GPMC hides policy settings that cannot be fully managed. To change, Computer Configuration or User Configuration Administrative Templates View Filtering 'Only show policy settings that can be fully managed' unticked.
  3. Customised template settings to not show up in the 'Settings' tab of GPMC
  4. Regview.exe can be found in the Windows Server 2003 Deployment Kit

Wayne's World of IT (WWoIT), Copyright 2008 Wayne Martin.

Read more!

Sunday, April 6, 2008

Domain local security groups for cross-forest security

With multi-domain forests there is a chance of access control entries being ignored when querying a Global Catalog if domain local groups for a domain other than that of the current GC are used to secure directory objects. This can include both allowing and denying access, each of which carry risks. This is mitigated somewhat as this is limited to GC read-only operations on secured Active Directory objects, but still needs to be carefully assessed before using in multi-domain forests. There are no known issues with single-domain forests.

These issues arose in the scenario of a cross-forest administration domain, where there is little choice but to use domain local groups to secure directory objects in managed domains, thereby enabling access to be granted to global/universal groups across-forests.

This goes against general Microsoft recommendation, and some research has been completed to further understand the limitations.


  1. Connecting with a cross-forest account to administer a trusting domain.
  2. The cross-forest account is a member of a global or universal group in the trusted domain
  3. The cross-forest group is a member of a trusting domain local group
  4. ACLs are applied on directory objects to the domain local group.
  5. The trusting domain is one of several domains in a forest
  6. There is an external one-way NTLM trust, with each domain in the forest being accessed (but I don't believe a forest trust would change the result)
  7. ACLs are set on a local group in the trusting domain denying access to the read a security group object.
  8. The user is a member of this cross-forest domain global group that is a member of the domain local group to deny access

The cross-forest user tries to query the secured object:

  • dsquery * "CN=SecurityGroupName,CN=Users,%domainDN%" -attr * -gc

As access is denied on this object, Dsquery reports:

  • dsquery failed:The specified directory service attribute or value does not exist.

However, to bypass the security applied to the ACE, you could target the GC query at the other trustin domain in the forest:
  • dsquery * "CN=SecurityGroupName,CN=Users,%domainDN%" -attr * -gc -s

Object is successfully enumerated as the trusting domain local ACE is ignored.

The same occurs in the scenario of granting specific access to an object that would otherwise be denied or not implicity granted.

What this does not affect:

  1. Single-domain forests. The scope of domain local is not an issue as there is only one domain.
  2. LDAP 389 operations to a domain partition on a DC, rather than a read-only GC type query.
  3. Samr/netlogon operations to a Domain Controller in the trusting domain
    Replication issues. Originally it was thought that this could cause issues with replication consistency, this is not the case and only affects security on the GC.

What this could affect:

  1. Security on automated GC usage, such as Universal group enumeration (during logon or otherwise), forest-wide searches, Exchange address-book lookups and UPN logons
  2. Security on any manually initiated GC query (such as used in the testing).


  1. Application directory partitions with a forest-wide replication scope, such as forest dns zones. It seems the 'Security descriptor reference domain' of an application partition partially solves this issue, in addition to GC not containing replicas of any application partitions and an application directory partition can't contain security principals.


  1. The user token contains universal, global and domain local groups for the user domain (cross-forest), not domain local groups in the DC domain. An impersonation token is created for the GC access, containing the forest-wide universal, domain global and domain local scope of the domain that the GC is in.
  2. The same can be seen through the GUI when connecting with ldp.exe to the a GC instance of one domain over 3268, and then browsing to the other domain tree
  3. Groups of scope domain local are partially replicated to the Global Catalog in the forest, so when viewing the security descriptor of an object that contains a domain local reference from another domain than the domain of the connected GC, the SID is easily resolved to a name. However, the token used to authenticate the GC query contains only the domain local groups in the GC's domain, not the intended trusting domain, causing the ACE to be ineffective.
  4. Exchange 2007 has some capability to automate the configuration of cross-forest Exchange administration. I assume it won't be too long before a Windows AD administration equivalent is available. There is reference to parallel groups and then using the ForeignForestFQDN Exchange setup option, I'm unsure of the outcome, but I assume it is universal->local->ACE (see Step 7 in 'How to Configure Cross-Forest Administration')
Excerpt from 'Global catalog replication' below:

A global catalog stores a replicated, read-only copy of all objects in the forest and a partial set of each object's attributes, including the security descriptor for each object. The security descriptor contains a discretionary access control list (DACL), which specifies permissions on the object. When a user connects to a global catalog and tries to access an object, an access check is performed based on the user's token and the object's DACL. Any permissions specified in the object's DACL for domain local groups that are not from the domain that the domain controller hosting the global catalog (to which the user has connected) belongs to, will be ineffective because only domain local groups from the global catalog's domain of which the user is a member are represented in the user's access token. As a result, a user may be denied access when access should have been granted, or allowed access when access should have been denied.

As a best practice, you should avoid using domain local groups when assigning permissions on Active Directory objects, or be aware of the implications if you do use them. To prevent unauthorized access to global catalog data, use global groups or universal groups instead. For information about global and universal groups, see Group scope.

Global catalog replication

DNS zone replication in Active Directory

Application directory partitions

What Is the Global Catalog?

What's New in Active Directory

How to Configure Cross-Forest Administration (Exchange 2007)

Exchange 2007 Permission Considerations

Multiple Forest Considerations in Windows 2000 and Windows Server 2003

Group Scope (2003):

Group Type and Scope Usage in Windows

Accessing resources across-forests

Accessing resources across domains

Wayne's World of IT (WWoIT), Copyright 2008 Wayne Martin.

Read more!

Account Management eventlog auditing

This post describes a method of account management event log auditing to extract relevant events from a domain for auditing and analysis. It's a simple batch file running on a server to export daily logs and collate them based on month.

Note that this is not a replacement for - or even an attempt at - a security event log management tool, it's really just a quick method of gathering events occurring in a Windows Active Directory domain.

Installation and use

  • Create ADExportEvents.bat using the batch commands below
  • Create AccountManagementEvents.txt as below or modify to suit your needs
  • Create directories to store the batch file, dumpel, daily and month logs. Get dumpel.exe from windows resource kit if you don't already have it
  • Test the batch file works from the command prompt.
  • Create a scheduled task on the DC you are running this on to run the batch file every day

This should allow:

  • Administrators to interrogate the logs, showing a holistic view of changes made throughout the domain without having to look at each DC
  • Easy tracking of one-off problems (eg. somebody accidentally deletes a user/group/group membership with or without realising)
Any number of events can be monitored, however the dumpel.exe utility used to extract the events has a limit of 10 IDs per extraction. Interesting events have been grouped in categories based on the object being targeted, which allows:
  • Inter-related events to show up in a single log file (eg. A user being created, their account being automatically locked, and then the password change to fix the problem)
  • Log files to be separated based on group, simplifying the process when looking for a particular event (eg, when a group was deleted)
To ensure this is a low-maintenance solution the task:
  • Dynamically queries the directory for a list of DCs to operate against
  • Works from a simple batch file that calls a Microsoft Resource Kit utility to export the events
  • Uses an input file describing the events to be collected in CSV format, enabling new events to be added to an existing group and new groups to be added simply by editing the control file.
  • Account Management Auditing is turned on for the domain
  • Each DC has a large enough security log to store n hours of security events, where n is length between scheduled task runs
Permanent Logging

To provide logs collated by month, the following command is executed as part of the batch file. Adjust the %MonthlyLogDir% to whatever directory you choose.

For /f %%p in (%UniqueGroups%) do for /f "tokens=3,4 delims=/ " %%i in ('echo %date%') do if exist %LogDir%\AcctMgmt_%%p_%%j%%i??.txt copy %LogDir%\AcctMgmt_%%p_%%j%%i??.txt %MonthlyLogDir%\AcctMgmt_%%p_%%j%%i.txt /y 1>nul


The batch file runs from one DC in the domain, querying all other DCs. This was completed by modifying the Domain Controllers Group Policy to allow the 'Domain Controllers' security group to have the 'Manage Auditing and Security Log' right, allowing any DC to look at the security log of any other DC.

Advantages of this approach:
  • Caters dynamically for the scenario when the DC performing the query changes, without having to modify scripts
  • Caters dynamically when new DCs are added, the DC running the script will have access
  • There is no need to create/maintain a new security group


  • Potential security risk - anyone with unauthorised access to a domain controller computer context can make changes to security logs and configure object access auditing on DCs. This is mitigated somewhat by the fact that if someone can run something as a DC computer account context then by default they have the rights to make any changes in the domain anyway...

Possible Improvements

  • Have each Domain Controller export local events and passing the information to a central server. If a large number of remote DCs across slow links were used (for example, more than 50 or 100) the current process would be unmanageable.

This is the CSV file used to define the group, the event ID and a description of the event, used by the batch file to determine which events to dump:



Computer,645,A computer account was created.
Computer,647,A computer account was deleted.
Policy,643,A domain policy was modified.

Account,624,A user account was created.
Account,630,A user account was deleted.
Account,685,Name of an account was changed.
Account,684,Set the security descriptor of members of administrative groups, Every 60 minutes on a domain controller a background thread searches all members of administrative groups (such as domain, enterprise, and schema administrators) and applies a fixed security descriptor on them. This event is logged.

AccountPwd,627,A user password was changed.
AccountPwd,628,A user password was set.
AccountPwd,644,A user account was auto locked.

Group,631,A global group was created.
Group,634,A global group was deleted.
Group,635,A new local group was created.
Group,668,A group type was changed.
Group,639,A local group account was changed.
Group,638,A local group was deleted.
Group,649,A local security group with security disabled was changed.
Group,648,A local security group with security disabled was created, SECURITY_DISABLED in the formal name means that this group cannot be used to grant permissions in access checks.

GroupMembership,632,A member was added to a global group.
GroupMembership,636,A member was added to a local group.
GroupMembership,633,A member was removed from a global group.
GroupMembership,637,A member was removed from a local group.

SecDisGroupMem,656,A member was removed from a security-disabled global group.
SecDisGroupMem,651,A member was removed from a security-disabled local security group.
SecDisGroupMem,666,A member was removed from a security-disabled universal group.
SecDisGroupMem,661,A member was removed from a security-enabled universal group.
SecDisGroupMem,655,A member was added to a security-disabled global group.
SecDisGroupMem,650,A member was added to a security-disabled local security group.
SecDisGroupMem,665,A member was added to a security-disabled universal group.
SecDisGroupMem,660,A member was added to a security-enabled universal group.

SecDisGroup,654,A security-disabled global group was changed.
SecDisGroup,653,A security-disabled global group was created.
SecDisGroup,657,A security-disabled global group was deleted.
SecDisGroup,652,A security-disabled local group was deleted.
SecDisUniGroup,664,A security-disabled universal group was changed.
SecDisUniGroup,663,A security-disabled universal group was created.
SecDisUniGroup,667,A security-disabled universal group was deleted.
SecDisUniGroup,659,A security-enabled universal group was changed.
SecDisUniGroup,658,A security-enabled universal group was created.
SecDisUniGroup,662,A security-enabled universal group was deleted.



@echo off
for /f "tokens=1,2" %%i in ('date /t') do @for /f "tokens=1,2,3 delims=/" %%m in ('echo %%j') do @set LOGDATE=%%o%%n%%m
Set Log=%temp%\%~n0_%logdate%.log
Set LogDir=c:\Logs\Daily
Set MonthlyLogDir=c:\Logs\Monthly
Set Events=AccountManagementEvents.txt
Set UniqueGroups=%Temp%\AccManUnique.txt
Set DCList=%Temp%\DCList.txt
Set NoOfDays=1

Echo Started %Date% %Time%
Echo Started %Date% %Time% > %log%

If Not Exist %Events% (
Echo Error: %Events% could not be found
Echo Error: %Events% could not be found > %log%
Goto End

If Exist %UniqueGroups% Del %UniqueGroups%
If Exist %DCList% Del %DCList%

:: Find the domain controllers for the local domain
dsquery.exe server -o rdn > %DCList%

:: Find the unique groups from the events we are going to process
for /f "tokens=1-3 delims=," %%i in (%Events%) do @Find /i "%%i" %UniqueGroups% 1>nul 2>nul & If errorlevel 1 echo %%i >> %UniqueGroups%

:: For each group of events, call the sub
For /f %%i in (%UniqueGroups%) do Call :ProcessEventGroup %%i

Echo Collating monthly logs
For /f %%p in (%UniqueGroups%) do for /f "tokens=3,4 delims=/ " %%i in ('echo %date%') do if exist %LogDir%\AcctMgmt_%%p_%%j%%i??.txt copy %LogDir%\AcctMgmt_%%p_%%j%%i??.txt %MonthlyLogDir%\AcctMgmt_%%p_%%j%%i.txt /y 1>nul

Echo Finished %Date% %Time%
Echo Finished %Date% %Time% > %log%
Goto End

Set EventList=

:: For Each event in the event file, add the ID if it belongs to this group (calling a sub because for /f doesn't work with repeated inline references to variables
For /f "tokens=1-3 delims=," %%m in (%Events%) do if /i _%1==_%%m Call :AddEvent %%n

Set OutputFile=%logdir%\AcctMgmt_%1_%LogDate%.txt
Echo Dumping %1 events IDs: %EventList% to %OutputFile%

:: For each DC in the domain, dump the events
For /f %%p in (%DCList%) do Echo Processing %%p & dumpel.exe -d %NoOfDays% -e %EventList% -l Security -m Security -s %%p -c >> %OutputFile%
Goto End

:: Concatenate the latest ID
Set EventList=%EventList% %1
Goto End


Information on the 'Manage Auditing and Security Log' Right:

Wayne's World of IT (WWoIT), Copyright 2008 Wayne Martin.

Read more!

Friday, April 4, 2008

VMware cluster/Virtual Center Statistics

This post provides a set of SQL commands to extract data from a VMware Virtual Center database. The statistics provided include VMs and their config LUNs, VM disk total and free space (includes RDM), VMFS volumes used and free space, DRS migrations that have occurred, manual migrations that have occurred, Virtual Machine information, and the current distribution of VMs across cluster nodes

Set the date suffix:

  • for /f "tokens=1-8 delims=/:. " %i in ('echo %date%') do set DateFlat=%l%k%j

VMs and their config LUNs

  1. sqlcmd -S %server% -d %database% -W -s "," -Q "select VMS.Name as 'VM Name', TDS.Name as 'Config Datastore', SubString(ConfigFileName, CharIndex('/', ConfigFileName, Len('sanfs://vmfs'))+1, 255) as 'Config Path' from vpxv_vms VMS inner join vpx_datastore TDS on TDS.Storage_URL = SubString(VMS.ConfigFileName, 1, CharIndex('/', VMS.ConfigFileName, Len('sanfs://vmfs'))) Order By 'Config Datastore'" > VMConfigLuns_%DateFlat%.csv

VM disk total and free space (includes RDM)

  1. sqlcmd -S %server% -d %database% -W -s "," -Q "select VM.Name, Cast(Round(Sum(Cast(VDISK.CAPACITY as numeric))/1024/1024/1024, 1) as int) as 'Total Disk', Cast(Round(Sum(Cast(VDISK.FREE_SPACE as numeric))/1024/1024/1024, 1) as int) as 'Free Disk' from dbo.VPX_GUEST_DISK VDISK inner join VPXV_VMS VM on VDISK.VM_ID = VM.VMID group by VM.Name compute sum(Cast(Round(Sum(Cast(VDISK.CAPACITY as numeric))/1024/1024/1024, 1) as int))" > VMDiskInfo_%DateFlat%.csv

VMFS volumes used and free space

  1. sqlcmd -S %server% -d %database% -W -s "," -Q "select Name, Cast(Round(Cast(Capacity as numeric)/1024/1024/1024, -1) as int) as 'Total Space', Cast(Round(Cast(Free_Space as numeric)/1024/1024/1024, -1) as int) as 'Free Space', Type from vpx_Datastore compute sum(Cast(Round(Cast(Capacity as numeric)/1024/1024/1024, -1) as int))" > VMFS_volumes_%DateFlat%.csv

DRS migrations that have occurred

  1. sqlcmd -S %server% -d %database% -W -s "," -Q "select DateAdd(Hour, 10, Create_Time) as 'Relocation Finished', VM_Name, Host_Name as 'Host Destination', (select Host_Name from dbo.VPX_EVENT Where Chain_ID = EVTDEST.Chain_ID and event_type = 'vim.event.VMBeingHotMigratedEvent') as 'Host Source', ComputeResource_Name, DataCenter_Name from dbo.VPX_EVENT EVTDEST where event_type = 'vim.event.DrsVmMigratedEvent'" > VMDRSMigrations_%DateFlat%.csv

Manual migrations that have occurred

  1. sqlcmd -S %server% -d %database% -W -s "," -Q "select DateAdd(Hour, 10, Create_Time) as 'Relocation Start', UserName, VM_Name, Host_Name as 'Host Source', (select Host_Name from dbo.VPX_EVENT Where Chain_ID = EVTDEST.Chain_ID and event_type = 'vim.event.VmRelocatedEvent') as 'Host Destination', ComputeResource_Name, DataCenter_Name from dbo.VPX_EVENT EVTDEST where event_type = 'vim.event.VmBeingRelocatedEvent'" > VMManualMigrations_%DateFlat%.csv

Virtual Machine information

  1. sqlcmd -S %server% -d %database% -W -s "," -Q "select ENT.Name as 'Name', Lower(VM.DNS_Name) as 'DNS Name', VH.DNS_NAME as 'Host', Guest_OS as 'OS', Mem_Size_MB as 'Mem', Num_VCPU as 'CPU', Num_NIC as 'NIC', VM.IP_Address as 'IP', NET.MAC_Address as 'MAC Address', VM.FILE_Name as 'VMX location' from vpx_vm VM inner join VPX_GUEST_NET_ADAPTER NET on VM.ID = NET.VM_ID inner join VPX_ENTITY ENT on VM.ID = ENT.ID inner join vpx_host VH on VM.HOST_ID = VH.ID order by ENT.Name" > VMInfo_%DateFlat%.csv

Current distribution of VMs across cluster nodes

  1. sqlcmd -S %server% -d %database% -W -s "," -Q "Select VH.DNS_NAME as 'Host', count(VM.HOST_ID) from vpx_vm VM inner join vpx_host VH on VM.HOST_ID = VH.ID group by VM.HOST_ID, VH.DNS_NAME order by 'Host'" > VMDistribution_%DateFlat%.csv

  1. These commands were created for use with a SQL 2005 database and the sqlcmd.exe utility. You can also just manually run the query string in SQL enterprise manager/management studio if you prefer.
  2. The first command below will set a variable used to provide a date suffix appended to the output of each command, ie YYYYMMDD - 20080404
  3. SQL queries using the DateAdd() function are adding GMT+10 (my local timezone), change the value to your GMT offset to modify the UTC times recorded in the database.

Wayne's World of IT (WWoIT), Copyright 2008 Wayne Martin.

Read more!

Thursday, April 3, 2008

Running scheduled tasks as a non-administrator

This post contains information on creating and running a scheduled task on a Windows Server 2003 system with a non-administrative account, following the Principle of Least Privilege methodology.

By default, running a task as a non-administrative poses several problems, but overcoming these problems can help reduce the attack surface and adhere to the Principles of Least Privilege.

To ensure the task will run successfully:

  1. Create the user account, either locally or in a domain.
  2. Ensure the user is only a member of the users group and any groups required for local and remote resource access (eg. Data fileshares, application access)
  3. Ensure the user has local access to the files required to run the command, eg. By default cscript.exe has ACLs requiring either administrative or system/service/batch/interactive access, but the account or NT Authority\Batch will also need access to the script file, and possibly temp and/or log directories. Note that this should be recorded in security templates, allowing for automatic reapplication of security.
  4. Create the task and set the task to be run as the newly created account. Note that doing this will automatically grant the 'log on as a batch job' (SeBatchLogonRight) right to allow the task to start.
  5. Ensure that the user account has at a minimum, Read, Execute and Write permissions to the schedule .job file
  6. Run the task, checking the Scheduled Task log if a result other than success is returned (0x0). Note that if account auditing is enabled, the Security event log should show a Logon Event 528, of Type 4 (LOGON32_LOGON_BATCH) for the newly created account.

  • While developing this process, the Service principal (S-1-5-6) was tested, which was unsuccessful, as the scheduled task is started as a batch job, rather than the schedule (or any other) service.
  • IMPORTANT: Ensure that the shell executable (cmd.exe, cscript.exe, powershell.exe etc.) is accessible to the 'NT Authority\Batch' security principle eg. when running the command cscript d:\admin\local\scripts\test.wsf, the 'NT Authority\Batch' security principle must have access to cscript.exe as well as the .wsf script file. Access can also be granted to an account/group rather than batch if this is too permissive.
  • When a task starts, the command is created under the security context of the specified user, and the user token has the Batch SID (S-1-5-3) attached. This is a well-known security principal, commonly displayed as 'NT Authority\Batch'


Well-known Security Identifiers:

Security Identifier Types:


Audit Logon Events:

Wayne's World of IT (WWoIT), Copyright 2008 Wayne Martin.

Read more!

Tuesday, April 1, 2008

Audit Windows 2003 print server usage

This post provides information on a solution to provide audit logs and summary information on printer usage on a Windows print server. This will provide daily and monthly printer event logs, and summary results based on one or more log files.


  • A scheduled task that runs every day to collect and process the logs, it doesn’t have to be on the print server.
  • 'Log Spooler Information Events' on the printer spooler in question, which will write Event ID 10 entries every time someone prints through the spooler
  • A system event log big enough to capture at least one day's logs

Create a batch file that contains the following commands. Note that you will need to modify the variables or to reference paths as appropriate, eg for dumpel.exe and the VBScript, and the print/log dir.

Set PrintDir=c:\Print
Set LogDir=C:\logs
for /f "tokens=1-8 delims=/:. " %%i in ('echo %date%') do Set DateFlat=%%l%%k%%j
dumpel -s \\%print_server% -l System -e 10 -m Print -d 1 >> %logDir%\%server%_jobs_%DateFlat%.csv
for /f "tokens=3,4 delims=/ " %%i in ('echo %date%') do copy %server%_jobs_%%j%%i??.csv %PrintDir%\PrintJobs_%%j%%i.csv /y
cscript ProcessPrinterLogs.wsf /f:%LogDir%

If you create a scheduled task to run this batch file every day at the same time, these commands will:

  1. Dump the event logs for the past day to a daily log file with YYYYMMDD suffix.
  2. Collate the daily log files into a monthly log file, by appending each daily file. For each day in a month, this command will overwrite the previous monthly log, until it runs on the last day of the month.
  3. The script processes the dumpel log entries, providing different views in the form of per-printer, per-user, per-day totals of jobs/pages/bytes (useful for graphics), as well as summary totals of the information.

This has the following advantages:

  • It’s simple and not very intensive. A SQL database with a recurring DTS job to import the logs and then using SQL Reporting Services would be a lot prettier, but really not that much more functional or useful.
  • You will have a permanent set of log files, one for each month that you can store for historical purposes, while purging the daily log file directory every so often.

The ProcessPrinterLogs.vbs script that does all the work is listed below. To run:
cscript ProcessPrinterLogs.vbs /f:%logDir%

Const ForReading = 1, ForWriting = 2, ForAppending = 8

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objShell = CreateObject("WScript.Shell")


Sub Main()
    If WScript.Arguments.Named.Exists("f") Then
        sSource = Wscript.Arguments.Named("f")
        Wscript.Echo "Source file or directory must be supplied"
    End If

    If Wscript.Arguments.Named.Exists("o") Then
        sOutputFile = Wscript.Arguments.Named("o")
        dNow = Now    
        dLogDate = DatePart("yyyy", dNow) 
        dLogDate = dLogDate & String(2 - Len(DatePart("m", dNow)),"0") & DatePart("m", dNow)
        dLogDate = dLogDate & String(2 - Len(DatePart("d", dNow)),"0") & DatePart("d", dNow)    
        sOutputFile = objShell.ExpandEnvironmentStrings("%Temp%")
        sOutputFile = sOutputFile & "\" & Left(WScript.ScriptName, InStrRev(WScript.ScriptName,".vbs")-1) & "_" & dLogDate & ".csv"
    End If

    wscript.echo "Input file/dir: '" & sSource & "'"
    wscript.echo "Output file: '" & sOutputFile & "'"

    If objFSO.FileExists(sSource) Then 
        sFileSet = sSource                                        ' Process a single file
        wscript.echo "Single file specified - " & sFileSet
    ElseIf objFSO.FolderExists(sSource) Then
        wscript.echo "Source specified was a directory, reading files from '" & sSource & "'"
        sFileSet = ""
        Set oFolder = objFSO.GetFolder(sSource)                                ' Get the folder
        Set oFiles = oFolder.Files
        For Each oFile in oFiles                                    ' For each file
            sFileset = sFileset & vbCRLF & oFile.Path                         ' Append to the fileset
        If Len(sFileSet) > Len(vbCRLF) Then sFileSet = Right(sFileSet, Len(sFileSet) - Len(vbCRLF))    ' Trim the leading CRLF
    End If

    Set dPrinters  = CreateObject("Scripting.Dictionary")                            ' Create the dictionary objects
    Set dusers = CreateObject("Scripting.Dictionary")
    Set dDates = CreateObject("Scripting.Dictionary")
    Set dJobs = CreateObject("Scripting.Dictionary")

    For Each sFile in Split(sFileset, vbCRLF)                                ' For Each file
        wscript.echo "Processing '" & sFile & "'"
        sBuffer = ""
           Set objTextStream = objFSO.OpenTextFile(sFile, ForReading)      
        sBuffer = objTextStream.ReadAll

        For Each sLine in Split(sBuffer, vbCRLF)                            ' For each line in this file
            Call ProcessLogEntry(sLine, dPrinters, dUsers, dDates, dJobs)                ' Process the log entry

    Call ProduceOutput(sOutput, dPrinters, dUsers, dDates, dJobs)                        ' Produce the output
    Set objTextStream = objFSO.OpenTextFile(sOutputFile, ForWriting, True)
    objTextStream.Write sOutput
    wscript.echo "Output saved to '" & sOutputFile & "', " & Len(sOutput) & " characters."

End Sub

Function ProduceOutput(ByRef sOutput, ByRef dPrinters, ByRef dUsers, ByRef dDates, ByRef dJobs)
    Dim strPrinter, strPort, dtmDate, strUser, strserver, strDocumentName, intSize, intPages, strInformation, strTotal
    Dim strUserTotal, strPrinterTotal, strDateTotal, strJobTotal, aJobTotal

    sOutput = ""
    For Each strPrinter in dPrinters.Keys        
        sOutput = sOutput & vbCRLF & strPrinter & "," & dPrinters.Item(strPrinter)

    sOutput = sOutput & vbCRLF
    For Each strUser in dUsers.Keys
        sOutput = sOutput & vbCRLF & strUser & "," & dUsers.Item(strUser)

    sOutput = sOutput & vbCRLF
    For Each dtmDate in dDates.Keys
        sOutput = sOutput & vbCRLF & dtmDate & "," & dDates.Item(dtmDate)

    sOutput = sOutput & vbCRLF
    For Each strTotal in dJobs.Keys
        strJobTotal = dJobs.Item(strTotal)
        aJobTotal = Split(strJobTotal, ",")
        sOutput = sOutput & vbCRLF & "Total Jobs," & aJobTotal(0)
        sOutput = sOutput & vbCRLF & "Total Pages," & aJobTotal(1)
        sOutput = sOutput & vbCRLF & "Total Size (MB)," & aJobTotal(2)

    sOutput = sOutput & vbCRLF
    strUserTotal = UBound(dUsers.Keys)+1
    strPrinterTotal = UBound(dPrinters.Keys)+1
    strDateTotal = UBound(dDates.Keys)+1
    sOutput = sOutput & vbCRLF & "Printers," & strPrinterTotal 
    sOutput = sOutput & vbCRLF & "Users," & strUserTotal 
    sOutput = sOutput & vbCRLF & "Days," & strDateTotal 

    aJobTotal = Split(strJobTotal, ",")
    sOutput = sOutput & vbCRLF

    sOutput = sOutput & vbCRLF & "Average jobs/person," & CInt(aJobTotal(0)/strUserTotal)
    sOutput = sOutput & vbCRLF & "Average pages/person," & CInt(aJobTotal(1)/strUserTotal)
    sOutput = sOutput & vbCRLF & "Average pages/person/day," & CInt(CInt(aJobTotal(1)/strUserTotal) / strDateTotal)
    sOutput = sOutput & vbCRLF & "Average pages/minute," & CInt(aJobTotal(1) / (strDateTotal * 8 * 60))

End Function

Function ProcessLogEntry(ByRef sLine, ByRef dPrinters, ByRef dUsers, ByRef dDates, ByRef dJobs)
    Dim strPrinter, strPort, dtmDate, strUser, strserver, strDocumentName, intSize, intPages, strInformation 
    Dim aPrintJob, intOffset, strTemp, aTemp

    aPrintJob = Split(sLine, vbTAB)

    If UBound(aPrintJob) = 9 Then
        dtmDate = aPrintJob(0) ' & " " & aPrintJob(1)
        aTemp = Split(dtmDate, "/")
        dtmDate = Right("00" & Trim(aTemp(1)), 2) & "/" & Right("00" & Trim(aTemp(0)), 2) & "/" & aTemp(2)        ' Trim, pad and switch to dd/mm/yyyy instead of mm/dd/yyyy
        strServer = aPrintJob(8)

        strInformation = Trim(aPrintJob(9))
        strInformation = Right(strInformation, Len(strInformation) - InStr(strInformation, " "))    ' Remove the job ID
        intOffset = InStrRev(strInformation, " ")
        intPages = Right(strInformation, Len(strInformation) - intOffset)        ' Extract the number of pages from the end
        strInformation = Left(strInformation, intOffset-1)                ' Trim the string
        intOffset = InStrRev(strInformation, " ")
        intSize = Right(strInformation, Len(strInformation) - intOffset)        ' Extract the number of bytes from the end
        strInformation = Left(strInformation, intOffset-1)                ' Trim the string    
        intOffset = InStrRev(strInformation, " ")
        strPort = Right(strInformation, Len(strInformation) - intOffset)        ' Extract the port from the end
        strInformation = Left(strInformation, intOffset-1)                ' Trim the string    
        intOffset = InStrRev(strInformation, " ")
        strPrinter = Right(strInformation, Len(strInformation) - intOffset)        ' Extract the printer from the end
        strInformation = Left(strInformation, intOffset-1)                ' Trim the string    
        intOffset = InStrRev(strInformation, " ")
        strUser = Right(strInformation, Len(strInformation) - intOffset)        ' Extract the user from the end
        strInformation = Left(strInformation, intOffset-1)                ' Trim the string    
        strDocumentName = strInformation

        If dPrinters.Exists(strPrinter) Then                         ' Does this printer already exist in the dictionary?
            aTemp = Split(dPrinters.Item(strPrinter), ",")                ' Find the existing printer job/page count
            aTemp(0) = aTemp(0) + 1                            ' Increment the job count
            aTemp(1) = aTemp(1) + CInt(intPages)                    ' Add to the page count
            aTemp(2) = aTemp(2) + CInt(intSize/1024/1024)                ' Add to the byte count
            dPrinters.Item(strPrinter) = Join(aTemp, ",")                ' Update the dictionary
            aTemp = Array(1, intPages, CInt(intsize /1024/1024))            ' Start the job/page count
            dPrinters.Add strPrinter, Join(aTemp, ",")                ' Create this item
        End If
        If dUsers.Exists(strUser) Then                             ' Does this user already exist in the dictionary?
            aTemp = Split(dUsers.Item(strUser), ",")                ' Find the existing user job/page count
            aTemp(0) = aTemp(0) + 1                            ' Increment the job count
            aTemp(1) = aTemp(1) + CInt(intPages)                    ' Add to the page count
            aTemp(2) = aTemp(2) + CInt(intSize/1024/1024)                ' Add to the byte count
            dUsers.Item(strUser) = Join(aTemp, ",")                    ' Update the dictionary
            aTemp = Array(1, intPages, CInt(intsize /1024/1024))            ' Start the job/page count
            dUsers.Add strUser, Join(aTemp, ",")                    ' Create this item
        End If

        If dDates.Exists(dtmDate) Then                             ' Does this date already exist in the dictionary?
            aTemp = Split(dDates.Item(dtmDate), ",")                ' Find the existing date job/page count
            aTemp(0) = aTemp(0) + 1                            ' Increment the job count
            aTemp(1) = aTemp(1) + CInt(intPages)                    ' Add to the page count
            aTemp(2) = aTemp(2) + CInt(intSize/1024/1024)                ' Add to the byte count
            dDates.Item(dtmDate) = Join(aTemp, ",")                    ' Update the dictionary
            aTemp = Array(1, intPages, CInt(intsize /1024/1024))            ' Start the job/page count
            dDates.Add dtmDate, Join(aTemp, ",")                    ' Create this item
        End If

        If dJobs.Exists(JOB_TOTAL) Then                         ' Does the total already exist in the dictionary?
            aTemp = Split(dJobs.Item(JOB_TOTAL), ",")                ' Find the existing total counts
            aTemp(0) = aTemp(0) + 1                            ' Increment the job count
            aTemp(1) = aTemp(1) + CInt(intPages)                    ' Add to the page count
            aTemp(2) = aTemp(2) + CInt(intSize/1024/1024)                ' Add to the byte count
            dJobs.Item(JOB_TOTAL) = Join(aTemp, ",")                ' Update the dictionary
            aTemp = Array(1, intPages, CInt(intsize /1024/1024))            ' Start the job/page count
            dJobs.Add JOB_TOTAL, Join(aTemp, ",")                    ' Create this item
        End If
        wscript.echo "skipped '" & sLine & "'"
    End If
End Function

* Please don’t print this post :) *

Wayne's World of IT (WWoIT), Copyright 2008 Wayne Martin.

Read more!

Active Directory Diagnostics

The text in this post is a batch file for Active Directory diagnostics. Simply set the variables for DCs, and it will collect information about your Active Directory environment. This is useful for troubleshooting, benchmarking, analysis and history. All operations are read-only.

:: Changes:
:: 21/09/2006, Initial version
:: 18/02/2008, Updated commands to be more generic and use variables for DC/DN/FQDN commands
:: 19/02/2008, Updated 'Subnet Information' to return a mapping of subnet to site.
:: 20/02/2008, Updated 'Find all connection objects' to provide more information
:: Author:
:: Wayne Martin
:: Use:
:: Perform various diagnostic commands against a domain and capture the output for analysis and history
:: Notes:
:: Most commands will work against a 2000 domain, but some are targeted at 2003-only functionality
:: Requires:
:: acldiag.exe
:: AdFind.exe
:: adrestore.exe
:: dcdiag.exe
:: dnscmd.exe
:: dsastat.exe
:: gpotool.exe
:: ldifde.exe
:: netdiag.exe
:: nltest.exe
:: psexec.exe
:: Psinfo.exe
:: repadmin.exe
:: setspn.exe


Set Server=%DC1%
Set SecondDc=%DC2%
Set ThirdDC=%DC3%
Set TimeServer=%TIME%
Set DomainDN=DC=domain,DC=com

:: Make the directory for the output
If not Exist .\Diag md Diag

:: FSMO Roles
ntdsutil roles Connections "Connect to server %Server%" Quit "select Operation Target" "List roles for conn server" Quit Quit Quit >>"Diag\FSMO_Roles_%Server%.txt

:: Domain Controllers
Nltest /dclist:%userdnsdomain% >>"Diag\Domain_Controllers_%computername%.txt

:: Domain Controller IP Configuration
for /f %%i in ('dsquery server -domain %userdnsdomain% -o rdn') do psexec \\%%i /s ipconfig /all >>"Diag\Domain_Controller_IP_Configuration_%%i.txt

:: Domain Controller SystemInfo
for /f %%i in ('dsquery server -domain %userdnsdomain% -o rdn') do systeminfo /s %%i >>"Diag\Domain_Controller_SystemInfo_%%i.txt

:: AD Database disk usage
for /f %%i in ('dsquery server -domain %userdnsdomain% -o rdn') do dir \\%%i\admin$\ntds >>"Diag\AD_Database_disk_usage_%%i.txt

:: Global Catalog Servers from DNS
dnscmd %Server% /enumrecords %userdnsdomain% _tcp find /i "3268" >>"Diag\Global_Catalog_Servers_from_DNS_%Server.txt

:: Global Catalog Servers from AD
dsquery * "CN=Configuration,%DomainDN%" -s %Server% -filter "(&(objectCategory=nTDSDSA)(options:1.2.840.113556.1.4.803:=1))" >>"Diag\Global_Catalog_Servers_from_AD_%Server%.txt

:: DNS Information
for /f %%i in ('dsquery server -domain %userdnsdomain% -o rdn') do dnscmd %%i /info >>"Diag\DNS_Information_%%i.txt

:: DNS Zone Detailed information
dnscmd %Server% /zoneinfo %userdnsdomain% >>"Diag\DNS_Zone_Detailed_information_%server%.txt

:: Garbage Collection and tombstone
dsquery * "cn=Directory Service,cn=Windows NT,cn=Services,cn=Configuration,%DomainDN%" -s %Server% -attr garbageCollPeriod tombstoneLifetime >>"Diag\Garbage_Collection_and_tombstone_%server%.txt

:: Group Policy Verification Tool
gpotool.exe /checkacl /verbose >>"Diag\Group_Policy_Verification_Tool.txt

:: AD OU membership
dsquery computer -s %Server% -limit 0 1>>"Diag\AD_OU_membership_%server%.txt

:: AD OU membership
dsquery user -s %Server% -limit 0 1>>"Diag\AD_OU_membership_%server%.txt

:: List Service Principal Names
for /f %%i in ('dsquery server -domain %userdnsdomain% -o rdn') do setspn -L %%i >>"Diag\List_Service_Principal_Names_%%i.txt

:: Compare DC Replica Object Count
dsastat -s:%server%;%SecondDC%;%ThirdDC% -p:999 >>"Diag\Compare_DC_Replica_Object_Count.txt

:: Check AD ACLs
acldiag %DomainDN% >>"Diag\Check_AD_ACLs.txt

:: NTFRS Replica Sets
for /f %%i in ('dsquery server -domain %userdnsdomain% -o rdn') do ntfrsutl sets %%i >>"Diag\NTFRS_Replica_Sets_%%i.txt

:: NTFRS DS View
for /f %%i in ('dsquery server -domain %userdnsdomain% -o rdn') do ntfrsutl ds %%i >>"Diag\NTFRS_DS_View_%%i.txt

:: Domain Controllers per site
Dsquery * "CN=Sites,CN=Configuration,%DomainDN%" -s %server% -filter (objectCategory=Server) >>"Diag\Domain_Controllers_per_site_%%i.txt

:: DNS Zones in AD
for /f %%i in ('dsquery server -o rdn') do Dsquery * -s %%i domainroot -filter (objectCategory=dnsZone) >>"Diag\DNS_Zones_in_AD_%%i.txt

:: Enumerate DNS Server Zones
for /f %%i in ('dsquery server -o rdn') do dnscmd %%i /enumzones >>"Diag\Enumerate_DNS_Server_Zones_%%i.txt

:: Subnet information
dsquery * "CN=Subnets,CN=Sites,CN=Configuration,%DomainDN%" -s %server% -attr cn siteObject description location >>"Diag\Subnet_information_%server%.txt
::Dsquery subnet -s %server% >>"Diag\Subnet_information_%server%.txt

:: List Organisational Units
Dsquery OU -s %server% >>"Diag\List_Organisational_Units_%server%.txt

:: ACL on all OUs
For /f "delims=" %%i in ('dsquery OU -s -s %server%') do acldiag %%i >>"Diag\ACL_on_all_OUs.txt

:: Domain Trusts
nltest /domain_trusts /v /server:%server% >>"Diag\Domain_Trusts_%server%.txt

:: Print DNS Zones
dnscmd %Server% /zoneprint %DomainFQDN% >>"Diag\Print_DNS_Zones_%server%.txt

:: AD Subnet and Site Information
dsquery * "CN=Subnets,CN=Sites,CN=Configuration,%DomainDN%" -s %server% -attr cn siteObject description location >>"Diag\AD_Subnet_and_Site_Information_%server%.txt

:: AD Site Information
dsquery * "CN=Sites,CN=Configuration,%DomainDN%" -s %server% -attr cn description location -filter (objectClass=site) >>"Diag\AD_Site_Information_%server%.txt

:: Printer Queue Objects in AD
dsquery * domainroot -filter "(objectCategory=printQueue)" -s %server% -limit 0 1>>"Diag\Printer_Queue_Objects_in_AD_%server%.txt

:: Group Membership with user details
dsget group groupDN -members dsget user -samid -fn -mi -ln -display -empid -desc -office -tel -email -title -dept -mgr >>"Diag\Group_Membership_with_user_details_%server%.txt

:: Site Links and Cost
dsquery * "CN=Sites,CN=Configuration,%DomainDN%" -s %server% -attr cn cost description replInterval siteList -filter (objectClass=siteLink) >>"Diag\Site_Links_and_Cost_%server%.txt

:: Check time against Domain
w32tm /monitor /computers:%server%,%SecondDC%,%ThirdDC%,%TimeServer% >>"Diag\Check_time_against_Domain.txt

:: Domain Controller Diagnostics
for %%i in (%server% %SecondDC% %ThirdDC%) do dcdiag /s:%%i /v /e /c >>"Diag\Domain_Controller_Diagnostics_%%i.txt

:: Domain Replication Bridgeheads
repadmin /bridgeheads >>"Diag\Domain_Replication_Bridgeheads.txt

:: Replication Failures from KCC
repadmin /failcache >>"Diag\Replication_Failures_from_KCC.txt

:: Inter-site Topology servers per site
Repadmin /istg * /verbose >>"Diag\Inter-site_Topology_servers_per_site.txt

:: Replication latency
repadmin /latency /verbose >>"Diag\Replication_latency.txt

:: Queued replication requests
repadmin /queue * >>"Diag\Queued_replication_requests.txt

:: Show connections for a DC
repadmin /showconn * >>"Diag\Show_connections_for_a_DC.txt

:: Replication summary
Repadmin /replsummary >>"Diag\Replication_summary.txt

:: Show replication partners
repadmin /showrepl * /all >>"Diag\Show_replication_partners.txt

:: All DCs in the forest
repadmin /viewlist * >>"Diag\All_DCs_in_the_forest.txt

:: ISTG from AD attributes
dsquery * "CN=NTDS Site Settings,CN=CLB,CN=Sites,CN=Configuration,%DomainDN%" -s %server% -attr interSiteTopologyGenerator >>"Diag\ISTG_from_AD_attributes_%server%.txt

:: Return the object if KCC Intra/Inter site is disabled for each site
Dsquery site dsquery * -attr * -s %server% -filter "((Options:1.2.840.113556.1.4.803:=1)(Options:1.2.840.113556.1.4.803:=16))" >> "Diag\Return_the_object_if_KCC_Intra-Inter_site_is_disabled_for_each_site_%server%.txt"

:: Find all connection objects
::dsquery * forestRoot -s %server% -filter (objectCategory=nTDSConnection) -attr distinguishedName fromServer whenCreated displayName >>"Diag\Find_all_connection_objects_%server%.txt
dsquery * "CN=Servers,CN=%SITECODE%,CN=Sites,CN=Configuration,%DomainDN%" -attr fromServer cn >>"Diag\Find_all_connection_objects_%server%.txt

:: Find all connection schedules
adfind -b "CN=Configuration,%DomainDN%" -f "objectcategory=ntdsConnection" cn Schedule -csv >>"Diag\Find_all_connection_schedules_%server%.txt

:: Software Information for each server
for /f %%i in ('dsquery server -domain %userdnsdomain% -o rdn') do psinfo \\%%i > ServerInfo_%%i.txt & filever \\%%i\admin$\explorer.exe \\%%i\admin$\system32\vbscript.dll \\%%i\admin$\system32\kernel32.dll \\%%i\admin$\system32\wbem\winmgmt.exe \\%%i\admin$\system32\oleaut32.dll >>"Diag\Software_Information_for_each_server_%%i.txt

:: Check Terminal Services Delete Temp on Exit flag
For /f %%i in ('dsquery server -domain %userdnsdomain% -o rdn') do Reg query "\\%%i\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Terminal Server" /v DeleteTempDirsOnExit >>"Diag\Check_Terminal_Services_Delete_Temp_on_Exit_flag_%%i.txt

:: Information on existing GPO’s
dsquery * "CN=Policies,CN=System,%DomainDN%" -s %server% -filter "(objectCategory=groupPolicyContainer)" -attr displayName cn whenCreated gPCFileSysPath >>"Diag\Information_on_existing_GPOs_%server%.txt

:: Domain Controller Netlogon entries
for /f %%i in ('dsquery server /o rdn') do echo %%i & reg query \\%%i\hklm\system\currentcontrolset\services\netlogon\parameters >>"Diag\Domain_Controller_Netlogon_entries_%%i.txt

:: Find empty groups
dsquery * -s %server% -filter "&(objectCategory=group)(!member=*)" -limit 0 -attr whenCreated whenChanged groupType sAMAccountName distinguishedName memberOf >>"Diag\Find_empty_groups.txt

:: Disk statistics, including the number of files on the filesystem
for %%i in (%server% %SecondDC% %ThirdDC%) do psexec \\%%i chkdsk C: /i /c >>"Diag\CheckDisk_%%i.txt
for %%i in (%server% %SecondDC% %ThirdDC%) do psexec \\%%i defrag C: -a -v >>"Diag\CheckDisk_%%i.txt

:: Query IIS web sites
for /f %%i in ('dsquery server /o rdn') do iisweb /s %%i /query "Default Web Site" >>"Diag\IIS_Default_Web_Sites_%%i.txt

:: Forest/Domain Functional Levels
ldifde -s %server% -d cn=partitions,cn=configuration,%DomainDN% -r "((systemFlags=3)(systemFlags=-2147483648))" -l msds-behavior-version,dnsroot,ntmixeddomain,NetBIOSName -p subtree -f "Diag\Query_IIS_web_sites_%server%.txt"

:: Forest/Domain Functional Levels
dsquery * cn=partitions,cn=configuration,%DomainDN% -s %server% -filter "((systemFlags=3)(systemFlags=-2147483648))" -attr msDS-Behavior-Version Name dnsroot ntmixeddomain NetBIOSName >>"Diag\Forest-Domain_Functional_Levels_%server%.txt

:: Lookup SRV records from DNS
nslookup -type=srv _ldap._tcp.dc._msdcs.%DomainFQDN% %server% >>"Diag\Lookup_SRV_records_from_DNS_%server%.txt

:: Find when the AD was installed
dsquery * cn=configuration,%DomainDN% -s %server% -attr whencreated -scope base >>"Diag\Lookup_SRV_records_from_DNS_%server%.txt

:: Find a DC for each trusted domain
for /f "skip=1" %%i in ('"dsquery * CN=System,%DomainDN% -filter (objectClass=trustedDomain) -attr trustPartner"') do nltest /dsgetdc:%%i >> "Diag\Find_a_DC_for_each_trusted_domain_%server%.txt"

:: Verify SMB connectivity to the admin share on DCs
for /f %%i in ('dsquery server -o rdn') do @if not exist \\%%i\admin$ (echo Could not access %%i\admin$) Else (Echo %%i\admin$ exists) >> Verify_SMB_connectivity_to_DCs_%%i.txt

Wayne's World of IT (WWoIT), Copyright 2008 Wayne Martin.

Read more!

All Posts

printQueue AD objects for 2003 ClusterVirtualCenter Physical to VirtualVirtual 2003 MSCS Cluster in ESX VI3
Finding duplicate DNS recordsCommand-line automation – Echo and macrosCommand-line automation – set
Command-line automation - errorlevels and ifCommand-line automation - find and findstrBuilding blocks of command-line automation - FOR
Useful PowerShell command-line operationsMSCS 2003 Cluster Virtual Server ComponentsServer-side process for simple file access
OpsMgr 2007 performance script - VMware datastores...Enumerating URLs in Internet ExplorerNTLM Trusts between 2003 and NT4
2003 Servers with Hibernation enabledReading Shortcuts with PowerShell and VBSModifying DLL Resources
Automatically mapping printersSimple string encryption with PowerShellUseful NTFS and security command-line operations
Useful Windows Printer command-line operationsUseful Windows MSCS Cluster command-line operation...Useful VMware ESX and VC command-line operations
Useful general command-line operationsUseful DNS, DHCP and WINS command-line operationsUseful Active Directory command-line operations
Useful command-linesCreating secedit templates with PowerShellFixing Permissions with NTFS intra-volume moves
Converting filetime with vbs and PowerShellDifference between bat and cmdReplica Domain for Authentication
Troubleshooting Windows PrintingRenaming a user account in ADOpsMgr 2007 Reports - Sorting, Filtering, Charting...
WMIC XSL CSV output formattingEnumerating File Server ResourcesWMIC Custom Alias and Format
AD site discoveryPassing Parameters between OpsMgr and SSRSAnalyzing Windows Kernel Dumps
Process list with command-line argumentsOpsMgr 2007 Customized Reporting - SQL QueriesPreventing accidental NTFS data moves
FSRM and NTFS Quotas in 2003 R2PowerShell Deleting NTFS Alternate Data StreamsNTFS links - reparse, symbolic, hard, junction
IE Warnings when files are executedPowerShell Low-level keyboard hookCross-forest authentication and GP processing
Deleting Invalid SMS 2003 Distribution PointsCross-forest authentication and site synchronizati...Determining AD attribute replication
AD Security vs Distribution GroupsTroubleshooting cross-forest trust secure channels...RIS cross-domain access
Large SMS Web Reports return Error 500Troubleshooting SMS 2003 MP and SLPRemotely determine physical memory
VMware SDK with PowershellSpinning Excel Pie ChartPoke-Info PowerShell script
Reading web content with PowerShellAutomated Cluster File Security and PurgingManaging printers at the command-line
File System Filters and minifiltersOpsMgr 2007 SSRS Reports using SQL 2005 XMLAccess Based Enumeration in 2003 and MSCS
Find VM snapshots in ESX/VCComparing MSCS/VMware/DFS File & PrintModifying Exchange mailbox permissions
Nested 'for /f' catch-allPowerShell FindFirstFileW bypassing MAX_PATHRunning PowerSell Scripts from ASP.Net
Binary <-> Hex String files with PowershellOpsMgr 2007 Current Performance InstancesImpersonating a user without passwords
Running a process in the secure winlogon desktopShadow an XP Terminal Services sessionFind where a user is logged on from
Active Directory _msdcs DNS zonesUnlocking XP/2003 without passwords2003 Cluster-enabled scheduled tasks
Purging aged files from the filesystemFinding customised ADM templates in ADDomain local security groups for cross-forest secu...
Account Management eventlog auditingVMware cluster/Virtual Center StatisticsRunning scheduled tasks as a non-administrator
Audit Windows 2003 print server usageActive Directory DiagnosticsViewing NTFS information with nfi and diskedit
Performance Tuning for 2003 File ServersChecking ESX/VC VMs for snapshotsShowing non-persistent devices in device manager
Implementing an MSCS 2003 server clusterFinding users on a subnetWMI filter for subnet filtered Group Policy
Testing DNS records for scavengingRefreshing Computer Account AD Group MembershipTesting Network Ports from Windows
Using Recovery Console with RISPAE Boot.ini Switch for DEP or 4GB+ memoryUsing 32-bit COM objects on x64 platforms
Active Directory Organizational Unit (OU) DesignTroubleshooting computer accounts in an Active Dir...260+ character MAX_PATH limitations in filenames
Create or modify a security template for NTFS perm...Find where a user is connecting from through WMISDDL syntax in secedit security templates

About Me

I’ve worked in IT for over 20 years, and I know just about enough to realise that I don’t know very much.