Script to find outdated computer objects in Active Directory

Computers have accounts in Active Directory and log on just as user accounts do. The “user name” of a computer is its name with a dollar sign appended, e.g: MYPC1$. The password is set by the machine when it is joined to the domain and changed every 30 days by the machine. Just as with user accounts, computer accounts gets left in the domain when the machine is decomissioned or reinstalled under another name etc. This leaves our directory service full of outdated user and computer object. (Outdated in this context means “has not logged on to the domain for X days). This in turn makes it hard to know how many computers are actually active. So how to battle this problem?

On Windows 2000 every DC maintend an attribute on each computer object called lastLogon. It stored the timestamp of the last logon for a computer on that DC. The “on that DC” part is important, because the lastLogon attribute was not replicated beyond the local DC. So to figure out when computer CORP-PC1 last logged on, you would have to query the lastLogon attribute on all the DCs in the domain and find the most recent one. This could be done, but was tedious and time consuming. Enter Windows Server 2003.

In Windows Server 2003 a new attribute was introduced; lastLogonTimestamp. Just like lastLogon, lastLogonTimestamp stored the timestamp of the last logon to the domain for a computer account, but lastLogonTimestamp is a replicated attribute, which means that now every DC knows the most recent logon time for a computer. So to figure out when CORP-PC1 last logged on to any DC in the domain we can query any DC for the lastLogonTimestamp attribute of CORP-PC1. Very nice.

I often find myself in a situation where I need to “clean house” in customer’s directories so I created a script that uses lastLogonTimestamp to find all computers that have not logged on to the domain for X days. X varies greatly from customer to customer, but a good start would be 90 days. The sciprt is called FindOutdatedComputers.vbs and in this post I would like to share it with you.

FindOutdateComptuers.vbs has 3 user changeable variables: intTimeLimit, strDomain and strDC. These are defined at the very beginning of the code and should be the only variables you change.

intTimeLimit: the number of days since a computer logged on to the domain

strDomain: LDAP domain name; e.g. dc=mydomain,dc=com

strDC: FQDN of DC in domain; e.g. dc1.mydomain.com

The script must be run with cscript.exe and takes one command line argument; a 0 (zero) or a 1 (one). 0 mens just echo out the machines that have not logged on since the defined limit and 1 means echo out, but also move the comptuters to the Outdated Computer Objects OU (which the script creates). It will never delete any information from your AD, only move computer accounts to the Outdated Comptuer Objects OU. If you omit this argument, the default action is just to display the results and not move anything.

For FindOutdatedComptuers.vbs to work your domain functional level must be at least Windows Server 2003. The script checks for this and warns you if you are below the needed level.

Here is the code:

https://gist.github.com/morgansimonsen/8040007

Desktop.ini customizations do not take effect

You copy a desktop.ini file into a folder to customize and maybe localize it. You have correctly set the file’s attributes to Hidden, System and Read-Only, but still your customizations do not work. To make it work you need to set the Read-Only or System flags on the folder where the desktop.ini file resides. As I am sure you know, folders cannot be read-only, neither can they “remember” your settings so that all new files placed in the folder become read-only as well. So this flag is purely to tell Windows that this is a special folder and that it should look for a desktop.ini file inside it.

To change the attributes from the command line type:

attrib +R +S <your folder>

From PowerShell:

Set-ItemProperty <your folder> -Name Attributes -Value “ReadOnly,System”

It took me a while to figure this out so hopefully someone can make use of this info.

Here is a link to another issue where your desktop.ini customizations do not work:

If you won’t translate RDS profiles; I will!

Out of pure frustration with the fact that the Active Directory Migration Tool (ADMT) is unable (unwilling is my guess) to do security translation for users’ Remote Desktop Services (RDS) roaming profiles, I decided to take matters into my own hands and created the script below. It is not very refined just now, but I have a lot of ideas for future versions. In the meantime, if you can use it for something; great!

# TranslateRDSProfiles.ps1
# Morgan Simonsen
# http://morgansimonsen.wordpress.com
#
# Script to translate a user profile belonging to a migrated user.
# Primarily intended to solve the problem with ADMT not translating
# Remote Desktop Service roaming profiles.
#
# Version: 1.0 (13.02.2012)
#     Initial version.

