Wednesday, December 28, 2016

Any sufficiently advanced Active Directory Web Service is indistinguishable from magic

Or how to retrieve the password of a Group Managed Service Account

I’m not one to leave things alone if I feel like I don’t have a solid and deep understanding of them, well that or I completely ignore them… In the case of the Active Directory Web Service I have always ignored it and been confused why it existed or what it did. As far as I could tell, it created more attack surface on DCs and extra dependencies. As far as I had seen, the ADWS did a poor job retrieving data I have always gotten in other ways in the past. It limits result set sizes and other odd things. The one thought I had was, “maybe the ADWS does less than LDAP or ADSI, so one can block those in certain situations”. That would be a nice security control. It turns out that the ADWS lets you do just about anything that can be done via LDAP v3, including the use of extended LDAP operations. At this point, I assumed that the ADWS was fully redundant to the LDAP and ADSI interfaces. Then I found something that “can only be done via the ADWS”, interaction with Group Managed Service Accounts.

Group Managed Service accounts, or gMSAs, are the new hotness in service accounts.  By new I mean 2012. The gMSA is a service account that auto rotates its password and works with services that support it, so that the service and AD rotate the password at the same moment. Microsoft first introduced the managed service account (notice the missing word group), which was a great idea, but didn’t meet the needs of an enterprise. The old managed service account could only run on a single server and wasn’t well supported by various services. The gMSA can run on any number of servers, allowing for the account to hold a Service Principal Name (SPN), which is key to the service working with Kerberos. While Kerberos is supposed to be the default auth method for Windows, since 2000, a huge number of systems may fall back to NTLM if Kerberos is misconfigured. As I’ve noted before, NTLM comes in flavors of bad and worse. gMSAs help deal with this while removing the overhead of manual service account password changes.

Here’s where the learning began for me. Not all services and systems can work using a gMSA. I’m not clear why this is the case when something simply runs as a service, but I am sure there are good reasons. Possibly it could be things like DPAPI encryption, though the gMSA stores, and returns, two versions of the password, so DPAPI shouldn’t be an issue. None the less, I decided I’d just write my own .NET tools to retrieve the password and do the needful.

The Trail of Failure

The gMSA password is stored in the attribute msDS-ManagedPassword. Also, here. So, I created a gMSA and tried to retrieve the password using LDAP, via ldifde. Then the sadness started. I tried to pull back the samaccountname and password blob. Yes, like a computer object the tools add a trailing $ to the samaccountname.

C:\Windows\system32>ldifde -f ssss  -l msDS-ManagedPassword,samaccountname -r "samaccountname=_coolaccount$"

Connecting to "fundc1.xcloud.gbl"

Logging in as current user using SSPI

Exporting directory to file ssss

Searching for entries...

Writing out entries

No Entries found

 

The command has completed successfully

 

C:\Windows\system32>

 

I tried again, leaving out the password.

C:\Windows\system32>ldifde -f ssss  -l samaccountname -r "samaccountname=_coolaccount$"

Connecting to "fundc1.xcloud.gbl"

Logging in as current user using SSPI

Exporting directory to file ssss

Searching for entries...

Writing out entries.

1 entries exported

 

The command has completed successfully

 

C:\Windows\system32>

 

This was odd so I decided to get more sophisticated by running the command in .NET code and failed.  I always use the System.DirectoryServices.Protocols (S,DS.P) namespace, as it calls the native LDAP libraries with no interference by the ADSI layer. It also works great with 3rd party LDAPs. 

I did a search request similar to this:

SearchRequest searchRequest = new SearchRequest

                                    (targetOu,

                                      ldapSearchFilter,

                                      SearchScope.OneLevel,

                                      strArrayOfAttributes);

 

I still got nothing back and be began to suspect that there was a mystery afoot.  I then tired via PowerShell.

PS C:\Users\whataname> Get-ADServiceAccount -Identity _coolaccount2 -Properties 'msDS-ManagedPassword'

 

 

DistinguishedName    : CN=_coolaccount2,CN=Managed Service Accounts,DC=xcloud,DC=gbl

Enabled              : True

msDS-ManagedPassword : {1, 0, 0, 0...}

Name                 : _coolaccount2

ObjectClass          : msDS-GroupManagedServiceAccount

ObjectGUID           : 380a65ba-fb12-491f-ac4f-775f0eb7ceba

SamAccountName       : _coolaccount2$

SID                  : S-1-5-21-2885290700-3155337720-2128568531-2106

