CTF WriteUpeasy - phplimit (PHP无参RCE深入解析)0x01 题目代码与分析?phpif(;preg_replace(/[^\W]\((?R)?\)/,,$_GET[code])){eval($_GET[code]);}else{show_source(__FILE__);}?核心正则分析/[^\W]\((?R)?\)/这个正则是整道题的关键我们拆解来看[^\W]匹配一个或多个“非非单词字符”即匹配字母、数字、下划线等同于\w。这里主要是匹配函数名。\(和\)匹配左右括号。(?R)?核心中的核心?R代表递归匹配整个正则表达式?表示可选0次或1次。正则的作用它只匹配纯字母加括号组成的无参函数调用且允许函数嵌套。例如a(b(c()))会被匹配替换为空后剩余;通过判断进入eval。限制条件不能传入参数括号里不能有字符串、数字、变量如a(1)或a($b)都会被正则拒绝。只能使用无参函数嵌套。解题核心思路要想执行任意代码如system(ls)我们必须找到一个返回值可控的无参函数将它作为内层函数的返回值传给外层函数。0x02 解法一利用 HTTP 请求头 (Apache / PHP 7.3)1. 核心函数getallheaders()getallheaders()会获取当前请求的所有 HTTP 头信息返回一个数组。由于 HTTP 头如 User-Agent、Cookie 等是我们可以完全控制的这就变相突破了“不能传参”的限制。2. Payload 构造GET ?codeeval(next(getallheaders())); HTTP/1.1 Host: xxx User-Agent: phpinfo(); ...getallheaders()获取所有请求头数组。next()将数组内部指针向后移动一位并返回当前元素的值。由于不同服务器对请求头的排序可能不同通常next()可以跳过默认的Host字段指向我们可以控制的User-Agent等字段。eval()执行next()返回的字符串即我们设置的phpinfo();。⚠️ 关键知识点Nginx 与 Apache 的环境差异你提出的问题传统认知PHP 7.3getallheaders()是 Apache (mod_php) 的专属函数在 Nginx (php-fpm) 下调用会报错Call to undefined function。因此老 WriteUp 说 Nginx 下不能用。当前现状PHP 7.3从 PHP 7.3 开始getallheaders()被移植到了 FastCGI/FPM 模式中所以现在在 Nginx PHP 7.3 的环境下getallheaders()是可以正常使用的。结论如果你的 Nginx 靶机 PHP 版本 7.3用这个方法完全没问题如果版本低就会报错。0x03 解法二利用全局变量 (全环境通用无视版本)如果getallheaders()被禁用或者 PHP 版本低于 7.3 导致 Nginx 下不可用我们就需要找其他返回值可控的函数。最经典的就是get_defined_vars()。1. 核心函数get_defined_vars()该函数返回由所有已定义变量组成的数组包括$_GET,$_POST,$_COOKIE,$_SERVER等。由于我们可以控制 GET 传参这就相当于我们有了一个可控的数据源。2. 数组结构分析当我们发送?codeeval(...);bphpinfo();时get_defined_vars()的返回值大致如下Array([0]Array// 这是 $_GET 数组([code]eval(...);[b]phpinfo();)[1]Array// 这是 $_POST 数组...)我们的目标是取到最内层的phpinfo();这个字符串。3. Payload 构造GET ?codeeval(next(current(get_defined_vars())));bphpinfo(); HTTP/1.1get_defined_vars()获取所有变量大数组。current()获取当前指针元素默认指向第一个元素即$_GET数组。(注PHP 7.3 前用currentPHP 8.1 后current对内部指针行为有变化部分环境可能需要用reset或array_pop等)next()将$_GET数组的指针从code移动到b并返回b的值即字符串phpinfo();。eval()执行该字符串。0x04 解法三纯目录遍历读取 (无需注入代码)有时候eval被禁用或者我们不需要执行系统命令只需要读取 Flag 文件可以使用纯文件操作函数的嵌套。1. 核心思路利用scandir()列目录配合数组操作函数找到 flag 文件最后用readfile()或file_get_contents()读取。2. Payload读取当前目录倒数第二个文件通常目录结构为[., .., flag.php, index.php]。倒数第二个往往是 flag。?codereadfile(next(array_reverse(scandir(getcwd()))));getcwd()获取当前工作目录路径。scandir()列出目录中的文件和目录。array_reverse()将数组反转原来的倒数第二变成正数第二。next()跳过第一个原最后一个index.php指向第二个原倒数第二flag.php。readfile()读取并输出文件内容。3. Payload读取上级目录的 flag如果 flag 在上一级目录需要结合chdir()改变当前目录因为scandir()接受目录路径参数。?codereadfile(next(array_reverse(scandir(dirname(chdir(dirname(getcwd())))))));这里的巧妙之处在于chdir()成功返回1true失败返回false但它确实改变了工作目录。嵌套在dirname()中dirname(1)会返回当前目录.或配合改变后的路径继续向上。0x05 总结与提炼这道题是 PHP 无参 RCE 的母题掌握它就掌握了一大类题。请记住以下核心应对策略场景可用 Payload备注有请求头控制权限eval(end(getallheaders()));(修改最后请求头)eval(next(getallheaders()));(修改User-Agent等)Apache全版本可用Nginx需 PHP 7.3。无请求头控制/老版本Nginxeval(next(current(get_defined_vars())));1phpinfo();eval(end(get_defined_vars()));(用POST传参)最通用无视服务器类型和PHP版本。只需读文件无注入点readfile(next(array_reverse(scandir(getcwd()))));利用数组指针操作函数next,end,array_pop,array_rand。需要跳目录读文件readfile(array_rand(array_flip(scandir(dirname(chdir(dirname(getcwd())))))));array_flip交换键值配合array_rand随机读取也是常见绕过姿势。划重点做题时优先尝试getallheaders()因为构造简单如果报错未定义函数立刻切换思路使用get_defined_vars()这是保底解法