Enable Constrained Delegation with a Perl Script

An Active Directory Script to Enable Constrained Delegation:  Delegation is a confusing word (used for different things in AD). In this case, delegation means "allowed to impersonate". For example, if a user logs onto a web server, and the web server must get data from a database or a file share on behalf of the user, the web server must impersonate the user when logging into the database or fileshare. In order for this to work, you must configure the web server's computer account in Active Directory to be "trusted for delegation".

There are two options for delegation: trusted for delegation to any service (wide open), and trusted for delegation to specified services only (contrained).

Two attributes in Active Directory control delegation. One is the userAccountControl attribute, and the other is the msDs-AllowedToDelegateTo attribute. The userAccountControl attribute contains a number that represents a binary bitmask. The msDs-AllowedToDelegateTo attribute contains a list of services that the computer is allowed to delegate to. To enable wide-open delegation we set a specific bit in the userAccountControl attribute, and clear the contents of msDs-AllowedToDelegateTo. To enable constrained delegation, we clear the userAccountControl bit and populate the list of services in the msDs-AllowedToDelegateTo attribute.

You can see a script to configure wide open trust for delegation here.

Below is an example of how to configure constrained delegation. We specify the name of the computer and the SPN (servicePrincipalName) that we want to allow this computer to access when impersonating a user. We search for the computer account using ADO, then we add that SPN (fully qualified and shortened) to the msDs-AllowedToDelegateTo attribute. The attribute is multivalued, so just in case there are already entries, we get the existing entries, then add our new ones, so we don't overwrite the existing list.

use Win32::OLE;
$TRUSTED_FOR_DELEGATION = 524288;
$computer="myComputer";
$spn="HTTP/myweb.mydomain.com";
$base="<GC://".Win32::OLE->GetObject("LDAP://RootDSE")->Get("RootDomainNamingContext").">";
$connection = Win32::OLE->new("ADODB.Connection");
$connection->{Provider} = "ADsDSOObject";
$connection->Open("ADSI Provider");
$command=Win32::OLE->new("ADODB.Command");
$command->{ActiveConnection}=$connection;
$command->{Properties}->{'Page Size'}=1000;
$rs = Win32::OLE->new("ADODB.RecordSet");
$command->{CommandText}="$base;(cn=$computer);distinguishedName;subtree";
$rs=$command->Execute;
if($rs->{recordCount} < 1){ print "Computer account not found\n"; }
until ($rs->EOF){
 $dn=$rs->Fields(0)->{Value};
 if($obj=Win32::OLE->GetObject("LDAP://$dn")){
  print "$obj->{userAccountControl}\n";
  if($obj->{userAccountControl} & $TRUSTED_FOR_DELEGATION){ $obj->{userAccountControl} ^= $TRUSTED_FOR_DELEGATION; }
  $spns=$obj->Get("msDS-AllowedToDelegateTo");
  push(@{$spns},$spn);
  ($shortspn,$domain)=split /\./,$spn;
  push(@{$spns},$shortspn);
  $obj->Put('msDS-AllowedToDelegateTo',$spns);
  $obj->SetInfo();
  $error=Win32::OLE->LastError();
  if($error == 0){
   print uc($computer)." configured for constrained delegation successfully\n";
  }else{
   print "Failed with error $error\n";
  }
 }else{
  print "Failed to connect to computer object\n";
 }
 $rs->MoveNext;
}

Find Disabled Users in Active Directory using VBScript

A Script to Find Disabled Users in Active Directory:  To see the Perl version of this script, click here, and click here to see the PowerShell version. The script uses our typical ADODB search code to search AD. The key, as always is the search filter. In this case, we're searching for disabled users. Unfortunately, there is no attribute that holds the enabled/disabled status of the user. Suprising. It turns out that the disabled status is stored as a bit in the useraccountcontrol attribute. This attribute contains a number that is made up of binary bits, each having a different meaning. You can look up the meaning of each bit on MSDN at http://msdn.microsoft.com/en-us/library/ms680832(VS.85).aspx

Anyway, the second bit (2) is the account disabled bit.

