MD2PDF
Lab Info
| Detail | Info |
|---|---|
| Room | MD2PDF |
| Platform | TryHackMe |
| Difficulty | Easy |
| Type | SSRF via HTML Injection in PDF Renderer |
| Knowledge Required | Web, SSRF, HTML, Gobuster |
Tools Used
Nmap — Port scanning
Gobuster — Directory enumeration
Browser — Interacting with the converter and reading the output PDF
First Impressions
The room description reads: "TopTierConversions LTD is proud to announce its latest and greatest product launch: MD2PDF. This easy-to-use utility converts markdown files to PDF and is totally secure! Right…?"
That "Right…?" is doing a lot of work. The vulnerability is baked into the product's core function — and this room is a great illustration of why features and vulnerabilities aren't always separate things.
Recon
nmap -sC -sV <ip>
22/tcp open ssh
80/tcp open http
Two ports. Web server on 80. Let's look at what's running.
The homepage is minimal — a single text area, a convert button, and a download link. You paste Markdown, it spits out a PDF. I test a few basic inputs to understand the behavior: headings, bold text, links. Everything converts cleanly.
Nothing jumps out yet. Time to enumerate.
gobuster dir -u http://<ip>/ -w /usr/share/seclists/Discovery/Web-Content/common.txt
One result stands out:
/admin (Status: 403)
A 403 — not a 404. The page exists. The server is actively refusing us. And the error message when you try to visit it directly tells us exactly why:
This page is only accessible from localhost.
So there's an admin panel. We can't reach it from the outside. But we know the server can reach it from the inside. And we have a tool that makes server-side requests: the PDF renderer.
Understanding the Attack Surface
Markdown is a lightweight syntax for writing formatted text. But here's the thing — HTML is valid Markdown. By spec, any raw HTML tag you embed in a Markdown document gets passed through as-is. No escaping, no sanitization — it's a feature, not a bug.
The PDF renderer this app uses is wkhtmltopdf — a tool that converts HTML to PDF by running a headless browser engine on the server. When it encounters an <iframe> pointing to a URL, it doesn't just note the URL. It fetches it. Server-side. From localhost.
That's the key insight: when wkhtmltopdf renders our PDF, the HTTP requests it makes come from the server itself — not from our browser. So anything restricted to localhost is accessible to it.
This is Server-Side Request Forgery (SSRF): we're tricking the server into making requests on our behalf, reaching internal resources we can't directly access.
Exploitation
The payload is a single HTML tag:
<iframe src="http://localhost:5000/admin" width="1000" height="1000"></iframe>
Paste it into the Markdown input box and click Convert.
The PDF downloads. Open it.
Inside the rendered PDF is the /admin page — the one that told us to go away when we visited it in our browser. The iframe was fetched server-side, rendered into the document, and handed back to us as a PDF.
Flag: THM{REDACTED} ✅
Why This Works — The Full Picture
When you visit /admin in your browser, the request comes from your external IP. The server checks: not localhost — 403.
When wkhtmltopdf processes the iframe, the request comes from 127.0.0.1. The server checks: that's localhost — 200.
The access control was checking who was asking, not whether the resource should be exposed at all. The PDF renderer became a proxy that bypassed the check entirely.
Key Takeaways
1. HTML in Markdown is a trust boundary problem Most Markdown parsers accept raw HTML by design. If that HTML gets rendered by a server-side engine rather than a client browser, the entire threat model changes. Server-side rendering of user-supplied HTML should always be treated with the same caution as eval().
2. PDF generators are server-side browsers wkhtmltopdf, Puppeteer, Headless Chrome — these tools fetch URLs, load resources, and execute HTML just like a browser does, except they do it from the server. Any URL they can reach is reachable by an attacker who can control their input. Internal services, cloud metadata endpoints (169.254.169.254), and admin panels with localhost-only restrictions are all in scope.
3. Localhost-only restrictions are not security boundaries Restricting an endpoint to 127.0.0.1 only prevents direct external access. If any server-side component can make requests and can be influenced by user input — a PDF renderer, a URL fetcher, an image proxy — that restriction collapses. Real internal services need authentication, not just IP filtering.
4. The simplest payload is often the right one One <iframe> tag. No exploit chain, no encoding tricks, no brute force. The entire room is solved by understanding what the renderer does and giving it a URL to fetch. This is a pattern worth remembering — the most dangerous SSRF payloads are usually the most obvious ones.
Day 25 of 30 ✅ — See you tomorrow.