KalmarCTF 2023 - Invoiced - Writeup

Posted on Mar 5, 2023

Final working payload

Name: a</title><meta http-equiv="Refresh" content="0; url=''" />

Others: empty

What we know

  • Download source
  • Read files:
    • payments.js - get discount code FREEZTUFSSZ1412
    • app.js - get the implementation
      • GET /cart => show form to post to checkout
      • POST /checkout => visit url, turn to pdf
        • pdf impl: open puppeteer, visit /renderInvoice?name=…, wait for networkidle0, bake to pdf
      • GET /renderInvoice => replace variables in template
        • URL parameters are rendered without escaping => XSS
      • GET /orders => return flag
  • Find unique safety features:
    • /renderInvoice has:
      • CSP
        • default-src unsafe-inline maxcdn.bootstrapcdn
        • object-src none
        • script-src none
        • img-src self dummyimage.com
    • /orders has:
      • checks remote address is
      • checks for no “Bot” cookie
      • sets X-Frame-Options to none (NO EFFECT)
      • returns flag
    • PDF bot always sets the bot cookie
      • SameSite=Strict
      • HttpOnly

Enumerate the possibilities

In no defined order, we tried to inject:

  • script tag
    • fails because script-src
  • meta http-equiv CSP
    • first thought it was failing because it was rendered twice… used tricks like name=...<!-- address=--> to no avail wrong, it was failing because of the subtractive principle of csp
    • if CSP header and meta tag are both present, they are subtractive
  • iframe /orders
    • fails because frame-src
    • got too stuck on credentialless iframe, spent way too much time
  • iframe srcdoc then script
    • fails because srcdoc inherits parent CSP
  • Reviewing bootstrap
    • Nothing interesting .. and would still follow CSP
  • Considered scanning maxcdn
    • didn’t
  • <img src="/orders">
    • works but wrong mime so broken image :)
    • no interesting mime types that are actually plaintext, either
    • using <picture><source src> to set the mime type doesn’t work
      • because the flag is not an SVG
  • loading it as a VTT subtitle
    • using <video><track> doesn’t work because CSP (media-src => default-src => no ‘self’)
  • got desperate, thought we could load JS from bootstrap cdn
    • still can’t, script-src hasn’t changed, it’s still none …
  • <base> tag?
    • no
  • trusted types?
    • what?
  • then @amahlaka97 flies in with a</title><meta http-equiv="Refresh" content="0; url='http://localhost:5000/orders'" />
  • have to bypass bot cookie
    • maybe we run the meta inside a credentialless iframe?
      • no, because CSP frame-src
  • amahlaka97: we somehow just need to get the bot cookie removed from the refresh request
    • yes
    • just change the origin
    • localhost is not
    • a</title><meta http-equiv="Refresh" content="0; url=''" />
    • BOOM FL√ĄG



  • Be comprehensive
  • Get an @amahlaka97 in your team
  • There are many many ways to load files
  • CSP can’t be relaxed, only restricted with more CSP tags
  • Never forget meta refresh tag

Ranting and rambling while solving it…

  • lots of different things we tried to mess with CSP
<iframe loading="eager" src="/orders" width="200" height="200" style="background:red" credentialless>NOT LOADED</iframe>
  • empty page (cc invoice.pdf, invoice2.pdf)
<iframe srcdoc="<!DOCTYPE html>"></iframe>

^ Works but can’t run scripts – it inherits csp

Idea from Carlos (Hi @carlospolop)


<iframe src='data:text/html,<script>fetch("/orders")</script>'></iframe>


csp default-src ‘unsafe-inline’ maxcdn.bootstrapcdn.com; object-src ’none’; script-src ’none’; img-src ‘self’ dummyimage.com

<iframe src="https://maxcdn.bootstrapcdn.com" sandbox="allow-scripts"></iframe>

We can write the csp tag since name is injected to head?



fred</title><meta http-equiv="Content-Security-Policy" content="default-src * 'unsafe-inline'; frame-src http://localhost:5001"></head><body><!--


--><iframe src="http://localhost:5001/orders" credentialless></iframe>

fred</title><meta http-equiv="Content-Security-Policy" content="default-src * 'unsafe-inline'; script-src *; frame-src http://localhost:5001"></head><body><!--


Judging the CSP again …

  • img-src ‘self’
  • the only endpoint without explicit setHeader(“Content-Type”…) is /orders

Can’t load it as CSS (default-src ! ‘self’)


Can’t seem to load it as SVG


Can’t seem to use base href for anything useful