Microsoft has given us a way to make a search filter that can search against a bit in an attribute, called LDAP matching rules. They are specified by OID's (long ugly numbers). According to the Search Filter Syntax page (http://msdn.microsoft.com/en-us/library/aa746475(VS.85).aspx), 1.2.840.113556.1.4.803 is equivelant to a bitwise AND.

So here's the script. The search filter does a bitwise AND of the contents of the useraccountcontrol attribute and the number 2 (remember the 2 bit means disabled). So the script searches for everyone in your AD that has the 2 bit set (disabled users).

set dse = GetObject("LDAP://RootDSE")
root = dse.Get("RootDomainNamingContext")
base = "<GC://" & root & ">"
Set conn = CreateObject("ADODB.Connection")
Set comm = CreateObject("ADODB.Command")
conn.Provider = "ADsDSOObject"
conn.Open "Active Directory Provider"
Set comm.ActiveConnection = conn
comm.Properties("Page Size") = 500
comm.CommandText = base & ";(&(objectCategory=User)(userAccountControl:1.2.840.113556.1.4.803:=2));cn,displayName,distinguishedName;subtree"
Set rs = comm.Execute
Do Until rs.EOF
    Wscript.Echo rs.Fields(0).Value & " (" & rs.Fields(1).Value & ") " & rs.Fields(2).Value
    rs.MoveNext
Loop

Active Directory Enumeration and Search

In Part 1 and Part 2, we covered how to connect to objects, read and write attributes, call methods, and create users and groups. Now let's look at two more items. Enumeration and search.

By enumeration, we mean get a list of objects within an OU, or more precisely, get a collection of objects by accessing the enumerator of a container. The enumerator of a container is a collection of the objects within the container, be they users, groups, sub-OU's, etc. The following script returns the names of the objects within a container.

use Win32::OLE;
$ou=Win32::OLE->GetObject("LDAP://ou=HR,dc=myDomain,dc=net");
foreach $obj (in $ou){
 print "$obj->{name}\n";
}

Here we've introduced a few new things. The Perl keyword: foreach, which loops through each item in a list or collection, and the Win32::OLE keyword in, which returns the enumerator from our OU object. The name attribute returns a string that includes the CN= for a user or group, and an OU= for an OU.

