Ricera CTF 2023 writeup

2023/4/22に行なわれたRicerca CTF 2023にdodododoで参加して、2位でした。

dodododoでは普段CTFに参加するときは、Google Docsにドキュメントを用意しておき、どの問題を解こうとしているかなどの進捗状況を共有できるようにしています。大したものはないのですが、せっかくなので中身を晒しつつ、writeupを書いていきます。

解いた問題

crackme

Google Docsの内容:

[solved] crackme
RicSec{U_R_h1y0k0_cr4ck3r!}

何も詳細は書いていません。warmup問題のようなやるだけの問題はflagを書いて終わっています。

N1pp0n-Ich!_s3cuR3_p45$w0rDとstrcmpしている部分を見かけたので、そのまま求められるパスワードとして入力したところ、flagが出力されました。

Cat Cafe

Google Docsの内容:

[solved] Cat Cafe
/img?f=..././flag.txt

RicSec{directory_traversal_is_one_of_the_most_common_vulnearbilities}

.replace("../", "")../を置き換えるようになっていますが、再帰的な置換ではないので...././../になるというものです。

BOFSec

Google Docsの内容:

[solved] BOFSec
b'A' * 0x101 + b'\n'

RicSec{U_und3rst4nd_th3_b4s1c_0f_buff3r_0v3rfl0w}

これもwarmup問題なので、かなり簡略に書いています。そのまま送信するだけです。

tinyDB

Google Docsの内容:

[solved] tinyDB
clearするタイミングでadmin自体のパスワードが********************************になる

RicSec{j4v45cr1p7_15_7000000000000_d1f1cul7}

デバッグの際に適当にconsole.logを仕込んでいると、userDB.sizeが10より大きくなったときに走る処理によって、adminのパスワードが********************************になることに気づきました。以下のコードを読んだときは単にレスポンスの内容にだけ影響するものと思っていましたが、Mapのkeyとしているauthの参照自体を書き換えているのでそのまま書き換えられてしまいますね。

  let auth = {
    username: username ?? "admin",
    password: password ?? randStr(),
  };
  if (!userDB.has(auth)) {
    userDB.set(auth, "guest");
  }

  if (userDB.size > 10) {
    // Too many users, clear the database
    auth.username = "admin";
    auth.password = getAdminPW();
    userDB.set(auth, "admin");
    auth.password = "*".repeat(auth.password.length);
  }

こういうタイプの問題は手元で動かしたらすぐにわかってしまうという意味で、package.jsonなど実際に動かすのに必要なファイルを配布していないのだろうと思うのですが、とはいえこれが本質ではないとは思うので他の問題のように、Dockerなりですぐ動く状態のものを配布してもらいたいところです……

NEMU

Google Docsの内容:

[solved] NEMU
reg自体の元々のサイズはint32_tだけど各命令ではuint64_tで読み書きするので4バイト分はみでるというバグ

https://gist.github.com/akiym/a4b816c93c3b201cca4ed35368e6f6e4

RicSec{me0w_i_am_n3mu_n3mu_c4tt0}

上に書いてあるバグを利用することで、add命令の先頭のコードを書き換えることができます。ただし、書き換えられるのは一部なので任意のコードを実行できるようにするにはバイト数が足りません。

スタック上にはacc, r3, r2と12バイト分並んだ、自由に操作できる部分があるのでそれらに対してシェルコードを読み込むstagerを仕込んでおきます。あとは書き換えたadd命令からその部分へジャンプすることで、自由にシェルコードを実行できるようになります。

tic tac toe?

Google Docsの内容:

[solved] tic tac toe?
いろいろ崩壊してるマルバツゲーム

  | a | b | c |
--|---|---|---|
1 | 0 | 3 | 6 |
--|---|---|---|
2 | 1 | 4 | 7 |
--|---|---|---|
3 | 2 | 5 | 8 |
--|---|---|---|

盤面からfork-exitでexitcodeとして何か計算してチェックしてるっぽい

https://gist.github.com/akiym/037b9347ff6e43f17ca373daf730a728
mainから0x1590のところはこういうかんじだと思ったのだけどunsat

RicSec{t1c_t4c_t03_1s_3x1t1ng_g4m3}

前半部分はチームメンバーが書いていて、後半のgistのURLを貼っているところから自分が書いています。

gistのrevisionsを見ると、最初は解析した結果をz3のスクリプトに落とし込んでいるところが間違っています。途中でexit codeって8bitだな、とか細かいミスに気づいて直したところsatだったので、flagを求める処理に突っ込んで終わりです。

funnylfi

Google Docsの内容:

[solved] funnylfi
f!ile:// みたいなかんじでscheme_detectorはbypassできるけど、RicSecのWAFがある

gopher protocol + uwsgiで何かと思ったけど、_が消されてしまうのでUWSGI_FILEを作れない
というか%が使えないんだった

?url=˚f!ile://«/var/www/flag˚

競技時間もほぼ終盤になった頃、チームメンバーから˚を使うとコマンド内にスペースを含められることを教えてもらいました。レスポンス中にRicSecが含まれると怒られる(flagはRicSec{...}という形式なので、flagをそのまま出力できない)ので、解法的にはRangeのリクエストだろうと推測して、curl-rオプションを使う方法はないかを探しました。

curl-rオプションは通常であれば-r 0-100のように使いますが、-r0とした場合でもwarningは出るものの-r0-と同様に動くことがわかります。つまり、引数の中に-r2のようにオプションを指定することができれば、RicSecが含まれる先頭部分を捨ててflagを出力させることができるはずです。

色々試したところ、b'xn-- file:///var/www/flag -r2a'.decode('idna')の結果が' file://«/var/www/flag 'だったので、˚f!ile://«/var/www/flag˚を入力したところb'xn-- file:///var/www/flag -r2a476lwa'と変換され、無事に-r2が指定できました。