UserPrincipalName    :

 

 

 

PS C:\Users\whataname>

 

I got back the password blob and did a small dance, then I went to work on figuring out how PowerShell got the password when I was unable to. I started a packet capture using Netmon 3.4, Which showed me that PowerShell was reaching out to a DC on TCP port 9389. This is the port for the ADWS. I did much searching and tried several tools, such as Fiddler, to let me see inside the AWDS traffic. None of these worked.

On the Road to Successville

Finally I found a great article boasting exactly what I needed, How to view SOAP XML messages to and from AD Webservices and Powershell. This was the cure I was looking for. I setup tracing and then created a new gMSA, and then used the PowerShell command to retrieve the password blob.

In both the creation and the retrieval of the password, I found this:

<ad:controls>

<ad:control type="1.2.840.113556.1.4.801" criticality="true">

<ad:controlValue xsi:type="xsd:base64Binary">MIQAAAADAgEH</ad:controlValue>

</ad:control>

</ad:controls>

If you do a lot of AD work, 1.2.840.113556.1.4.801 pops out as an LDAP Extended Control OID. In this case, it is LDAP_SERVER_SD_FLAGS_OID control that lets one send and receive Security Descriptors. The controlValue ‘30 84 00 00 00 03 02 01 07’ is the BER encoded ASN.1  value of 07 which gets us the user owner, group owner, and DACL.

With this information, I realized I couldn’t use ldifde, as it does not let you specify LDAP Controls. The next step was to use S.DS.P to issue the search request with the LDAP Control.  I had a well-built project using S.DS.P, that implemented a control already, so rather than rewriting it all, I just cut and pasted a bit and requested the password blob and it worked!

With the assurance that I could pull back the password blob, I went about making a cleaner implementation than my borrowed code. I also went about writing a class to decode the password blob.

As the passwords are random, the chars map to mostly non-standard characters. Using the password in an LDAP bind confirms the password is good.

My leaner and cleaner implementation for retrieving the password blob did not go so well. I repeatedly got back {"An operation error occurred."}. This occurred even with strong LDAP focused error handling.  It left me with very little to go on.

When I took a close look at the different code, I noticed that the working code explicitly setup my LdapConnection securely:

            theConn.SessionOptions.Sealing = true;

            theConn.SessionOptions.Signing = true;

 

The default options set signing to true, but sealing to false. 

This means that the connection, by default, has integrity (tamper) protection, but the data is not encrypted, so anyone who can sniff it can read it.  It makes perfect sense to not send back an unencrypted password over and unencrypted channel. In fact, password changes are also not allowed over unencrypted connections, by default.  Knowing that encryption is required it was easy to find the ldifde syntax to encrypt and get back the password blob.

-h              Enable SASL layer signing and encryption

 

C:\Windows\System32\pwdb>ldifde -f ssss  -l msDS-ManagedPassword,samaccountname -r "samaccountname=_coolaccount$" -h

Connecting to "fundc1.xcloud.gbl"

Logging in as current user using SSPI

Exporting directory to file ssss

Searching for entries...

Writing out entries.

1 entries exported

 

The command has completed successfully

 

C:\Windows\System32\pwdb>type ssss

dn: CN=_coolaccount,CN=Managed Service Accounts,DC=xcloud,DC=gbl

changetype: add

sAMAccountName: _coolaccount$

msDS-ManagedPassword::

 AQAAACIBAAAQAAAAEgEaASJTvMGeMn+PpMxf+1PtLESZoQu7OKmjqR1ndZIRZf821rjx0KNBvR2kii

 2Jadwe5km9fZFrejLezkuzznJ5VeHAaHuNEklQgwNd8vMjpXYwMc13jMGSxnRoT3M727mn4EL887Dj

 RjGyhKsN7Jg1vPGIl7ZQXspR8vfU09Oo26EkMsPw67NNrDyZLRdSLjMvaZ3jSh6wfFqH5IaKMevSs2

 ouDrj3dTsx63lrj/bXuQ1XMrH39ChcZ07qkIAW/F8dDAdjGVSMPFusmP+WqyrotDS8s7NYpVVNJPV0

 nP+CnBePs+38WCgG0cJBfZCv4bfPZELYJb8x9UX9FiD+D5LHVEAAAPIH4veYFgAA8qkRRZgWAAA=

 

 

C:\Windows\System32\pwdb>

 

