July 23, 2026
MD2PDF (THM) Walkthrough
TopTierConversions LTD wants you to know their new markdown-to-PDF converter is “totally secure.” That phrase alone in a challenge…

By L4ZZ3RJ0D
4 min read
TopTierConversions LTD wants you to know their new markdown-to-PDF converter is "totally secure." That phrase alone in a challenge description is basically a dare.
I've used these online markdown to PDF tools plenty of times myself but never actually thought about what's happening behind the scenes, so before poking at anything I did a bit of reading first. Turns out most of these tools follow the same basic pipeline, take your markdown, parse it into HTML through something like Marked, then hand that HTML off to a headless browser or PDF library, Puppeteer, wkhtmltopdf, Electron-pdf, to actually render and print the final PDF.
And once you know a headless browser is quietly rendering your HTML behind the scenes, a whole shopping list of classic bugs comes to mind. RCE if the parser reads config blocks with a JS evaluation flag left switched on. SSRF if that headless browser can be tricked into fetching internal URLs through a sneaky image tag or link. Local file read if it'll happily render a file:// path stuffed into an iframe. XSS if raw HTML in your markdown doesn't get sanitized before rendering. None of that guaranteed anything about this specific app, just a mental checklist to test against once I actually started poking.
Checked the page source first and found the JS backing the convert button.
There was also a much bigger file called markdown.js sitting alongside it that I really did not feel like reading line by line, so I just fed it to an AI and asked it to break down what mattered.
Verdict, the client-side code itself isn't the vulnerability, it's just a markdown editor and a fetch call. The real attack surface is whatever's happening server-side once that markdown hits /convert. Which, honestly, tracked with everything I'd just read about how these tools work in the first place.
So I sent a completely normal conversion through and watched it happen in Caido.
Got back a blob, opened it up, just a rendered PDF with my test text sitting in it. Nothing obviously wrong yet. Checked the request itself for any version numbers or fingerprinting info, since knowing exactly which library is doing the rendering usually points straight at a known CVE. Caido showed nothing useful here.
Spent a genuinely annoying amount of time on this before I finally clocked that the version info actually was leaking, Caido just wasn't displaying it for whatever reason. Ran the same request through Burp instead and there it was, sitting plainly in the generated PDF's own metadata.
That was a good reminder to myself, when one tool comes back empty-handed, try the same request in a different one before assuming there's nothing there.
With the tool identified, most of what I read about that specific converter pointed toward SSRF escalating into local file read. So first test, can this thing actually go fetch a URL and render whatever comes back.
Tried pointing it at a local file directly using a file:// path inside an iframe.
<iframe src="file:///etc/passwd"><iframe src="file:///etc/passwd">
Bad request, straight away. So direct file reads were off the table, at least through that exact syntax. Next test, point it at something I controlled instead, my own Python HTTP server, just to confirm the SSRF side actually worked even if file read didn't.
It hit my server and pulled down a PHP reverse shell file I'd staged there, confirmed in the server log and the listener both. But it just fetched the file, it never executed it, since the headless renderer treats whatever comes back as content to display, not code to run. SSRF confirmed, RCE through this exact path, not happening.
Since SSRF clearly worked, the next logical move was checking whether there were other endpoints on this box worth reaching through it, since I hadn't actually fuzzed beyond /convert yet.
admin [Status: 403, Size: 166, Words: 15, Lines: 5, Duration: 72ms]
convert [Status: 405, Size: 178, Words: 20, Lines: 5, Duration: 59ms]admin [Status: 403, Size: 166, Words: 15, Lines: 5, Duration: 72ms]
convert [Status: 405, Size: 178, Words: 20, Lines: 5, Duration: 59ms]An /admin route, forbidden from the outside. Tried hitting it directly and got told exactly why.
"This page can only be seen internally (localhost:5000)." Which, considering I already had a working SSRF primitive that could make the server fetch whatever URL I wanted, was basically an invitation. Except when I ran nmap against port 5000 directly to confirm it was actually there, it came back closed.
Sat there confused for a bit assuming I'd missed something, until it clicked that the challenge instance itself might have just needed a fresh relaunch, services on these boxes don't always spin up instantly. Relaunched the instance and tried again.
Port 5000 alive this time. Sent the SSRF payload through /convert again, this time pointing the iframe at the internal admin panel instead of my own server.
<iframe src="http://127.0.0.1:5000/admin"></iframe><iframe src="http://127.0.0.1:5000/admin"></iframe>And there it was, sitting inside the rendered PDF, the admin panel content the outside world was never supposed to see, flag included.
The whole box in one sentence, an SSRF bug that couldn't read local files or execute code directly, but could absolutely make the server fetch a page it wasn't supposed to talk to from the outside, and that was more than enough once there was an internal-only admin panel sitting there waiting to be asked nicely. Also a solid reminder that when your tooling goes quiet on something, try a second tool before assuming the target's actually clean.
Happy hacking, see you in the next room, hopefully one where my proxy of choice doesn't ghost me on the one detail that mattered :)