よもぎのメモ帳

備忘録的な感じで技術的なことをストックしていきます。

SECCON Beginers CTF 2020 Writeup

f:id:y0m0g1:20200524141312p:plain
社畜ちゃん台詞メーカーより

久しぶりの投稿です。よもぎです。

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へのリンクが出来上がる

f:id:y0m0g1:20200524175411p:plain
一番下に出来てる

ctf4b{y0u_c4nn07_7ru57_4ny_1npu75_1nclud1n6_z1p_f1l3n4m35}


crypto

R&B

暗号化が、

  1. FORMAT文字列から1文字取り出す
  2. 1.でRがでたらROT13をして先頭にRを足す
  3. 1.でBがでたらBASE64Encodeをして先頭にBを足す
  4. 1.から繰り返す

なので復号するときには、

  1. 暗号文の先頭1文字を取り出す
  2. 1.でRがでたら2文字目以降をROT13する
  3. 1.でBがでたら2文字目以降をBASE64Decodeする
  4. 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}

このうち、Caが出力されているものの、乱数で決まるCnaがばらばらになってしまい、メチャクチャな値になる。 しかし、実は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で開くと、判定部分が見つかった。

f:id:y0m0g1:20200524165406p:plain
main関数内

いろいろ複雑な判定をしている部分はこんなかんじ

f:id:y0m0g1:20200524165558p:plain
めっちゃ複雑

これ最初手で追おうかと思ったけれども、問題文には「自動解析つかうのおすすめ」みたいなこと書いてあったし、 同じようにソロで参加していた友人に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の項

qiita.com


終わりに

一人で参加してみましたが、去年は2問しか解けなかったのに比べると少しは成長しているのかな? 未だにBasicレベルめう……

f:id:y0m0g1:20200524175915p:plain

*1:wsl2ではないです、2020 May Update早く来てほしい

*2:しばしば時間がかかる

*3:NOW()でよかったらしい

*4:309桁の自然数

*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:実はこれ惜しかったっぽい