SECCON Beginers CTF 2020 Writeup
久しぶりの投稿です。よもぎです。
SECCON Beginers CTF 2020に参加しました。今回はソロです。 あまりにも久しぶりだったので解ける気がしなかったのですが、Welcome問題含めて運良く10問解けました。
取り急ぎになりますが、Writeupを書いていこうと思います。
随時更新予定です →ひとまず完成です
Windowsで取り組んでみたんですけど、wsl*1もあって結構行けるんですね
Web
spy
flaskを使ったコントローラーのPythonファイルが配られた。 一部を見てみると、
def index(): t = time.perf_counter() if request.method == "POST": name = request.form["name"] password = request.form["password"] exists, account = db.get_account(name) if not exists: return render_template("index.html", message="Login failed, try again.", sec="{:.7f}".format(time.perf_counter()-t)) # auth.calc_password_hash(salt, password) adds salt and performs stretching so many times. # You know, it's really secure... isn't it? :-) hashed_password = auth.calc_password_hash(app.SALT, password) if hashed_password != account.password: return render_template("index.html", message="Login failed, try again.", sec="{:.7f}".format(time.perf_counter()-t)) session["name"] = name return render_template("dashboard.html", sec="{:.7f}".format(time.perf_counter()-t))
こんな感じで
- まずユーザーが存在するか確認
- 存在したらパスワードをハッシュにかける*2
- ハッシュされたパスワードとDBを照合
いわゆる タイミング攻撃 が成立しそうなので、一人ひとりの経過時間をみる。 利用していないユーザーは~1msなのに対して、利用しているユーザーは0.3~0.7sくらいと大きく差がある。
結局利用していたユーザーは、Elbert,George,Lazarus,Marc,Tony,Ximena,Yvonneで
ctf4b{4cc0un7_3num3r4710n_by_51d3_ch4nn3l_4774ck}
tweetstore
※この問題は非想定解で解いてしまいました
DBのユーザー名がflagとなっている問題。 与えられたファイルには次のようにSQL文を生成しています。
var sql = "select url, text, tweeted_at from tweets" search, ok := r.URL.Query()["search"] if ok { sql += " where text like '%" + strings.Replace(search[0], "'", "\\'", -1) + "%'" } sql += " order by tweeted_at desc" limit, ok := r.URL.Query()["limit"] if ok && (limit[0] != "") { sql += " limit " + strings.Split(limit[0], ";")[0] }
WHERE句でSQLiできないか考える。'
を\\'
でエスケープするようにしてしまっているので、
LIKE句を終わらせられない。
strings.Replace()
を回避できないかなぁと考えていたら、\
を'
につけてあげればエスケープが前倒しになって'
が効くようになると思いつく。
試してみると、
%\' --
-> 0件ヒット%\' union select url, text, tweeted_at from tweets --
-> たくさんヒット
でいい感じになってそう。
3カラム目はgo上でtime.Time
型に指定されているので単純な文字列ではいけない。
これはtweets
テーブルのtweeeted_at
を使いまわそうと思った。*3
%\' union select url, usename, tweeted_at from tweets, pg_user --
これをsearchクエリにセットしてでてきた
ctf4b{is_postgres_your_friend?}
unzip
Hintとして配られたdocker-compose.yml
を見てみると
volumes: - ./public:/var/www/web - ./uploads:/uploads - ./flag.txt:/flag.txt
この/flag.txt
を見たい
一方で、配られたindex.php
を見てみると、次のような感じみたいだった
- zipファイルをアップロードすると解答してくれる
- ただしzipファイルは1000バイト以下じゃないといけない
- リンクを作ってくれる。クリックしたらそのファイルにアクセスできる
- 作業ディレクトリは
/uploads/<session_id()>/
ぱっと見でファイル名のバリデーション見ていないので、そのあたりで行けそうかな?ってなる。
相対パス的には../../flag.txt
を見たいので、
次のような構成のフォルダを作ってzip圧縮する
zz - zz - flag.txt
これをzip圧縮したあとにバイナリエディタで開いて、適当にzzを..に変えてあげると、
../../flag.txt
へのリンクが出来上がる
ctf4b{y0u_c4nn07_7ru57_4ny_1npu75_1nclud1n6_z1p_f1l3n4m35}
crypto
R&B
暗号化が、
- FORMAT文字列から1文字取り出す
- 1.でRがでたらROT13をして先頭にRを足す
- 1.でBがでたらBASE64Encodeをして先頭にBを足す
- 1.から繰り返す
なので復号するときには、
- 暗号文の先頭1文字を取り出す
- 1.でRがでたら2文字目以降をROT13する
- 1.でBがでたら2文字目以降をBASE64Decodeする
- 1.から繰り返す
pythonで書いてみたらこんな感じ?
import codecs import base64 CRYPTO = 'BQlVrOUllRGxXY2xGNVJuQjRkVFZ5U0VVMGNVZEpiRVpTZVZadmQwOWhTVEIxTkhKTFNWSkdWRUZIUlRGWFUwRklUVlpJTVhGc1NFaDFaVVY1Ukd0Rk1qbDFSM3BuVjFwNGVXVkdWWEZYU0RCTldFZ3dRVmR5VVZOTGNGSjFTMjR6VjBWSE1rMVRXak5KV1hCTGVYZEplR3BzY0VsamJFaGhlV0pGUjFOUFNEQk5Wa1pIVFZaYVVqRm9TbUZqWVhKU2NVaElNM0ZTY25kSU1VWlJUMkZJVWsxV1NESjFhVnBVY0d0R1NIVXhUVEJ4TmsweFYyeEdNVUUxUlRCNVIwa3djVmRNYlVGclJUQXhURVZIVGpWR1ZVOVpja2x4UVZwVVFURkZVblZYYmxOaWFrRktTVlJJWVhsTFJFbFhRVUY0UlZkSk1YRlRiMGcwTlE9PQ==' def defrot13(s): return codecs.decode(s, 'rot13') def defbase64(s): d = base64.b64decode(s) return d.decode('utf-8') while True: if CRYPTO[0] == 'B': CRYPTO = defbase64(CRYPTO[1:]) elif CRYPTO[0] == 'R': CRYPTO = defrot13(CRYPTO[1:]) else: print(CRYPTO) break
結果は
ctf4b{rot_base_rot_base_rot_base_base}
RSA Calc
とりあえずサーバーにアクセスすると、Nは知ることができる。*4
Execで入力したいデータは1337,F
だけど、SignしたいデータにFもしくは1337が含まれるとエラーになるようになっている。
今回のRSA署名ではmod N
をとっているので、入力するdataに関する署名はNに関して合同となる。
1337,F
を整数に直してNを足したやつに署名すれば、Signのバリデーションを回避しつつ1337,F
に対する署名を得られる。
pythonで(変数名は適当にしてしまった)
from netcat import Netcat N = 1044524947292(中略)5515759550956133 a = int.from_bytes(b'1337,F', byteorder='big') sign = a+N byte_sign = sign.to_bytes(256, byteorder='big') nc = Netcat('host server url', port) b = nc.read_until('>') print(b) nc.write('1'+'\n') c = nc.read_until('>') print(c) nc.write_bytes(byte_sign) nc.write('\n') d = nc.read() print(d) nc.close()
これで有効な署名を得られるので、あとはExecで入力してあげて、
data> 1337,F
signature> 4e06f717e1724(中略)4e985d15
ctf4b{SIgn_n33ds_P4d&H4sh}
Error
Noisy Equations
与えられたファイルの一部はこんな感じ
def dot(A, B): assert len(A) == len(B) return sum([a * b for a, b in zip(A, B)]) coeffs = [[getrandbits(L) for _ in range(N)] for _ in range(N)] seed(SEED) #SEEDは環境変数に格納されている=固定値 answers = [dot(coeff, FLAG) + getrandbits(L) for coeff in coeffs] print(coeffs) print(answers)
求めたいFlagベクトルをf、各成分が乱数で決まる係数正方行列をC、各成分が乱数となる雑音ベクトルをn、得られる出力をaとすると
\begin{align} C \cdot \boldsymbol{f} + \boldsymbol{n} = \boldsymbol{a} \end{align}
このうち、Cとaが出力されているものの、乱数で決まるCとnでaがばらばらになってしまい、メチャクチャな値になる。 しかし、実はnを乱数で生成する前にいつも同じ値をSEEDに入れる処理があるので、nは一定値となっている。 ここから、2回アクセスして2つの出力を比較することで、連立一次方程式の問題になる
\begin{align} C_1 \cdot \boldsymbol{f} + \boldsymbol{n} &= {\boldsymbol{a}}_1 &\cdots (1) \\ C_2 \cdot \boldsymbol{f} + \boldsymbol{n} &= {\boldsymbol{a}}_2 &\cdots (2) \\ \left( C_1 - C_2 \right) \cdot \boldsymbol{f} &= \left( {\boldsymbol{a}}_1 - {\boldsymbol{a}}_2\right) &\cdots (1) - (2) \end{align}
よって、
\begin{align} \boldsymbol{f} = \left( C_1 - C_2 \right)^{-1} \cdot \left( {\boldsymbol{a}}_1 - {\boldsymbol{a}}_2\right) \end{align}
で求められたりする。
numpyだと値が大きすぎてだめみたいで、np.linalg.solve()
とかnp.linalg.inv(),np.dot()
とかでで解こうとしたら
TypeError: No loop matching the specified signature and casting was found for ufunc solve1
というエラーが出てだめでした……*5*6
代わりにmpmathというパッケージを使いました
from mpmath import * # 得られた値たち # 数字は80桁程度 C_list0=[[8239...(略)]] # 44x44の2次元配列 a_list0=[235..(略)] # 長さ44 C_list1=[[3582...(略)]] a_list0=[233..(略)] C0 = matrix(C_list0) C1 = matrix(C_list1) a0 = matrix(a_list0) a1 = matrix(a_list1) C_sub = C0 - C1 a_sub = a0 - a1 f = lu_solve(C_sub, a_sub) print(f)
floatなので数値に誤差があるけれども、fの各成分は整数になっていて、 それをasciiコードとして読んであげる。
list = [99,116,102,52,98,123,114,52,110,100,48,109,95,53,51,51,100,95,49,53,95,110,51,99,51,53,53,52,114,121,95,102,48,114,95,53,51,99,117,114,49,55,121,125] for i in list: print(chr(i), end="")
ctf4b{r4nd0m_533d_15_n3c3554ry_f0r_53cur17y}
Reversing
mask
とりあえずいつものようにfile
とかstrings
でやったりすると、怪しい文字列を見つける
atd4`qdedtUpetepqeUdaaeUeaqau c`b bk`kj`KbababcaKbacaKiacki
実際にmask
ファイルを実行すると、flagとなる引数を1つとって、
その引数から生成される文字列を2つ出力し、正誤判定をするみたいな感じっぽい。
とりあえず引数に英数字や記号を一通り入力してみる
abcdefghijklmnopqrstuvwxyz1234567890{}_ -> a`adede`a`adedepqpqtutupqp1014545010quU abc`abchijkhijk`abc`abchij!"# !"#() kiK
ここからさっきの怪しい文字列を縦に一つずつ見ていくとflag文字列
ctf4b{dont_reverse_face_mask}
を得る。
$ ./mask ctf4b{dont_reverse_face_mask} Putting on masks... atd4`qdedtUpetepqeUdaaeUeaqau c`b bk`kj`KbababcaKbacaKiacki Correct! Submit your FLAG.
yakisoba
file
とかstrings
でやったけど、いい情報は得られず。
IDA freeで開くと、判定部分が見つかった。
いろいろ複雑な判定をしている部分はこんなかんじ
これ最初手で追おうかと思ったけれども、問題文には「自動解析つかうのおすすめ」みたいなこと書いてあったし、 同じようにソロで参加していた友人にangrの存在を教えてもらう。
ので、angrを試してみようとしたら色々エラー起きてぴえんになってた。 *7
分岐を見て、0x6D2
に到達してほしくて、0x6f7
は避けてほしいので、
次のようなソルバを書きます。
import angr p = angr.Project("/pass/to/yakisoba") state = p.factory.entry_state() sim = p.factory.simulation_manager(state) sim.explore(find=(0x400000+0x6D2,), avoid=(0x400000+0x6f7,)) if len(sim.found) > 0: print(sim.found[0].posix.dumps(0))
angrで解かせたらすぐだったの笑っちゃった
WARNING | 2020-05-23 23:25:38,609 | cle.loader | The main binary is a position-independent executable. It is being loaded with a base address of 0x400000.
b'ctf4b{sp4gh3tt1_r1pp3r1n0}\x00\xd9\xd9\xd9\xd9'
よって、
ctf4b{sp4gh3tt1_r1pp3r1n0}
etc
welcome
ディスコードサーバーのanncounementチャンネルのトピックにあった
運営からのアナウンスを行うチャンネルです
ctf4b{sorry, we lost the ownership of our irc channel so we decided to use discord}
emoemoencode
1wordごとに0xF09F8C80
を引いて上げれば、Ascii codeとなっていい感じになったと思う
使ってるHexEditorで減算しようとしたら、お前のライセンスじゃむりでーすってなったので、力技でやった。 別にプログラム書いても良かったと思うけどGuessingできるし、まあいいでしょう
ctf4b{stegan0graphy_by_em000000ji}
参考URLはこちら
追記:こんな感じ差を求めればよかったんですね……
diff = ord('🍡') - ord('a')
readme(unsoloved)
/home/ctf/flag
を見たいけど、入力にctf
の文字が含まれていると拒否されちゃうので、
うまく/home/ctf/
にたどり着けるような方法を探したい。
最初は定番ファイルの/etc/passwd/
をあたってみたら、最終行に
ctf:x:1000:1000:Linux User,,,:/home/ctf:/bin/ash
とあったのでこれをうまく使って、
/home/"+exec(tail -n 3 /etc/passwd | head -c 3)+"/flag
な感じで行きたいと思ってた。
けど多分無理*8
/proc/self/cmdline
*9や/proc/self/cwd
*10をあたったけど、
この問題サーバー側がエラーメッセージをたまにしか返してくれなくてよくわからずに終わってしまった。
/proc/self/
を調べようとしたきっかけはここのADMIN UIの項
終わりに
一人で参加してみましたが、去年は2問しか解けなかったのに比べると少しは成長しているのかな? 未だにBasicレベルめう……
*1:wsl2ではないです、2020 May Update早く来てほしい
*2:しばしば時間がかかる
*3:NOW()でよかったらしい
*5:他にもだめだったという人いた
*6:大丈夫だった人もいた
*7:wslのUbuntuが16.04だったのでPython3.5までしかなく、angrが依存するパッケージ内の関数でenumにintFlagのAttributionないけどっていうエラーが出てangrが動かなかった。wslのUbuntuを18.04にすることで、python3.6になって解決した。この問題を解くためにOSをアップデートしたとゆこと
*8:openはbyteかstrをpath-like objectとみて開くから違うっぽいとなった
*9:レスポンスはpython3./server.py
*10:実はこれ惜しかったっぽい