#ctf4b 2023
SECCON Beginners 2023
全778チーム中191位(505pt)でした。
解けたやつだけの Write UP です。(CTF 歴 8ヶ月の限界)
✅:自分で解いたもの
💠:チームの人が解いたもの
🏳️:割と考えたけど解けなかったもの
Category | Challenges | Solved |
---|---|---|
crypto | CoughingFox2 | 💠 |
crypto | Conquer | ✅ |
pwnable | poem | ✅ |
misc | YARO | ✅ |
misc | polyglot4b | 🏳️ |
web | Forbidden | ✅ |
web | aiwaf | ✅ |
web | phisher2 | 🏳️ |
reversing | Half | ✅ |
welcome | Welcome | 💠 |
解けた問題
Conquer
-
length
はflag
のビット長 -
key
はflag
と同じビット長のランダムな値 -
cihper
=flag XOR key
-
ROL(bits, N)
とcipher = cipher XOR key
を32 回繰り返す -
ROL(bits, N)
はbits = abcdef
,N=6
の場合、(a-f = {0, 1}
)-
(bits << 1) = abcdef0
(1 ビット左に) -
2**length - 1 = 111111
(1 が N 個) -
bits >> (length - 1) = a
(最上位ビットだけ) なので -
bits = bcdefa
と左に 1 ビット巡回シフトしてるはず
-
これより、flag
の長さだけわかれば全て逆順の操作をすると flag
を出せるはず...
だけどわからないのでとりあえずローカルで試してみる。
def decrypt(length):
# length = flag.bit_length()
key = 364765105385226228888267246885507128079813677318333502635464281930855331056070734926401965510936356014326979260977790597194503012948
cipher = 92499232109251162138344223189844914420326826743556872876639400853892198641955596900058352490329330224967987380962193017044830636379
for i in range(32):
cipher ^= key
# Reverse ROL
for _ in range(pow(cipher, 3, length)):
low = key & 0b1
high = low << (length - 1)
key = (key >> 1) | high
flag = cipher ^ key
print(hex(flag))
return hex(flag)
# main()
for i in range(1, 500): # i is the length of flag.
print(decrypt(i))
ctf4b{
は HexString で 0x63746634627b
だからこれから始めるやつを探す。
...
0x1ee93f2ed5a6a18e8937b056f1c4e67abcd4adb3f6b80cf1a382cce01c602514dc54a018da71083629f03d51fde6114dc0a432752a5978
0x1ee93f2ed5a6a18e8937b056f1c4e67abcd4adb3f6b80cf1a382cce01c602514dc54a018da71083629f03d51fde6114dc0a432752a5978
0x63746634627b53656d69434952434c457243616e616c73486176654265656e436f6e7175657265644279546865434952434c452121217d
0x63746634627b53656d69434952434c457243616e616c73486176654265656e436f6e7175657265644279546865434952434c452121217d
0x976e990c74598678799961485336b0d6384ff98ea091b46b6e61e97b8e91fbf9ed246eeb410d49bc778adcec167ee5c34d3b3760794e36
0x976e990c74598678799961485336b0d6384ff98ea091b46b6e61e97b8e91fbf9ed246eeb410d49bc778adcec167ee5c34d3b3760794e36
...
decode してctf4b{Semi...}
になる。
poem
プログラムを見る限り、0-4 を入力すると配列の対応したところに格納されている文章を出すだけっぽい。flag
は別の値として格納されてるけど C 言語の変数の格納アドレスってどうだっけ…?
考えるより手探りで 6 とか -1 とか入れてみる方が早そうなので順に入れていくと...
% nc poem.beginners.seccon.games 9000
Number[0-4]: -4
ctf4b{y0u_...}
YARO
まず実行してみる
% nc yaro.beginners.seccon.games 5003
rule:
【Enter を入力】
OK. Now I find the malware from this rule:
Not found: ./redir.sh
Not found: ./server.py
Not found: ./flag.txt
Not found: ./requestments.txt
問題文は「サーバーにマルウェアが混入している可能性があるので、あなたの完璧なシグネチャで探してください」
YARA というツールがあるらしいので調べながら書いてみる。おそらく入力した YARA のルールに基づいて 4 つのファイルを調べてるっぽい?
rule find_flag {
strings:
$s = /ctf4b\{[\w_]*/
condition:
$s
}
を入力してみると
% nc yaro.beginners.seccon.games 5003 < yara0.txt
rule:
OK. Now I find the malware from this rule:
rule find_flag {
strings:
$s = /ctf4b\{[\w_]*/
condition:
$s
}
Not found: ./redir.sh
Not found: ./server.py
Found: ./flag.txt, matched: [find_flag]
Not found: ./requestments.txt
$s = /ctf4b\{[\w_]*/
の部分をなんとかすれば良さそう。
$s = /ctf4b\{[A-Z]/
にして実行すると Found になるから最初の文字は大文字のアルファベット。あとは[A-M]に分けたり(2分探索)を繰り返しで (実質ブルートフォースで) 突き詰めていくと
rule find_flag {
strings:
$s = /ctf4b\{Y3t_...(実際のファイルには記載)\}/
condition:
$s
}
% nc yaro.beginners.seccon.games 5003 < yara.txt
rule:
OK. Now I find the malware from this rule:
rule find_flag {
strings:
$s = /ctf4b\{Y3t_...\}/
condition:
$s
}
Not found: ./redir.sh
Not found: ./server.py
Found: ./flag.txt, matched: [find_flag]
Not found: ./requestments.txt
Forbidden
flag
は https://forbidden.beginners.seccon.games/flag
にあるらしいけど、見てみると
Forbidden :(
と…
プログラムを見ると
var express = require("express");
var app = express();
const HOST = process.env.CTF4B_HOST;
const PORT = process.env.CTF4B_PORT;
const FLAG = process.env.CTF4B_FLAG;
app.get("/", (req, res, next) => {
return res.send('FLAG はこちら: <a href="/flag">/flag</a>');
});
const block = (req, res, next) => {
if (req.path.includes('/flag')) {
return res.send(403, 'Forbidden :(');
}
next();
}
app.get("/flag", block, (req, res, next) => {
return res.send(FLAG);
})
var server = app.listen(PORT, HOST, () => {
console.log("Listening:" + server.address().port);
});
"/flag
" を含む URL をブロックするらしい。
そういえば URL は大文字小文字を区別しなかった気がする。
→"/flaG
" にしてみる?
ctf4b{403_...}
aiwaf
個人的に一番面白かった問題。
URL のクエリ ?file=<FILE>
を入力すると ./books/<FILE>
にアクセスするらしい。
ディレクトリ構造は
.
├── books
│ ├── book0.txt
│ ├── book1.txt
│ └── ...
└── flag
になってるので ?file=../flag
にすると良さそう。
ただ問題はサーバプログラムで「 ../
や flag
が含まれている場合には Yes を返す」ように指示された AI (ChatGPT ?) の検査をしてるのでアクセスできない...けどプログラムから察するにその検査対象になってる URL は最大 50 文字までっぽいので適当なクエリを増やす。
https\://aiwaf.beginners.seccon.games/?random=jwndsnbhjnbghjvgnbhguihjbvhjfyiuhbvhjcfyuihbjvhjffyuhjbvcgfygvmcgfjygvmjfhgbvhjbvmhc&file=../flag
とかでアクセスすると flag
が出てくる。
Half
バイナリデータの中身を見てみる。
% strings half
...(略)...
%99s%*[^
Invalid FLAG
ctf4b{ge4_t0_kn0w_the
_bin4ry...}
Correct!
:*3$"
...(略)...
実際には % hexdump -C half
で解きました。
Welcome
歓迎(洗礼)を受けました。
解きたかった問題
phisher2
自力でできたこと
わかったことは
HTTPリクエスト(POST)
POST / HTTP/1.1
Host: phisher2.beginners.seccon.games
Connection: close
Content-Type: application/json
{"text":"<URL>"}
を送ると、<URL>
をチェックして色々返してくれる。
チェックは
- 受け取った URL (
input_url
) をサーバにてchromeで開き、 - スクリーンショットを撮って文字列として抽出し (
ocr_url
) - それぞれ
r"https?://[\w/:&\?\.=]+"
が含まれてかつ -
ocr_url
が指定の URL (おそらくhttps://phisher2.beginners.seccon.games/
?) から始まるなら -
input_rul
に **flag
を含んだ** HTTP リクエストを送る。
ので考えとしては
-
input_url
に HTTP リクエストを送ってくれないとflag
がわからない。 -
ocr_url
は基本的にはinput_url
と同じになるはず。(後述) -
ocr_url
はhttps://phisher2.beginners.seccon.games/
から始まらないと HTTP リクエストは送られない。
色々考え、なぜかスクリーンショットから文字列検索していることに注目し、最初に送る POST リクエストに HTML ぶちこんで画像を操作できないかと考えた。
POST / HTTP/1.1
Host: phisher2.beginners.seccon.games
Connection: close
Content-Type: application/json
Content-Length: 101
{"text":"<span display='none'>https://example.com</span> https://phisher2.beginners.seccon.games/"}
こうすれば ocr_url
は https://phisher2.beginners.seccon.games/
で input_url
は https://exsample.com
になるのかと期待してたら
HTTP/1.1 200 OK
Server: nginx/1.25.0
Date: Mon, 05 Jun 2023 09:21:36 GMT
Content-Type: application/json
Content-Length: 182
Connection: close
{"input_url":"<span display='none'>https://example.com</span> https://phisher2.beginners.seccon.games/","message":"admin: It's not url or safe url.","ocr_url":"https://example.com"}
だめでした。
ここから解けなかったとこ
チェックの正規表現で %
が使われていないので
POST / HTTP/1.1
Host: phisher2.beginners.seccon.games
Connection: close
Content-Type: application/json
Content-Length: 75
{"text":"https://%65xample.com https://phisher2.beginners.seccon.games/"}
こうするといいらしい。
URL に %ascii 使っていいのか…
そもそも flag
を含んだ HTTP リクエストを受け取るためにサーバを用意しなければなかったらしい…
ローカル(とCTFサーバ)以外の範疇なんて考えてもなかった…
polyglot4b
自力でできたこと
プログラムを見る限り file
コマンドに JPG, PNG, GIF, ASCII の全てだと思わせれば良さそう?
GIF89a
を入力すれば GIF, ASCII だとは認識してくれる。
ただ JPG, PNG のマジックナンバーはそもそも非 ASCII だし無理では?
ここから解けなかったとこ
サンプルが含まれてる意味をもっと考えるべきでした。
% file -bkr sushi.jpg
JPEG image data, Exif standard: [TIFF image data, big-endian, direntries=4, description=CTF4B], baseline, precision 8, 1404x790, components 3
- data
[description=CTF4B]
って任意の文字列を入れているのであれば同様のことができそう?
どうやって入れるのかはいまだにわかってない…
The game is over.
まあ初参加ならこんなもんでしょう。まだ強くなる余地があると思えば…