SECCON CTF 2022 Finals
My CTF team, DiceGang, qualified for SECCON'22 finals and I got the chance to go to Tokyo to play in the finals on-site. We ended up winning first place out of the 10 teams there!
Here are some challenge writeups:
babybox
web (? solves)
In this challenge, you essentially had to get RCE via the latest version of the nodejs package expr-eval.
There were no known vulnerabilities or Github issues for code execution, so this was a 0day!
However, scrolling through issues, we do see an interesting one:
Which shows us that we can get a reference to Object
via constructor
, and call methods on Object
!
Using other useful functions on Object
, we can create a new Function
and call it.
From here, we can essentially eval()
anything, and can easily create a reverse shell using other NodeJS RCE payloads.
easylfi2
web (? solves)
In this challenge, we need to read the /flag.txt
file on the FS. The only endpoint gives us arbitrary file read via curl'ing a file://
URI, but there's a middleware in place that censors the output if /SECCON{\w+}/
is in the output.
This challenge left me stumped for a while, I thought we needed to find a way to get curl to encode the output somehow s.t. SECCON
wouldn't appear in plaintext.
Eventually, I started to focus on the error handling. If execFile
threw an error, a JSON object of the error is sent as a response:
{"code":37,"killed":false,"signal":null,"cmd":"curl file:///app/public/../../wtf","stdout":"","stderr":"curl: (37) Couldn't open file /wtf\n"}
The relevant fields in the error object were code
,stdout
,stderr
. After thinking about it, I started to wonder what happens if we make a very large stdout or stderr – would it be truncated at all?
The regex only censors the output if there's a closing brace as well, so if we can get it to truncate, it won't be censored!
It turns out, if execFile
produces a large STDOUT, it throws an error code ERR_CHILD_PROCESS_STDIO_MAXBUFFER
. So, we simply need to get curl to fill STDOUT with garbage, add the flag to the end, such that the closing flag brace goes past the MAXBUFFER size.
To fill STDOUT with garbage, we can use large existing files on the system. etc/ca-certificates.conf
ended up working well.
python3 -c 'print("GET /../../{" + "etc/ca-certificates.conf,"*183+"a"*1385+"/../flag.txt} HTTP/1.0\r\n")' | nc 127.0.0.1 3000
light-note
web (0 solves)
I unfortunately didn't solve light-note during the CTF, but figured it out shortly after.
light-note is a simple note taking app. A user can create and delete notes, and notes are stored in the user's session server side. There's an admin bot that creates a note with the flag in it, then visits the provided URL.
The overall exploitation path in this challenge was fairly obvious, but figuring out how to exploit it was difficult. The admin is vulnerable to CSRF, so we can use that to create notes, and we need to gain XSS to read and exfiltrate the admin's notes.
In the webpage (index.html), the following code is used to load and render notes.
noteTmpl
is a template tag, which contains an HTML template for a note:
The first bug I noticed – in the Safari fallback for write()
, the .replace()
call does not have the /g
flag, so only the first match is replaced. So, I figured that we need to create a note s.t. the HTMLSanitizer API and DOMPurify both throw errors.
I spent a lot of time and trial and error on this, but I couldn't find any way to get either HTMLSanitizer
or DOMPurify
to throw an error on controlled input.
Ultimately, I realized we can cause the first try-catch to throw an error by utilizing DOM clobbering!
Also, we can avoid needing to get DOMPurify to throw an error, because DOMPurify's default configuration allows for <style>
tags, and we can use CSS-based text exfiltration to retrieve the first note (font ligature technique)
Figuring out exactly how to DOM clobber setHTML
here was still difficult, because we can't have nested <form>
tags, and we need another nested property access inside of .note
. The solution was to use the form=
attribute to avoid putting any element inside <form>
, but still clobber $(noteTmpl).content
Now, .setHTML()
will throw an error because it's been clobbered with undefined
and is not a function, so our next note will be processed by DOMPurify instead.
Full solution:
IRL Pics
Tokyo was a lot of fun! here's some pictures