PHPを遠隔操作してフィルターやWAFルールを回避する方法

画像

これはテストのために使う、2つある脆弱なPHPスクリプトの最初の1つです。このスクリプトはあまりにも単純で稚拙ですが、リモートコード実行の脆弱性シナリオを再現するためのものです(実際の状況では、この状態に至るまでにもう少し作業が必要かもしれません):

画像

明らかに6行目は純粋な悪です。3行目はsystem, exec, passthruのような関数をキャッチしようとしています(PHPにはシステムコマンドを実行できる他の多くの関数がありますが、これらの3つに焦点を当てています)。このスクリプトはCloudflare WAFの背後で動いているwebサーバーで動作しています(いつもCloudflareが使われるのは利便性が高く広く知られているからです、これはCloudflare WAFが安全ではないという意味ではありません。他のWAFも同様に問題を抱えています...)。2つ目のスクリプトはModSecurity + OWASP CRS3の背後にあります。

最初のテストでは、リクエスト/cfwaf.php?code=system("cat /etc/passwd")を使ってsystem()関数を用いて/etc/passwdを読もうと試みます。

画像

ご覧の通り、CloudFlareが私のリクエストをブロックしています("/etc/passwd"のためかもしれませんが)、しかし私の最後の記事で未初期化変数について読んでいれば、cat /etc$u/passwdのようなもので簡単にバイパスできます。

画像

Cloudflare WAFはバイパスされましたが、ユーザー入力に対するチェックが私のリクエストをブロックしました。なぜならば私は"system"関数を使おうとしています。"system"文字列を使わずにsystem関数を使う構文はあるでしょうか?PHP文字列に関するドキュメントを見てみましょう。

PHP 文字列エスケープシーケンス

  • [0–7]{1,3} 8進数で表記された文字列のシーケンスで、バイトに収まるように静かにオーバーフローします(例:"\400" は "\000" と同じ)。

  • \x[0–9A-Fa-f]{1,2} 16進数で表記された文字列のシーケンス(例:"\x41")。

  • \u{[0–9A-Fa-f]+} Unicodeコードポイントのシーケンスで、このコードポイントのUTF-8表現として文字列に出力されます(PHP 7.0.0で追加)。

多くの人が知らないですが、PHPには文字列を表現するためのたくさんの構文があり、"PHP変数関数"を使えば、フィルターやルールを回避するためのスイスアーミーナイフになり得ます。

PHP Variable Functions

PHPは変数関数の概念をサポートしています。これは、変数名に括弧が追加されると、PHPはその変数が評価されるものと同じ名前の関数を探して実行しようとすることを意味します。これは、コールバック、関数テーブルなどを実装するために使用できます。

つまり、$var(args);や"string"(args);はfunction(args);と等価です。変数や文字列を使って関数を呼び出せるということは、関数名の代わりにエスケープシーケンスを使用できるということです。ここに例があります:

画像

3番目の構文は16進数表記の文字列のエスケープシーケンスで、PHPはそれを文字列"system"に変換し、その後引数"ls"を持つ関数systemに変換します。私たちの脆弱なスクリプトで試してみましょう:

画像

このテクニックは全てのPHP関数には機能しません。echo、print、unset()、isset()、empty()、include、requireなどの言語構造には変数関数は使用できません。これらの構造を変数関数として使用するには、ラッパー関数を利用します。

ユーザー入力のサニタイズを改善する

もし脆弱なスクリプトでユーザー入力からダブルクオートとシングルクオートを除外したらどうなるでしょうか?ダブルクオートを使わずにバイパスすることは可能でしょうか?試してみましょう:

画像

3行目に示すように、今スクリプトはGET[code]クエリ文字列パラメータ内に" と ' の使用を防ぎます。私の前のペイロードは今やブロックされるはずです:

画像

幸いにもPHPでは、常に文字列を表現するためにクオートを使う必要はありません。PHPでは要素の型を宣言することができ、例えばa = (string)foo; の場合、aは文字列"foo"を含んでいます。さらに、特定の型宣言なしに丸括弧内部のどんなものも文字列として扱われます:

画像

この場合、新しいフィルターを回避するための2つの方法があります:1つ目は(system)(ls); のように使うことですが、codeパラメータ内で"system"を使うことはできませんので、(sy.(st).em)(ls);のように文字列を結合することができます。二つ目は_GET変数を使用することです。リクエストを?a=system&b=ls&code=_GETa; のように送ると、結果は次のようになります:_GET[a\]は文字列"system"に置き換えられ、_GET[b]は文字列"ls"に置き換えられ、全てのフィルターを回避できるようになります!

画像

この場合、それは便利ではありませんが、関数名や引数内にコメントを挿入することもできます(これは特定のPHP関数名をブロックするWAF Rule Setをバイパスするために役立つかもしれません)。以下の構文は全て有効です:

get_defined_functions

このPHP関数は、ビルトイン(内部)関数とユーザー定義関数のリストを含む多次元配列を返します。内部関数はarr["internal"]経由で、ユーザー定義関数はarr["user"]を使ってアクセス可能です。例えば:

画像

これは、その名前を使わずにsystem関数に到達する別の方法になりえます。"system"に対してgrepを行い、そのインデックス番号を見つけ出して、私のコード実行用の文字列として使用します:

画像

明らかに、これはCloudflare WAFとスクリプトフィルターに対して機能するはずです:

画像

文字配列

PHPの各文字列は(Pythonのように)文字配列として使用でき、string[2]やstring[-3]の構文で単一の文字列文字を参照できます。これは、PHP関数名をブロックするルールを回避する別の方法になり得ます。この文字列を使って$a="elmsty/ "; は、system("ls /tmp")の構文を構成できます:

画像

運が良ければ、スクリプトのファイル名内で必要なすべての文字を見つけることができます。同じ技術を使って、次のような方法で必要なすべての文字を拾うことができます:

画像

画像

OWASP CRS3
OWASP CRS3はもっと難しくなります。以前見た技術でのみパラノイアレベル1をバイパスできますが、これは素晴らしいことです!パラノイアレベル1はCRS3で見つけることができるルールの小さなサブセットで、偽陽性を防ぐために設計されています。パラノイアレベル2では、"特殊文字の異常検出(args): 特殊文字の数が超過した"という942430番のルールのため、全てが難しくなります。私ができることは、"ls"、"whoami"などの引数なしの単一コマンドを実行すること

こちらの記事はdev.toの良い記事を日本人向けに翻訳しています。
https://dev.to/tutorialboy/how-to-exploit-php-remotely-to-bypass-filters-waf-rules-lbm