If you had various object types in your OU (users, groups, and OU's), you'll see that you may not want to return all types of objects. You may be interested in just users or just groups. There are a few ways to filter out the items that you don't want. The first way is to use a filter. A filter is a property of a container, that specifies the object class or classes that you want returned in the enumeration. The filter is an array of class names, so we create an array, then set the filter attribute of the OU to that array. For example, if we want to see only the sub-OU's, we setup our filter and specify the organizationalUnit class.

use Win32::OLE;
$ou=Win32::OLE->GetObject("LDAP://ou=HR,dc=myDomain,dc=net");
@filter=("organizationalUnit");
$ou->{filter}=\@filter;
foreach $obj (in $ou){
 print "$obj->{name}\n";
}

Or if you only wanted to see users and groups, you could filter this way:

use Win32::OLE;
$ou=Win32::OLE->GetObject("LDAP://ou=HR,dc=myDomain,dc=net");
@filter=("user","group");
$ou->{filter}=\@filter;
foreach $obj (in $ou){
 print "$obj->{name}\n";
}

Alternatively, you could return all object classes, then look at the object class within the loop, like this:

use Win32::OLE;
$ou=Win32::OLE->GetObject("LDAP://ou=HR,dc=myDomain,dc=net");
foreach $obj (in $ou){
 if($obj->{objectClass}[1] eq "organizationalUnit"){
  print "$obj->{name}\n";
 }
}

You might notice this is slower than using a filter, since it's returning a larger set of objects, and you're retrieving an attribute (objectClass) from each one. Notice the [1] after the {objectClasss}. That's needed because {objectClass} returns an array of classes. If you're familiar with object-oriented programming, you may be familiar with class inheritance. One object class inherits part of its design (some if its attributes and methods) from a parent class. In the case of Active Directory, all objects inherit their design from the base object class, called top. So the {objectClass} array contains all of the object classes that the object inherited from it's native class and all inherited classes. {objectClass}[0] will always be top. Not very useful. But if we look at {objectClass}[1], we see the classes we're looking for, like group, user, or organizationalUnit. Enough on that.

You can envision a script that enumerates the root of the domain, and for each OU, enumerates that OU, and so on down the tree. Easily done, but I'm not so sure that's ever what you really want to do. Do you really want to ask your domain controller to spit out information from every object in your domain? Probably not, especially if you have a large domain. That's where search comes in.

I've already covered searches in many of my earlier posts but let's go over it again.

Active Directory is searchable just like a database (well, it is a database). And we use Microsoft ActiveX Data Objects (ADO) as the interface to do the search. Searches don't return actual objects, like enumeration does, instead it returns only the attributes we specify. For example, the following script returns the name of each OU in the domain:

use Win32::OLE;
$base="<LDAP://dc=myDomain,dc=net>";
$connection = Win32::OLE->new("ADODB.Connection");
$connection->{Provider} = "ADsDSOObject";
$connection->Open("ADSI Provider");
$command=Win32::OLE->new("ADODB.Command");
$command->{ActiveConnection}=$connection;
$command->{Properties}->{'Page Size'}=1000;
$rs = Win32::OLE->new("ADODB.RecordSet");
$command->{CommandText}="$base;(objectCategory=OrganizationalUnit);name;subtree";
$rs=$command->Execute;
until ($rs->EOF){
 $name=$rs->Fields(0)->{Value};
 print "$name\n";
 $rs->MoveNext;
}

Looks a little complicated, but let's look at each line. First, we define a searchbase ($base) that points to the root of our domain. Then we create an ADODB connection object and tell the connection to use the ADsDSOobject provider, which is the database provider for AD. We then call the Open method on the connection and give it a name (which is pretty much useless, but required).

Next we create an ADODB command object, link the command to the connection, set the page size property, and the command text property.

The page size property defines how you want large result sets handled. By default, 1000 records will be returned and no more. If you want more than 1000 records returned, you must specify a page size. You should experiment with page size to improve the performance of searches that return large result sets. The optimum page size will be determined mostly by the load and available memory on your domain controllers. Setting the page size too large will use up a lot of memory on your domain controllers, while setting the size too small will slow down your searches.

The command text is the actual search criteria. In this case we're asking ADO to search the location specified by $base, search for objects whose objectCategory is OrganizationalUnit, return the name, and search the entire subtree (within all sub-OU's).

The format of the command text can be one of two dialects. LDAP dialect, which I used in the example above, or SQL dialect. LDAP dialect is formatted as:

"searchBase;searchFilter;attributesToRetrieve;searchScope"

searchBase, as shown above, is the ADsPath of your domain or OU, with the protocol prefix (LDAP:// or GC://) and opening and closing <> brackets.

searchFilter is an LDAP search filter, which may take some time to master. In its simplest form, it's (attribute=value), but you can combine statements using binary operators like & (and) and ¦ (or). For example, (&(objectCategory=User)(cn=Harry)) will find our friend Harry.

The attributes to retrieve can be seperated by commas, and finally the searchScope specifices how many levels deep you want to search: just the location specified by the searchBase (base), one level deep (onelevel) or the entire subtree (subtree).

As an alternative to LDAP dialect, you can use SQL dialect, as shown below:

use Win32::OLE;
$base="LDAP://dc=myDomain,dc=net";
$connection = Win32::OLE->new("ADODB.Connection");
$connection->{Provider} = "ADsDSOObject";
$connection->Open("ADSI Provider");
$command=Win32::OLE->new("ADODB.Command");
$command->{ActiveConnection}=$connection;
$command->{Properties}->{'Page Size'}=1000;
$rs = Win32::OLE->new("ADODB.RecordSet");
$command->{CommandText}="select name from '$base' where objectCategory = 'OrganizationalUnit'";
$rs=$command->Execute;
until ($rs->EOF){
 $name=$rs->Fields(0)->{Value};
 print "$name\n";
 $rs->MoveNext;
}

I won't get into SQL dialect (I always use LDAP dialect), but you SQL programmers will get the idea.

So those are the basic mechanics of getting around in Active Directory. What remains is to get a handle on the Active Directory object classes and their attributes, and the challenges of the occasional special data type.

Please feel free to post comments and questions. I'll do my best to help out.

Creating Active Directory Users and Groups

In part 1, I showed how to use the Win32::OLE module to access objects in Active directory. We connected to a user, read an attribute, wrote to an attribute, and called a method. Now, let's create a new object.

Not suprisingly, we'll use the Create method. The Create method is a method of a container object (we're creating an object in a container so we call container->Create). So first, we have to connect to the container. As in part one, this is easy, as long as you know the distinguished name of the container. For example, let's say you have an OU called HR, and you want to create a new user in the HR OU. So, we need to connect to the HR OU.

