picoCTF 2023 WriteUps
今年 picoCTF 也是 solo 參與,只有前面第一兩天挑了些分數較高的題目來解解,後面就都沒碰了。
General Skills
Special
是一個 python 程式,它會對你的輸入做一些未知的處理之後送進
os.system
,不過因為沒 source code
就只能亂試而已。我的解法是輸入 a;`cat`
之後輸入 bash,之後
Ctrl-D
就拿到 shell 了。之所以能這樣做是因為題目都是用 ssh
連線的,所以有 tty 能讓我送 EOF。
我拿到 Flag 是
picoCTF{5p311ch3ck_15_7h3_w0r57_0c61d335}
,然後也順便把題目的
source code 抓了下來:
1 | #!/usr/bin/python3 |
Specialer
這題 ssh 上去後是個 bash shell,但是 ls
等等的指令都執行不了,可以猜測說大概是 binary 都被刪除了,只剩下
/bin/bash
而已。不過這個情況我很熟悉,因為它很類似我曾經出過的另一題 Free
Shell,但是那題困難很多。
不過核心概念就是怎麼只用 bash builtin 的功能做 ls
和
cat
的工作而已。echo *
可以讓你列出當前目錄下的檔案,也能結合 glob
做很多不同的事。而這題檔案很多,不確定 flag 在哪,所以用個 loop 把所有
glob 能 match 的檔案都用 echo $(<$file)
看看有沒有 flag
就好了:
1 | for f in **/*; do echo $(<$f); done |
Flag:
picoCTF{y0u_d0n7_4ppr3c1473_wh47_w3r3_d01ng_h3r3_d5ef8b71}
Web Exploitation
Java Code Analysis!?!
這題是個從這個改來的
spring boot 網頁,讀一下 source code 可以在 SecretGenerator
看到:
1 | private String generateRandomString(int len) { |
所以 jwt secret key 固定是 1234
,那麼 sign 個 admin jwt
就能拿到 flag 了:
1 | { |
msfroggenerator2
這題架構是有個 openresty (nginx) server 在最外層,然後中間經過 traefik 之後後面有 api 和 bot 兩個 backend。
nginx config:
1 | server { |
traefik:
1 | http: |
而 /var/www
那下面有個靜態網站,上面會 call 一些
/api/
的 api,不過簡單讀過之後會發現似乎根本沒辦法
XSS。不過這題另一個特別可疑的地方就是為什麼要用兩個 reverse
proxy,這部分其實和 bot 有關:
1 | import puppeteer from 'puppeteer'; |
還有
1 | import { createServer } from 'http'; |
從 nginx 那邊我們知道 /report
會把我們傳入的
id
參數變成
url=http://openresty:8080/?id=$id
,所以 bot 收到的 url
一定是 http://openresty:8080/
的對吧? 然而 traefik 在判斷
query string separator 的時候還會考慮分號 ;
,而在
2.7.2
版本之後還會直接把 ;
normalize 成
&
。 (ref: traefik issue
#9164, source)
所以只要讓 id
變成
;id=another_url
,那麼根據 new URL
出現重複參數會取後者的性質,another_url
就會直接進入
page.goto(url)
,中途沒有經過任何的檢查,所以我們可以塞
javascript:...
達成 XSS。
1 | base=http://saturn.picoctf.net:64716 |
不過後來和作者聊過之後發現前半正確,但 javascript:
不是
intended XD,正確解法是利用 chrome 強制下載的功能(這也是 bot 非 headless
的原因)可以讓檔案出現在 /root/Downloads/xxx.html
,然後覆蓋
fetch
後就能攔截到 flag 了。
作者原本預期 CSP 會擋住 javascript:
的,但 chrome
似乎會允許 page.goto
(等價於 user 自己在網址列輸入)
通過的樣子,不管 CSP。
cancri-sp
這題看起來就像是 browser pwn,因為題目給了一個 patch 過的 chromium 還有一些 mojo 方面的 C++ code,但我用 unintended 解了 XDDD。
它執行 bot 的 shell script 長這樣:
1 | set -eux |
而 server 是直接把你給的 url 原封不動的當 argv 傳入:
1 | var express = require('express'); |
不過有寫過 shell script 的人應該都知道把 variable 包在引號裡是非常重要的一件式,不然 shell 會自動對空白分割當多個 argv 傳入,細節請參見 Security implications of forgetting to quote a variable in bash/POSIX shells。
所以我們這邊只要讓 url
有空白就能對 chrome
做 argument injection,而查一下可以知道有很多 --no-sandbox
--disable-gpu-sandbox
--gpu-launcher
--renderer-cmd-prefix
等等的參數可以拿
RCE,但我這邊只能讓它執行 binary 而已,沒辦法控到參數。
不過我這邊就換了個做法,用了
--disable-web-security --remote-debugging-port=9222 --remote-allow-origins=* --headless=new
讓我能直接打 Chrome DevTools Protocol 去讀目錄並和讀 flag。
(--headless=new
好像是因為有遇到一些行為不同的問題才加的)
1 | <script> |
附註: 其實只用 Chrome DevTools Protocol 也是有機會拿 RCE 的,參考 ASIS CTF 2022 - xtr
Cryptography
SRA
1 | from Crypto.Util.number import getPrime, inverse, bytes_to_long |
這題的 RSA 給你了
我的做法是
1 | from pwn import process, remote |
PowerAnalysis: Warmup
1 | #!/usr/bin/env python3 |
這題會把你輸入的 message
因為這邊其實各個 byte 是可以分開討論的,所以這邊我們先假定要找的只是
key 的第一個 byte 而已。我的作法是先隨機送一些
1 | from pwn import context, process, remote |
PowerAnalysis: Part 1 / Part 2
這兩題其實很類似,不過第一題是允許你選擇 AES plaintext 然後得到目標的 power traces,而第二題只給你這種格式的 txt 而已:
1 | Plaintext: 78695fc56ec9de44bf6dabdc6e264760 |
因為我這題其實是先解第二題的,所以就寫個腳本隨機生成一些 plaintext 然後得到指定的 power traces,然後弄成格式一樣的 txt 就能一次解兩個了:
1 | from pwn import remote |
總之這題沒有 source code,不過 hint 有說
The power consumption is correlated with the Hamming weight of the bits being processed
,所以明顯是
Simple Power Analysis。
這邊的概念其實和 wramup 很類似,不過這邊是使用
1 | from pathlib import Path |
後來我才知道其實還有個 scared 的 library 可以幫你自動做這類的 side channel analysis,詳情可參考這篇 writeup。
Binary Exploitation
hijacking
ssh 上去 sudo -l
看到:
1 | User picoctf may run the following commands on challenge: |
所以 sudo vi
之後 ESC
再
:!/bin/sh
就可以拿到 root shell 了,而 flag 在
/root/.flag.txt
之中:
picoCTF{pYth0nn_libraryH!j@CK!n9_f56dbed6}
不過從 flag 可知它顯然不是 intended,正確做法是利用那個
.server.py
:
1 | import base64 |
python 預設會從 cwd 找 module,所以我們可以在 cwd 放一個
base64.py
裡面跑 shell 就能拿到 root 了。
tic-tac
ssh 上去可看到有個 suid binary,source code:
1 |
|
可以知道它在檢查 owner 和實際上讀取的時候有時差,所以可以利用 toctou 去讀取只有 root 才能碰的 flag。
1 | { while true; do ln -sf flag.txt lnk; ln -sf hello.txt lnk; done } & |
VNE
這題有個 binary,scp 下來反編譯可以看到這段 code:
1 | v14 = getenv("SECRET_DIR"); |
所以它會執行 system("ls" + SECRET_DIR)
,所以可以 command
injection 拿 root shell:
SECRET_DIR='/challenge;sh' ./bin
另一個方法是利用 ls
是用 relative path 呼叫的特性,而
suid binary 又不像 sudo 會幫你把一些危險的環境變數如 PATH
清掉,所以可以 PATH=/tmp
然後裡面放個 ls
的
script 也能拿 root shell。
Horsetrack
1 | [*] '/home/maple3142/workspace/pico2023/horsetrack/vuln' |
這題是很標準的 heap pwn,主要的洞在於有
UAF,然後它自訂的讀字串函數在遇到 \xff
時就會
return,所以讓它不會覆蓋掉 heap pointer 就能拿到 heap leak。
打法其實就很標準的 tcache poisoning,不過因為這題因為 glibc 版本是
2.33,所以還要繞過 safe linking。拿到 heap leak 和任意寫之後結合 No PIE
和 Partial RELRO 可知能寫掉 GOT,而對應了 binary 的一些操作我決定先寫
sh
到 bss 的某處,然後讓 stderr="sh"
並複寫
setbuf@GOT
到 resolve system 的地方,然後同時把
printf
覆蓋成呼叫 setbuf(stderr, ...)
的地方,這樣就有 shell 了。
1 | from pwn import * |
這題其實有個比較坑的地方是 remote 會把你輸入的東西 echo 回來,並且用
\r\n
而非 \n
...。就其他人所說這是因為 remote
用 socat 的 pty mode 導致的結果...。