はじめに
初めてのQiita記事です。
SECCON Beginners CTF 2021 に参加したので Writeup というものを書いてみます。
常設ではないCTFは去年の SECCON Beginners CTF 以来2回目、今回もチームは組まずソロ参加でした。
簡単なWeb問題だけでも解けたらいいなと思って参加してみましたが、Web4問・Misc3問・Crypto1問・Reversing2問が解けて結果は862点 943チーム中の191位でした。思ったよりできたので嬉しいです。
追記:Web以外を別記事に書きました。 https://qiita.com/tm741_/items/bace80869d5cee8c393f
Web問題Writeup
osoba
美味しいお蕎麦を食べたいですね。フラグはサーバの
/flag
にあります!
https://osoba.quals.beginners.seccon.jp/
想定難易度: Beginner
適当にリンクを踏むとクエリ変数にパスらしいものが出てきます。
https://osoba.quals.beginners.seccon.jp/?page=public/kikin.html
配布ファイルからflagファイルがあるのが分かるのでパストラバーサルでアクセスします。
https://osoba.quals.beginners.seccon.jp/?page=../flag
ctf4b{omisoshiru_oishi_keredomo_tsukuruno_taihen}
Werewolf
I wish I could play as a werewolf...
https://werewolf.quals.beginners.seccon.jp/
想定難易度: Easy
player.role == 'WEREWOLF'
になっていたらflagが出力されるのに、ランダムで選ばれるroleの値のリストにはWEREWOLF
が入っていません。
下のようにリクエストの変数を全部読み込んでしまう実装なので正しい変数名で送信すればいいのは分かりましたが、とりあえず __role=WEREWOLF
とか role=WEREWOLF
を送ってみてもうまくいかない。
if request.method == 'POST':
player = Player()
for k, v in request.form.items():
player.__dict__[k] = v
配布ソースの通りPlayer
クラスのオブジェクトを作って__dict__
を表示させることで、正しい変数名は_Player__role
であることがわかりました。
import random
class Player:
def __init__(self):
self.name = None
self.color = None
self.__role = random.choice(['VILLAGER', 'FORTUNE_TELLER', 'PSYCHIC', 'KNIGHT', 'MADMAN'])
testplayer = Player()
print(testplayer.__dict__) # {'name': None, 'color': None, '_Player__role': 'VILLAGER'}
Bodyを_Player__role=WEREWOLF
としたPOSTリクエストを送るとflagが返されます。
ctf4b{there_are_so_many_hackers_among_us}
mass assignment脆弱性というそうですが日本語訳の定訳を知りたいです。
check_url
Have you ever used
curl
?
https://check-url.quals.beginners.seccon.jp/
想定難易度: Easy
惜しいところまで行っていたようですが正答できませんでした。
$_SERVER["REMOTE_ADDR"]
が127.0.0.1になればflagが取得できるようです。
if ($_SERVER["REMOTE_ADDR"] === "127.0.0.1"){
echo "Hi, Admin or SSSSRFer<br>";
echo "********************FLAG********************";
}
$_SERVER["REMOTE_ADDR"]
はネットワーク層のIPアドレスが入っていてHTTPリクエストからは改変できないようなので、
SSRFって書いてあるのもヒントに、問題サーバのcurlを使って自身へのHTTPアクセスを試みます。
ただ、localhost
はブロックされ、127.0.0.1
はピリオドがSuper sanitizing👻されてしまうので渡せません。
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!");
}
IPアドレスの10進数表記があったことを思い出し、変換してみた2130706433
では400 Bad Request。
ほかに調べていてピリオドを使わない8進数表記017700000001
を見つけたもののこれも400 Bad Requestというところで挫折。
この記事に辿り着けなかったのが残念です。
作問者様Writeupより、正解は16進数表記の0x7F000001
だったそうです。
https://check-url.quals.beginners.seccon.jp/?url=https://0x7F000001/
ctf4b{5555rf_15_53rv3r_51d3_5up3r_54n171z3d_r3qu357_f0r63ry}
json
外部公開されている社内システムを見つけました。このシステムからFlagを取り出してください。
https://json.quals.beginners.seccon.jp/
想定難易度: Medium
ローカルネットワーク(192.168.111.0/24)内から閲覧する必要があるそうです。
bff\main.go
でClientIP()
のIPアドレスを見て制限をかけているようですが、go のClientIP()
は特に設定していないとHTTPリクエストのX-Forwarded-For
ヘッダで偽装できるのが分かりました。
リクエストにX-Forwarded-For: 192.168.111.1
ヘッダを追加して内部ページが表示されました。
Submitを押すとJSONをPOSTしますが、Flagを選択したときの{"id":2}
は禁止されています。
bffとapiでJSONに同じキーが2個あるときの解釈が違っているのを予想し(apiのソースは読んでなかった) {"id":2,"id":1}
を送信してみると正解。
{"result":"ctf4b{j50n_is_v4ry_u5efu1_bu7_s0metim3s_it_bi7es_b4ck}"}
cant_use_db
Can't use DB.
I have so little money that I can't even buy the ingredients for ramen.
🍜
https://cant-use-db.quals.beginners.seccon.jp/
深夜に見るラーメン画像、なんと攻撃力の高いことか…。
DBではなくuser_id別に作られるファイルでユーザーデータが保存されています。
普通に購入していくとbalanceが足りなくなりますが、ユーザーデータを読み込んでからファイルに書き込むまでの間はロックされていないのでデータ競合の問題があります。
1つリクエストを出したときのtime.sleep(random.uniform(-0.2, 0.2) + 1.0)
の間に残り2つのリクエストが出されるように、buy_noodles
を2回とbuy_soup
のリクエストを出せばいいことがわかります。
1秒前後も待ってくれるので特にコードを書かなくてもBuyボタンを続けて順番に押すだけで倍盛りラーメンを食べることに成功。
ctf4b{r4m3n_15_4n_3553n714l_d15h_f0r_h4ck1n6}
magic
トリックを見破れますか?
https://magic.quals.beginners.seccon.jp/
想定難易度: Hard
magic linkを上手く使ってcrawlerにXSSを踏ませる問題っぽいのは分かりましたが、CSPヘッダにscript-src 'self'
があるので普通にインラインスクリプトではダメというところから進まず解けませんでした。