use Win32::OLE;
$ou=Win32::OLE->GetObject("LDAP://ou=HR,dc=myDomain,dc=net");

Next we call the Create method. The Create method can create any class of object (user, group, etc), so we need to tell it what class of object to create, and give it a cn (commmon name). It's distinguished name will be the cn plus the dn of the container. So, let's create a user called Harry. Each object class has a set of mandatory attributes (and optional attributes). We have to fill out the mandatory attributes before we try to save the new user, otherwise we'll get an error. After calling the Create method, there's only one mandatory attribute missing for our new user: SAMAccountName. This attribute is the the user's logon ID (what they type in the user name field when they logon). Then Finally, we save the user with SetInfo().

use Win32::OLE;
$ou=Win32::OLE->GetObject("LDAP://ou=HR,dc=myDomain,dc=net");
$user=$ou->Create("User","cn=Harry");
$user->put("samAccountName","harry");
$user->SetInfo();

OK, that worked, but wait! Our new user not only doesn't have a password yet, but it's also disabled. So let's take care of those. Things like passwords and account flags are optional attributes, so we can't add them before the user is created. So, we have to add them after the SetInfo(), then call SetInfo() a second time...

use Win32::OLE;
$ou=Win32::OLE->GetObject("LDAP://ou=HR,dc=myDomain,dc=net");
$user=$ou->Create("User","cn=Harry");
$user->put("samAccountName","harry");
$user->SetInfo();
$user->{AccountDisabled}=0;
$user->SetPassword("swordfish");
$user->SetInfo();

Of course you can add additional attributes before you call the last SetInfo(), but we've already done enough to create a working user account that can logon.

Next let's have a look at groups. Let's create a group. A group has similar requirements as a user when creating it. You need to provide the object class, a common name, and a SAMAccountName. However, you also need to specify what type of group you're creating. There are several types of groups.

First, a group can be a distribution list or a security group. Both types of groups may be used to send email to the members of the group, but distribution lists may not be used in an access control list (ACL) because no security identifier (SID) is associated with a distribuition list. The benefit of using a distribution list instead of a security group is that because it does not have a SID, it will not add a SID to the user's security token, thereby avoiding token bloat when the user is a member of many distribution lists. Enough on that.

Then there is group scope. The scope of the group can be universal, global, or domain local. Universal groups can contain users, global and universal groups from any domain in the forest, and can be applied to any ACL in the forest. Global groups can only contain members from their own domain, but can be applied anywhere in the forest, and domain local groups can contain users and groups from anywhere in the forest, but can only be applied to ACLs on computers in their own domain. Anyway, here's the code to create a universal security group.

use Win32::OLE;
$GLOBALGROUP = 2;
$DOMAINLOCALGROUP = 4;
$UNIVERSALGROUP = 8;
$SECURITYGROUP = 2147483648;
$ou=Win32::OLE->GetObject("LDAP://ou=HR,dc=myDomain,dc=net");
$group=$ou->Create("Group","cn=HRUsers");
$group->put("samAccountName","HRUsers");
$group->put("groupType",($UNIVERSALGROUP ¦ $SECURITYGROUP));
$group->SetInfo();

OK, now that we've created our group, let's add a member to it. We just created a user account for Harry, so let's add him to the HRUsers group. To add a user to a group, you connect to the group and call the Add method. The Add method takes one parameter, the ADsPath of the member to add.

use Win32::OLE;
$group=Win32::OLE->GetObject("LDAP://cn=HRUsers,ou=HR,dc=myDomain,dc=net");
$group->Add("LDAP://cn=Harry,ou=HR,dc=myDomain,dc=net");

Simple. Notice that you don't have to call SetInfo() when adding members to a group, the Add method saves the changes for you.

That's the basics of how to create users and groups in Active Directory. Now on to part 3 where I'll discuss enumerating and searching for objects.