Dark Runes
Difficulty: Easy | Category: Web
Overview
A Node.js web application that lets users create documents and export them as PDFs, guarded by an admin-only debug endpoint. The goal is to read /flag.txt from the server by chaining three vulnerabilities together.
Initial Code Analysis
After downloading the challenge source, the following key files stood out:
Key Files
package.json — Revealed critical dependencies:
express— Web frameworkbetter-sqlite3— Databasemarkdown-pdf@11.0.0— PDF generation (vulnerable)node-html-markdown— HTML to markdown conversionsanitize-html— HTML sanitization
crypto.js — Cookie generation logic:
1 | const generateCookie = (username, id) => { |
middlewares.js — Admin check:
1 | const isAdmin = (req, res, next) => { |
pass.js — Access pass management:
1 | const rotatePass = () => { |
generate.js — Debug endpoint:
1 | router.post("/document/debug/export", isAuthenticated, isAdmin, async (req, res) => { |
exporter.js — PDF generation:
1 | const generatePDF = async (content) => { |
Vulnerabilities Identified
1. Weak Access Pass (4-digit brute force)
The generateAccessCode() function creates a 4-digit code (0000–9999), only 10,000 possibilities. This is trivially brute-forceable.
2. Local File Inclusion via PDF Generation
The markdown-pdf library with { remarkable: { html: true } } allows raw HTML injection. The underlying PhantomJS renderer follows <iframe src> attributes, enabling reading of local files directly from the filesystem.
3. Admin Registration Possible
No restrictions prevented registering as admin if the account didn’t already exist in the database.
Exploitation Steps
Step 1: Register as Admin
The admin account didn’t exist initially, so registration was possible:
1 | curl -X POST http://target:port/register \ |
Login to obtain the cookie:
1 | curl -X POST http://target:port/login \ |
Cookie obtained:
1 | eyJ1c2VybmFtZSI6ImFkbWluIiwiaWQiOjF9-19dca7e4ba74ad6c867044992c6c9ea99b609cf5a629619ba15a19b6cb57cb37 |
Step 2: Brute Force the Access Pass
The debug endpoint requires a 4-digit access pass. Script to brute force:
1 | import requests |
Result: Access pass 0130 was discovered.
Step 3: Find Working Payload
The debug endpoint accepts any markdown/HTML content and generates a PDF. Multiple payloads were tested to read /flag.txt:
1 | import re |
Working payload: <iframe src='/flag.txt'></iframe> (payload index 2)
The generated PDF (output_2.pdf) contained the flag.
Step 4: Extract the Flag
1 | strings output_2.pdf | grep HTB |
Flag obtained: HTB{...}
Why the Iframe Payload Worked
- The
markdown-pdflibrary uses PhantomJS (headless browser) to render HTML to PDF - PhantomJS processes iframes and makes HTTP requests to the specified URLs
/flag.txtwas served by the same web server (since static files are served from the root)- The iframe fetched the file content, which was then rendered into the PDF
The JavaScript payloads failed because PhantomJS likely had JavaScript restrictions or the fetch API wasn’t available. Image tags didn’t work because they expected actual image data. The iframe successfully loaded the text file as an HTML document.
How to Fix These Vulnerabilities
1. Strong Access Pass
1 | // Instead of 4-digit code: |
2. Rate Limiting
1 | const rateLimit = require('express-rate-limit'); |
3. Disable Local File Access in PDF Generator
1 | // Use a safer PDF generator or configure PhantomJS to block file:// and local requests |
4. Remove Debug Endpoint in Production
The /document/debug/export endpoint should never exist in production. Use environment variables to conditionally enable debug features.
5. Store Access Pass Securely
1 | // Don't use filename as the secret |
Conclusion
This challenge combined multiple small vulnerabilities chained together:
- Weak 4-digit access pass (brute force)
- Admin registration allowed (no account protection)
- PDF generator with HTML injection leading to local file inclusion
The fix requires strong authentication, removing debug endpoints, and properly sanitizing PDF generation input.
Have a nice day, and see you again in the next writeup!
Any feedback in the comments section is very appreciated.