Sure enough, buy adding signing and sealing to my new project, I was able to get back the blob. This spurred the question, “what’s up with the LDAP_SERVER_SD_FLAGS_OID?”  I commented out the LDAP Control and was still able to get the password back.

Soooo, what is up with the LDAP_SERVER_SD_FLAGS_OID?  It turns out, that for some unknown reason, the principals that can retrieve the password are not stored as part of a standard ACE, nor as some list of SIDs, but instead, the list is stored as a binary blob in security descriptor format on the attribute msDS-GroupMSAMembership. Like other security descriptors, the LDAP Control must be present to read or write to it.

To read the SD, the query is formed like this:

 

                string ldapSearchFilter = string.Format("(|(sAMAccountName={0})(sAMAccountName={0}$))", acctName);

                string[] attribs = new string[] { "name",

                            "msDS-GroupMSAMembership"                           };

 

                // create a SearchRequest object

                SearchRequest searchRequest = new SearchRequest

                                                (this.DomainDN, ldapSearchFilter,

                                                 System.DirectoryServices.Protocols.SearchScope.Subtree, attribs);

 

                byte[] OIDVal = new byte[] { 0x30, 0x84, 0x00, 0x00, 0x00, 0x03, 0x02, 0x01, 0x07 };

                DirectoryControl editSDOid = new DirectoryControl("1.2.840.113556.1.4.801", OIDVal, false, true);

                searchRequest.Controls.Add(editSDOid);

 

In order to read and write the security descriptors, one can use the System.DirectoryServices.ActiveDirectorySecurity class. Get the byte[] value of  msDS-GroupMSAMembership and pass it into that class like so:

public byte[] addSIDtoSD(byte[] currentSddlBytes, string newSID)

        {

            ActiveDirectorySecurity ads = new ActiveDirectorySecurity();

            SecurityIdentifier realSID = new SecurityIdentifier(newSID);

 

            byte[] sddlOut = new byte[2];

 

            try

            {

                ads.SetSecurityDescriptorBinaryForm(currentSddlBytes);

                AuthorizationRuleCollection bb = ads.GetAccessRules(true, true, typeof(SecurityIdentifier));

                bool bAlreadyInSD = false;

 

                //skip the add if the SID is already on the list

                foreach (AuthorizationRule ar in bb)

                {

 

                    if (ar.IdentityReference.Value.ToString() == realSID.ToString())

                    {

                        bAlreadyInSD = true;

                        break;

                    }

                }

 

                if (!bAlreadyInSD)

                {

                    //add it to the SD

                    ads.AddAccessRule(new ActiveDirectoryAccessRule(realSID, ActiveDirectoryRights.GenericAll, AccessControlType.Allow));

 

                    //output the new SD in bytes

                    sddlOut = ads.GetSecurityDescriptorBinaryForm();

 

One benefit of doing this in .NET is that the New-ADServiceAccount and Set-ADServiceAccount commands only let you overwrite the list of PrincipalsAllowedToRetrieveManagedPassword, so it is more work to add or remove a user or group. Also, the underlying PowerShell will not allow mixing the list so that it contains users and groups.

Set-ADServiceAccount : An AD Property Value Collection may only contain values of the same type. Specified value of

type 'Microsoft.ActiveDirectory.Management.ADGroup' does not match the existing type of

'Microsoft.ActiveDirectory.Management.ADUser'.

Parameter name: value

At line:1 char:1

+ Set-ADServiceAccount -Identity _coolaccount -PrincipalsAllowedToRetrieveManagedP ...

+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    + CategoryInfo          : InvalidArgument: (_coolaccount:ADServiceAccount) [Set-ADServiceAccount], ArgumentExcepti

   on

    + FullyQualifiedErrorId : ActiveDirectoryCmdlet:System.ArgumentException,Microsoft.ActiveDirectory.Management.Comm

   ands.SetADServiceAccount

 

There is no such constraint in AD, so using .NET code you can avoid breaking your account.

What it’s Like in Successville (Summary?)

The Active Directory Web Service, like all magic, just uses some sleight of hand and misdirection with gMSAs.

You simply need to request the password blob using SSL or SASL encryption. No LDAP Controls are needed to read it.

You can view or edit the msDS-GroupMSAMembership list if you know how to work with security descripts and include the LDAP Extended Control 1.2.840.113556.1.4.801 LDAP_SERVER_SD_FLAGS_OID.

Cleaner code can be found here.

 

 

No comments:

Post a Comment