#----------------------------------
# User changeable strings
# Only edit in this section!
#
# Root of folder or sharing storing the profiles
$RDSProfileRootDirectory = "Z:" # Include trailing backslash
# NetBIOS Name of source domain; the domain the user was migrated from
$SourceNBTDomainName = "DOMAIN_1" # Include trailing backslash
# NetBIOS Name of target domain; the domain the user was migrated to
$TargetNBTDomainName = "DDS" # Include trailing backslash
# DNS name of target domain
$TargetDNSDomainName = "dds.intern"
# FQDN of Domain Controller in target domain
$TargetDomainDC = "ddsdc1.dds.intern"
# Location of subinacl.exe
$SUBINACLLocation = "C:Program Files (x86)Windows Resource KitsToolssubinacl.exe"
# Location of echoArgs.exe (not actually used by the script)
$echoArgs = ($PSHome+"ModulesPscxAppsechoArgs.exe")
#----------------------------------

Get-ChildItem $RDSProfileRootDirectory | ForEach `
{
    Write-Host ("Processing folder: "+$_.name)
    If (Get-QADUser ($TargetNBTDomainName+$_.name) -service $TargetDomainDC)
    {
        $user = Get-QADUser ($TargetNBTDomainName+$_.name) -service $TargetDomainDC
        Write-Host (" Found matching user: "+$user.Userprincipalname)

        If ( (Test-Path ($_.FullName+"NTUSER.DAT")) -or (Test-Path ($_.FullName+"NTUSER.MAN")) )
        {
            Write-Host (" Found user registry hive: "+($_.FullName+"NTUSER.DAT"))
            Write-Host (" Updating permissions...")
            $entireprofile = ($_.FullName+"*.*")
            $completeusername = ($TargetNBTDomainName+$user.samaccountname)
            Write-Host (" SubInACL.exe File Output:")
            & $SUBINACLLocation /noverbose /subdirectories $entireprofile /grant="$completeusername=f" /setowner=$completeusername  > $null
            Write-Host (" SubInACL.exe Registry Output:")
            reg.exe load ("HKU"+$_.Name) ($_.FullName+"NTUSER.DAT") > $null
            $regkey = ("HKEY_USERS"+$_.Name)
            & $SUBINACLLocation /noverbose /subkeyreg $regkey /grant="$completeusername=f"  > $null
            reg.exe unload ("HKU"+$_.Name) > $null
        }
    }
    Else
    {
        Write-Host " Cannot find user in domain that matches folder name."
        Write-Host " Continuing with next user."
    }
}

	

Exploring the Global Catalog and examining the “universalness” of Universal Groups

Universal groups (UG) are stored in the Global Catalog (GC). But what exactly is the Global Catalog, and how does it store objects? Does it store anything at all! And how do Universal Groups work anyway?

Active Directory Domain Controllers (DC) have exactly one database. It is stored in %windir%NTDS and is called NTDS.DIT. DIT means Directory Information Tree. It is an MDB database. This database is the only place a Domain Controller stores directory data. So that means that the Global Catalog is not a separate database.

Domain Controller databases store Naming Contexts (NC). These are also called directory partitions, or just partitions, and those names actually makes it easier to understand what they are. By default a DC has at least 3 NCs. The first is the Domain NC, also called the Default Naming Context, which stores all the objects belonging to a domain; users, groups, computers etc. The DC has a copy of this NC because it is a DC for the same domain. The second is the Schema Naming Context which stores all the object definitions for a particular forest. And finally the Configuration Naming Context, which stores the common configuration for a forest, e.g. sites, subnets, forest-wide services etc. In addition to these we can have application partitions, used by DNS for example, but they are not important here. The Configuration and Schema partitions are stored by every DC in a forest and contain the same data on all DCs. If you are a DC in a forest you always store the full Configuration and Schema NCs. The Domain Naming Context is stored in full by every DC in the given domain. A DC can only be a DC for one domain, meaning it only ever stores one complete Domain Naming Context. In addition to its own full Domain Naming Context, the DCs that are designated as Global Catalogs also store partial Naming Contexts for all other Domain Naming Contexts in the forest. So the Global Catalog is not a separate Naming Context.

Now, the point of a Global Catalog is to have one or more places where you can get information, but not all the information, from all the domains in the entire forest. To be a GC, a machine must also be a DC. You cannot just be a GC. A GC knows a little bit about every object in every Domain NC in the forest. It also knows everything in the Configuration and Schema partitions, because the GC is also a DC and every DC holds the Configuration and Schema partitions. A forest must have at least one Global Catalog, and every site in a forest must have a Global Catalog. A user must be able to talk to a Global Catalog to log on. You talk to a GC using LDAP or ADSI. A regular DC listens for LDAP traffic on port 389 TCP (and 689 TCP for LDAPS), and on these ports you can retrieve all the information about any object in any of its full NCs. The Global Catalog, which is not a separate database or naming context, remember, listens for LDAP traffic on port 3268 TCP.

What controls the information the GC has about each object from the Domain NCs in the forest is called the Partial Attribute Set (PAS), and it is defined on the attributes in the Schema. Some attributes are part of the PAS, but most are not. If you would like to add an attribute to the PAS you can do so with the Schema Management Console Snap In. So every attribute definition in the schema has a flag that says if that particular attribute should be replicated to the Global Catalog. Which is not a separate database or naming context, but has its own port, but not its own protocol.

So let’s try to sum this up. Objects in domains in Active Directory are stored in Naming Contexts. An object is made up of several attributes, some of which are flagged as being part of the Partial Attribute Set. A DC designated as a GC stores a copy of every domain NC in the forest, containing all the objects in that NC, but only the attributes that are flagged as being part of the PAS. That means that the Global Catalog is just a copy of a regular domain NCs,but with a little less data. A DC/GC also has only one directory service process, LSASS.EXE, which listens on both port 389 TCP and 3268 TCP. If traffic comes in on port 389 you can ask about the Configuration, Schema or full Domain NCs. But if traffic comes in on port 3268 you can only retrieve data about attributes that are part of the PAS, and only from domain NCs (any domain NC on the DC/GC).

What I am trying to say here is that there really is no difference between the regular DC function and the GC function. To say that something is stored in the Global Catalog really does not make sense, since it is in the same database and in the same Domain Naming Context. The only difference is what you can “see” while looking at the GC, as opposed to looking at the DC. Think of this as having DC glasses and GC glasses. When viewed with the GC glasses you can see a fuzzy outline of an object, but when viewed with the DC glasses, the same object is clear and crisp.

When we say that a universal group is stored in the Global Catalog what we are really saying is that a Universal group is a group, stored in the Domain Naming Context of the domain where it was created, which has its members attribute in the Partial Attribute Set. To demonstrate this lets look at a Global Group in a full domain NC (with our DC glasses). You will see the members attribute populated with its members. Now look at the same global group on a GC (with GC glasses). The members attribute is missing so the GC does not know who is a member of the Global Group. A Universal group’s members attribute will look identical when viewed in the full Domain NC and when viewed in the partial Domain NC.

Also, think about this. If Domain B is a child domain of Domain A. What happens to a Universal group created in Domain B when Domain B is decommissioned? It will disappear! Because, it was just a group who had its members attribute in the PAS. If a Universal group was really stored in a separate Global Catalog, it would not be visible in any domains in the forest, it would only be visible when you connected to the Global Catalog partition. Instead, in real life. you can see Universal groups in Active Directory Users and Computers in the domain where you created them, but not in any other domains in your forest. Another effect of this is that when User A in Domain B is a member of a Universal Group created in Domain C, you cannot see that User A is a member of the Universal Group when you look at him in ADUC in Domain B. But you can see that he is a member when you look at the Universal Group’s members in Domain C.

So the Global Catalog is an illusion and saying that you replicate something to it is nonsense. What you are doing is applying a filter to objects.

(Some illustrations would probably be a good idea for this post, but it’s just too late. Sorry!)

Some nice one-liners

  • Restart the computer you are logged on to immediately, forcing all applications to close:
    shutdown.exe /r /t 0 /f
  • The same for a remote computer:
    shutdown.exe /r /t 0 /f /m \<computer>
  • Add someone to the local Administrators groups:
    net localgroup Administrators /add <user or group>
  • Enable Remote Desktop on a remote system:
    reg add “\<computername>HKLMSYSTEMCurrentControlSetControlTerminal Server” /v fDenyTSConnections /d 0 /t REG_DWORD /f
  • Schedule a reboot of the system you are logged on to (works on Windows Server 2003):
    schtasks.exe /Create /RU “NT AUTHORITYSYSTEM” /SC ONCE /TN “Restart” /TR “%systemroot%system32shutdown.exe /r /t 0 /f” /ST 22:00 /Z
  • Schedule a reboot of the system you are logged on to (works on Windows Server 2008 and later):
    schtasks.exe /create /TN “Restart” /SC Once /ST 23:00 /TR “%windir%system32shutdown.exe /r /t 0 /f” /RU “SYSTEM”

Some Active Directory Migration Tool (ADMT) Notes

The good old Active Directory Migration Tool (ADMT) has reached version 3.2 making it compatible with Windows 7/Server 2008 R2 and x64. ADMT started it’s Microsoft life as licensed software from One point. I’ve been using this baby since version 2.0. It offers what you need to perform intra or inter-forest Active Directory migrations/restructures, but it is very basic. The most annoying thing is that Microsoft have yet to implement PowerShell support in ADMT.

Here are some observations I have made about the use of ADMT over the course of many years:

  • No version of ADMT translates security for Remote Desktop (Terminal Server) Roaming Profiles.
    It will translate Local Profiles using the User Profiles option in the Computer Account Migration wizard, and regular Roaming Profiles using the Translate Roaming Profiles option in the User Account Migrationwizard, but it will not touch the Remote Desktop profile. This seems very strange to me since we are just talking about reading another attribute in Active Directory and doing the same thing as you are already doing with Roaming Profiles. Beats me…(Just to be clear, the regular Roaming Profile is specified on the Profile tab of the user in Active Directory Users and Computers (ADUC), and is stored in the profilePath attribute. The Remote Desktop Services Roaming profile is specified on the Remote Desktop Services Profile tab in ADUC and is stored in the userParameters attribute data blob. In the Windows Server 2008 Active Directory schema; Microsoft have created a new attribute called msTSProfilePath that is meant to store this setting in the future.)
  • ADMT needs Full Control NTFS permissions to translate a profile, roaming or local.
    Translating a profile means updating the NTFS permissions on all the files in the profile directory, but also loading the user’s registry hive; the NTUSER.DAT file, and translating the registry permissions set on all the keys in it. I have written about manually performing this process here.If you have not changed the default settings Windows will create roaming profiles with the following ACL/Owner:
    – SYSTEM – Full Control
    user_name – Full Control
    – Owner = user_name
    Obviously these permissions will not let ADMT, which connects to the computer as the user running it (it does not have a service), translate the profile. To work around this you can either enable a Group Policy setting called Computer ConfigurationPoliciesAdministrative TemplatesSystemUser ProfilesAdd the Administrators security group to the roaming user profiles, before the profile is created or you can grant the Administrators local group (or any other user/group you prefer) Full Control permissions to the profile folder and all its files and subfolders. The easiest way to do the latter, in my opinion, is to use PsExec from Sysinternals to start a command shell as SYSTEM and then use CACLS.EXE/XCACLS.EXE/ICACLS.EXE, or any other ACL editor, to add the Administrators group to the files/folders. Here’s an example using CACLS.EXE (which is included in every version of Windows since Windows 2000):
    1. Start the shell as SYSTEM: psexec.exe -i -s cmd.exe
    2. Change the permissions with CACLS.EXE: cacls.exe <root of profile share> /T /E /C /G Administrators:F
  • ADMT uses Server Message Block (SMB)  and Remote Procedure Calls (RPC) respectively, to install and communicate with it’s agents on machines when performing computer migrations or security translations.
    This means that you need to allow traffic from the ADMT machine to all the computers that you are going to migrate or translate security on, on these ports:
  • Traffic Port Notes
    RPC Endpoint mapper 135 TCP The endpoint mapper is like the RPC address book. Whenever an RPC connection is to be made; the client, in this case the ADMT machine, first connects to the endpoint mapper to find out which dynamic port the service it needs to talk to is listening on. It then makes a connection to that port.
    RPC Dynamic Ports Up to Windows Server 2008/Vista:
    1024 – 65535 TCPWindows Server 2008/Vista and later:
    49152 – 65535 TCP
    Any service using RPC talks to the RPC subsystem on the system where it is starting and asks for a port(s). This operation is performed each time the service starts and thus the port can potentially change on every service startup.
    Server Message Block (SMB) SMB Direct Hosting:
    445 TCPNetBIOS Over TCP/IP: 137/UDP
    137/TCP
    138/UDP
    138/TCP
    ADMT connects to the ADMIN$ share on computers it will either migrate or perform security translation on, and copies its files to the %windir%OnePointDomainAgent folder. It also installs a service on the machine called Active Directory Migration Service.
  • ADMT Agent source files
    Are stored in %windir%ADMTNT4Agent for Windows NT machines, or %windir%ADMTW2kAgent for Windows 2000 and above.
  • ADMT cannot migrate a user object that has child objects
    This is true for both inter and intra-forest migrations. Trying to do so will give you this error:
    ERR2:7422 Failed to move source object <RDN of user being migrated>. hr=0x8007208c  The operation cannot be performed because child objects exist. This operation can only be performed on a leaf object.
    The only case where I have found child objects for users are with Exchange Active Sync partnerships. These are stored in a sub container, called ExchangeActiveSyncDevices, as objects of class msExchActiveSyncDevice. Each device the user has an active partnership with has its own object. A typical name of the device object is iPhone§Appl690509FTA4S. It contains attributes of the device, ID, firmware, protocol version etc.You will have to get rid of these one way or the other if you want to migrate the user. If you just delete them, the user needs to set up sync on their devices from scratch once they are migrated. This is usually not a major problem if the users are warned beforehand that this will happen. Another way is to export the sub container and all the device objects, delete it (leaving the user as a leaf object), migrating, and finally importing the device objects back under the migrated user. This last solution requires that you change the distinguishedName attribute when you import the data, and it is probably not supported by Microsoft…at all. Here are some commands using LDIFDE.EXE to export and import the data. You can use this in a script before and after user migration with ADMT:
    Export:
    ldifde.exe –s <source domain DC> –f <export file> –d <DN of the ExchangeActiveSyncDevices container> -l * -o objectGUID
    Note that I have excluded the objectGUID attribute and included all others (-l *). When you import an object into Active Directory it will be assigned a new GUID. Importing a GUID, potentially causing two objects to have the same GUID, is a security violation, and the DC will refuse to perform it.
    Import:
    ldifde.exe -i –f <import file> –c <<old DN> replace with <new DN> (see LDIFDE help for more info)>
    Now, one thing about this. The device object has an attribute called msExchUserDisplayName. It has a value in the form of e.g.: <domain DNS name>/OU/OU/<user>. In other words an LDAP canonical name. If you perform and export/import as I have shown here, this attribute will contain the canonical name the user had in the source domain, i.e. the OLD canonical name. I know for a fact that this will break remote wipe and the Get-ActiveSyncDeviceStatistics cmdlet on Exchange 2007, and probably Exchange 2010. However, the device will continue to sync. (On a side note, this problem also occurs if you move a user within a domain. The msExchUserDisplayName attribute is not updated; breaking wipe and the cmdlet.) To work around this you can try to exclude the attribute when exporting with LDIDFE.EXE or implement code that changes it before you import it back into your target domain. I have not, as of now, tried any of these yet, but I am thinking that if I use PowerShell instead of LDIFDE.EXE to export the objects it would be easy to replace the old canonical name with the new one. I also do not know if removing the attribute from the device object will break the sync partnership. Another problem though, is the device. It stores the domain and username when you set up the account and usually won’t let you change it. The only thing you get to change is the password. So all this work may be for nothing, since users must update the account information on their devices anyway. Damn!
  • Inter-forest migrations do not exclude any attributes (except the SID…sort of)
    According to the ADMT documentation a system attribute exclusion list is created the very first time you run a user migration. The attributes in the list always include the attributes mail and proxyAddresses. I addition, says the docs, the objectGUID, objectSID and legacyExchangeDn attributes are also always excluded even if they are not on the system attribute exclusion list. However, when you do intra-forest migrations ALL attributes are copied. You can examine the system attribute exclusion list with this script, found in KB937537:
    Set o = CreateObject(“ADMT.Migration”)
    WScript.Echo o.SystemPropertiesToExclude
    When I ran this on an ADMT machine used for intra-forest migrations it returnes exactly zero attributes. Examining the migrated objects also verified that indeed all attributes had been copied. The mail, proxyAddressesobjectGUID and legacyExchangeDN were the same. The object had been given a new SID, which it had to because it was now living in a domain with a different domain SID, but the old SID had been copied into the sIDHistory attribute. Neat!

I will probably update this post with more info in the future. For now; Happy migrating!