news 2026/8/2 15:52:11

CTF实战:PHP无参数RCE绕过技巧与代码执行漏洞分析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CTF实战:PHP无参数RCE绕过技巧与代码执行漏洞分析

1. 项目概述:从一道CTF题看PHP代码执行与无参数RCE的攻防博弈

最近在带新人入门CTF的Web安全方向,发现很多朋友卡在了ctfshow web41这道题上。这道题本身并不复杂,但它非常经典地展示了PHP代码执行漏洞的一个进阶利用场景——无参数RCE(远程代码执行)。很多刚接触安全的朋友,一看到代码执行,第一反应就是直接上system(‘ls’)或者echo file_get_contents(‘/flag’),但现实中的漏洞利用往往没这么直接。这道题就像一道精心设计的“锁”,你需要找到正确的“钥匙”,而不是用蛮力去砸。它考察的不仅仅是知道某个函数能执行命令,更是对PHP语言特性、内置函数以及绕过限制的创造性思维的深度理解。如果你正在学习Web安全,或者对PHP代码审计感兴趣,那么通过这道题的完整拆解,你能清晰地看到一条从漏洞发现到利用构造的完整路径,以及防守方是如何通过层层过滤来增加攻击难度的。

这道题的核心场景是:你面对一个存在代码执行点的Web应用,但常用的命令执行函数和参数传递方式被过滤或限制,你需要在不直接传递参数的情况下,读取到服务器上的目标文件(通常是/flag)。这不仅仅是CTF比赛中的题目,其背后反映的“无参数RCE”思想,在真实的代码审计和渗透测试中同样具有参考价值。接下来,我将以这道题为蓝本,带你一步步拆解其背后的技术原理、可能的过滤规则,并详细演示如何利用PHP的“黑魔法”来构造出有效的利用链。

2. 核心思路拆解:为什么常规的“一句话木马”行不通了?

在深入解题之前,我们必须先理解出题人设置的“障碍”是什么。根据ctfshow web41的常见设定以及相关搜索词(如php 正则ctfshow命令执行)的提示,我们可以合理推断题目环境对用户的输入进行了严格的过滤。

2.1 常见过滤机制与我们的应对策略

通常,这类题目的PHP代码核心逻辑可能如下所示:

<?php if(isset($_GET['c'])){ $c = $_GET['c']; // 过滤1:检查是否包含括号`(` if(!preg_match("/[a-zA-Z0-9\(\)]/", $c)){ eval($c); } } else { highlight_file(__FILE__); } ?>

当然,实际的过滤规则可能更复杂。但从解题经验和相关热词(php 正则 变量金额 大于0,虽然不直接相关,但反映了对正则过滤的关注)来看,常见的过滤点包括:

  1. 禁止括号():这是最致命的一招。没有括号,你就无法调用函数,因为函数调用function_name(argument)的语法依赖括号。这直接封杀了system(‘ls’)eval($_POST[‘a’])等直接调用。
  2. 黑名单关键字过滤:过滤systemexecshell_execpassthruevalassert等明显的危险函数名。
  3. 限制参数传递:禁止或转义单双引号、$$_GET/$_POST等超全局变量,让你无法直接传入外部数据作为参数。
  4. 无字母数字限制:有时甚至会过滤所有字母和数字,将利用限制在极小的字符集内(这道题41关通常还没到这么极端)。

面对这些限制,我们的核心思路必须转变:从“直接调用函数执行命令”转变为“利用现有环境和不带括号的语法结构,间接达成代码执行或文件读取的目的”

2.2 无参数RCE的突破口:PHP的语言特性

PHP提供了许多独特的语言特性和内置函数,它们成为了我们在“紧身衣”里跳舞的工具:

  • 字符串获取函数getallheaders()get_defined_vars()session_id()等,可以从HTTP请求或服务器环境中获取数据,且通常调用时不需要括号(或括号内可为空)。
  • 数组操作与当前作用域current()end()next()prev()reset()等数组指针函数,可以操作$_GET$_POST等数组。配合localeconv()phpversion()等返回数组的函数,可以构造出我们需要的字符。
  • 文件系统遍历scandir()readdir()opendir()等,用于列出目录。highlight_file()show_source()readfile()file_get_contents()用于读取文件内容,但它们通常需要参数。
  • “无参”函数调用技巧:利用?><?php标签闭合与开启,或者通过include/require包含由我们控制输入的文件(如$_FILES),但这道题可能不提供文件上传点。

对于web41,根据其难度定位,过滤很可能集中在禁止括号黑名单函数上。因此,我们的主攻方向是:找到那些可以不使用括号(或使用()空括号)调用的函数,并且能让它们返回我们所需的数据(如目录列表、文件内容)

3. 关键利用链构造:从信息收集到获取Flag

假设我们面对的就是一个过滤了括号和危险函数名的环境。我们的攻击链可以分三步走:信息收集、定位目标、读取内容。

3.1 第一步:探测环境与获取目录列表

既然不能直接用system(‘ls’),我们就需要用PHP内置的函数来实现类似功能。这里,scandir()函数是一个绝佳的选择,它返回指定目录的文件和目录的数组。但问题来了,scandir(‘.’)需要括号和参数。

技巧1:利用scandir()的无参数调用与.的构造在PHP中,scandir()可以不带参数调用,默认扫描当前目录(.)。但我们的过滤可能禁止了括号。然而,有一个关键特性:如果函数定义时参数有默认值,调用时可以省略该参数。但scandir()的参数并非可选。不过,我们可以利用另一个特性:scandir(current(localeconv()))。这看起来复杂,我们拆解一下:

  1. localeconv():返回包含本地化数字和货币格式信息的数组。它不需要参数(或者使用空括号())。这个数组的第一项(current位置)通常是一个点.
  2. current():返回数组中的当前元素(第一个元素)。current(localeconv())就得到了字符串.
  3. scandir(‘.’):这样就构造出了参数。

但这里依然有括号。如果括号被完全禁止,这条路就走不通。这时,我们需要更“纯粹”的无参函数。

技巧2:使用getcwd()print_r(scandir(getcwd()))的变体getcwd()获取当前工作目录,也不需要参数。但同样面临括号问题。如果环境是eval($_GET[‘c’]),且只过滤了括号本身,但我们可以通过拼接来绕过对完整函数名的过滤吗?例如$a=’scandir’;$a(‘.’);,但这需要分号和变量赋值,也可能被过滤。

实际上,对于web41,更经典的解法是直接利用一个返回数组且无需括号的函数,然后去读取这个数组。但读取数组需要索引,如$_GET[1],这又涉及括号[ ]和数字。如果[ ]也被过滤了呢?

让我们换个思路:如果scandir(‘.’)不可行,我们能不能直接读取已知文件?但题目要求通常是找到/flagflag.php,位置未知。所以,目录列表是必须的。

经过对多种绕过技术的排查,在web41的常见解法中,session_id()配合session_start()是一条可行的路径,但需要session支持。而更通用、更常见的解法是使用DirectoryIterator类或FilesystemIterator。它们可以用foreach进行遍历,而foreach是语言结构,不是函数,不依赖括号调用函数。

例如,构造Payload:c=foreach(new DirectoryIterator(‘.’)){echo $it->getFilename().” “;}。这里new DirectoryIterator(‘.’)是一个对象实例化,括号是类实例化语法的一部分,可能未被过滤,或者我们可以用chr(46)来构造点号。但这仍然不够“无参”。

3.2 第二步:真正的无参数RCE核心技巧

经过对题目常见解法的回溯,web41最核心的考点是一种称为“无参数函数RCE”的技巧,其核心是利用函数返回值作为另一个函数参数的特性,且全程不使用括号来传递我们手写的参数。

核心Payload模型:eval(end(getallheaders()));

我们来拆解这个经典的利用链:

  1. getallheaders():这是一个非常关键的PHP函数。它返回所有HTTP请求头的关联数组,并且调用时不需要任何参数。这意味着我们可以完全控制这个数组的一部分内容——通过发送HTTP请求时自定义请求头。
  2. end():将数组的内部指针移动到最后一个元素,并返回其值。我们通过添加一个自定义HTTP头(例如X-Forwarded-For: phpinfo();),并确保它是最后一个头,end(getallheaders())就能返回我们注入的代码字符串。
  3. eval():执行该字符串。

那么,在题目中,我们的Payload可能就是:?c=eval(end(getallheaders()));然后,在发送这个HTTP请求时,我们额外添加一个请求头,比如:X-Forwarded-For: system(‘ls’);

但是,注意!这里出现了system(‘ls’),如果题目过滤了system等函数名,这个Payload会失效。所以我们需要替换为其他未被过滤的函数,比如用反引号执行命令`ls`,或者用shell_exec(‘ls’)(如果它不在黑名单)。更稳妥的方式是进行文件读取,因为目标很明确是获取flag

因此,改进的利用链是:

  1. 首先,我们需要知道文件名。所以先列目录。
  2. 我们可以构造一个能执行代码的请求,把目录列表打印出来。

eval(end(getallheaders()))本身需要eval,如果eval被关键字过滤了呢?我们可以尝试用assert,但assert也可能被过滤。这时,我们可以利用includerequire。但include需要一个文件名参数。

这就引出了另一个技巧:利用php://伪协议配合输入流php://inputphp://input可以读取POST请求的原始数据。我们可以这样构造:?c=include(‘php://input’);然后在POST Body里写:<?php system(‘ls’);?>但这里include有括号和参数。如果括号被禁,此路不通。

3.3 第三步:针对web41的实战Payload构造与测试

根据多方解题记录和ctfshow系列题目的连贯性,web41的典型环境是:代码执行点存在,但过滤了所有括号()和分号;(或者分号允许,但括号不允许)。同时,可能还过滤了常见的命令执行函数名。

在这种情况下,上面提到的eval(end(getallheaders()))因为包含括号而无法使用。我们需要找到完全不使用括号的代码执行方式。

突破口:echo ‘something’;这种语句不需要括号。但如何动态执行代码?在PHP中,有一个鲜为人知但极其强大的特性:反引号`操作符。它用于执行Shell命令,并返回输出。它的使用就像echols;。注意,这里的反引号是Shell命令执行,它本身是语言操作符,不是函数调用,所以不依赖括号!

那么,一个可能的Payload是:?c=echols;但这里有个问题:URL中反引号可能需要URL编码(%60),并且ls命令本身在Web服务器环境下可能没有权限,或者被禁用。更常见的是用cattacmoreless等命令读取文件。

但题目要求通常是在网站根目录或当前目录下找flag文件。我们先列出文件:?c=echols;如果成功,页面会显示当前目录的文件列表。

假设我们看到一个文件叫flag.php,那么直接读取:?c=echocat flag.php;

但是,这里有一个巨大的“坑”:命令执行后的输出,会被直接嵌入到PHP页面中。如果flag.php内容包含<?php ... ?>标签,它们会被当作PHP代码解析,而不会显示源码。我们需要查看源码,所以应该用读取纯文本文件的方式,或者使用能显示源码的命令。

更好的命令是tac(反向cat,可以避免某些文件开头特殊字符问题)或者cat后通过管道传递给base64编码:?c=echocat flag.php | base64;这样,输出是一段base64编码,我们解码后就能得到文件内容,包括<?php标签。

实操步骤:

  1. 使用Burp Suite或浏览器开发者工具的网络选项卡,发送GET请求:http://目标地址/?c=echols;
    • 注意:反引号在URL中需编码为%60,所以实际请求是:?c=echo %60ls%60;
  2. 查看响应,假设看到文件列表包含flag.php
  3. 发送请求读取文件:?c=echo %60cat flag.php%60;
  4. 如果返回空白或异常,尝试tac或base64编码:?c=echo %60cat flag.php | base64%60;
  5. 将返回的base64字符串解码,即可得到flag。

重要注意事项:这种方法成功的前提是,服务器操作系统是Linux/Unix,并且catlsbase64等命令可用,且Web进程有执行这些命令的权限。在CTF环境中,这通常是满足的。但在实际渗透测试中,需要具体情况具体分析。

4. 深入原理:为什么反引号能绕过过滤?

理解原理能帮助我们举一反三。反引号操作符在PHP中相当于调用了shell_exec()函数,但它是以语言操作符的形式存在的。这意味着:

  • 它在词法分析阶段就被识别为执行操作符,而不是一个函数名。
  • 因此,基于函数名黑名单的过滤(如preg_match(‘/system|exec|shell_exec/’, $c))对它无效。
  • 因为它不是函数调用,所以不违反“禁止括号”的规则。它的语法是 `command`,完全不需要括号。

然而,这种方法的局限性也很明显:

  1. 依赖系统Shell:只能在类Unix系统上使用,Windows系统上不适用(尽管CTF题大多用Linux)。
  2. 可能被禁用:PHP配置中可以通过disable_functions禁用shell_exec(),这同样会禁用反引号操作符。但CTF题目为了考察这个知识点,通常不会禁用。
  3. 特殊字符转义:如果命令中包含空格、管道|、重定向>等Shell元字符,需要确保它们能正确传递。在URL中,空格可以用%20或加号+表示,但最好整个命令用反引号包起来即可。

5. 其他可能的绕过路径与备用方案

虽然反引号是解决web41的一种直接方法,但作为学习者,我们应该探索更多可能性,以应对题目变种或实际环境中更复杂的情况。

5.1 利用$_SERVER超全局变量

$_SERVER是一个包含服务器和执行环境信息的数组。其中,$_SERVER[‘QUERY_STRING’]包含了URL中?之后的所有查询字符串。如果我们能控制它,并让eval执行它的一部分呢? 例如,Payload:?c=eval($_SERVER[‘QUERY_STRING’]);但这里包含了$_SERVER、括号和引号,很可能被过滤。

但我们可以尝试利用$_SERVER[‘HTTP_REFERER’]$_SERVER[‘HTTP_USER_AGENT’],这些是客户端发送的HTTP头,我们可以控制。例如:?c=eval($_SERVER[‘HTTP_REFERER’]);然后在请求中设置Referer头为:http://example.com/?phpinfo();但这同样需要括号和引号。

5.2 利用文件包含与伪协议

如果代码执行点是include($_GET[‘c’])而不是eval,那么我们可以利用PHP伪协议。

  • ?c=php://filter/convert.base64-encode/resource=flag.php:将flag.php的内容base64编码后输出,避免被当作PHP执行。
  • ?c=data://text/plain,<?php system(‘ls’);?>:使用data协议直接包含代码。但这需要allow_url_include设置为On,且可能被过滤。

对于web41,如果是eval,这些协议可能不适用,因为eval期望的是字符串代码,而不是文件名。

5.3 利用PHP的“可变函数”与字符串拼接

如果过滤了反引号,但允许美元符号$和花括号{}呢?我们可以尝试动态构造函数名。 例如:?c=$_=’sys’.’tem’;$_($_GET[‘a’]);&a=ls这里,$_GET[‘a’]可能被过滤。我们可以尝试用其他方式获取参数,比如用${‘_GET’}[‘a’]这种可变变量形式,但同样复杂。

6. 常见问题排查与实战心得

在实际操作中,你可能会遇到各种问题。这里我总结几个常见坑点和排查技巧:

  1. Payload发送后无回显或报错

    • 检查命令语法:确保反引号是成对的,命令本身正确。在Linux中,可以先用本地终端测试命令。
    • 检查URL编码:特殊字符如反引号%60、空格%20、管道%7c必须正确编码。使用Burp Suite的Repeater模块可以方便地修改和发送请求,避免浏览器自动编码带来的问题。
    • 查看服务器响应:用Burp或浏览器开发者工具查看原始HTTP响应,而不是只看页面渲染。可能命令执行的结果在HTML注释里,或者被错误信息掩盖了。
    • 尝试简单的命令:先试试echopwd;(打印当前目录)或echoid;(查看当前用户),确认命令执行是否生效。
  2. 命令执行了,但找不到flag文件

    • 扩大搜索范围ls -la查看所有文件(包括隐藏文件)。find / -name ‘*flag*‘ 2>/dev/null在全盘搜索包含flag的文件名(2>/dev/null是为了屏蔽权限错误产生的噪音)。
    • 检查常见位置/flag/flag.txt/home/ctf/flag/var/www/html/flag.php./flag
    • 读取环境变量:有时flag在环境变量里,可以试试echoenv;
  3. 反引号被过滤了怎么办?如果反引号也被加入黑名单,那么情况就更复杂了。可能需要回归到最原始的无参数RCE研究,利用localeconv()current()array_reverse()scandir()等函数进行复杂的嵌套,最终用readfile(end(scandir(current(localeconv()))));这样的形式来读取当前目录最后一个文件(假设flag是最后一个文件)。这需要极强的耐心和对PHP数组函数的深刻理解。

  4. 我得到的是一串代码,不是flag: 如果flag在PHP文件里,直接cat可能会因为<?php ... ?>标签被解析而看不到内容。一定要用cat flag.php | base64或者php -r ‘highlight_file(“flag.php”);’(如果PHP命令行可用)的方式。更简单的是在浏览器中直接查看网页源代码(Ctrl+U),有时flag就以注释形式<!-- flag{xxx} -->藏在页面里。

个人实操心得

  • 工具至上:手工构造Payload容易出错,尤其涉及复杂编码时。Burp Suite是Web安全测试的“瑞士军刀”,它的Repeater、Decoder、Intruder模块能极大提升效率。把Payload放在Repeater里,方便修改和重放。
  • 思维要活:不要被题目描述或自己的第一印象困住。如果一种方法不行,立刻回溯过滤规则,思考哪些字符可用,哪些不可用,然后组合可用的“积木”去构建Payload。
  • 信息收集是第一步:在尝试RCE之前,先用phpinfo();(如果允许)看看环境,用echopwd;echols -la;看看目录结构。信息越多,后续利用越有针对性。
  • 注意细节:Linux命令区分大小写,Flagflag是不同的文件。文件名后面可能有换行符,使用cat flagcat flag.php更稳妥,因为Tab键补全可能会出错。

这道ctfshow web41就像一把钥匙,它打开的不是一道简单的门,而是通往PHP代码安全世界的一扇窗。通过它,你看到的不仅仅是绕过技巧,更是对PHP语言本身和服务器安全配置的深度理解。真正的安全攻防,就是在这样一层层的限制与突破中不断演进的。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/2 15:51:33

密码游戏设计解析:从规则引擎到安全策略的启示

1. 项目概述&#xff1a;当密码规则变成一场“游戏”如果你觉得设置一个“安全”的密码只是头疼地混合大小写字母和数字&#xff0c;那说明你还没玩过《The Password Game》。这个由独立开发者尼尔阿加瓦尔&#xff08;Neal Agarwal&#xff09;创作的网页游戏&#xff0c;在20…

作者头像 李华
网站建设 2026/8/2 15:45:26

IDA Pro动态调试实战:从零破解CTF逆向题的核心思路与操作

1. 项目概述&#xff1a;从零开始&#xff0c;用动态调试破解你的第一道CTF题如果你刚接触CTF逆向&#xff0c;面对一堆看不懂的汇编代码和加密逻辑&#xff0c;是不是感觉无从下手&#xff1f;别担心&#xff0c;这几乎是每个新手都会经历的阶段。逆向工程听起来高大上&#x…

作者头像 李华
网站建设 2026/8/2 15:43:26

3个关键步骤彻底解决华硕笔记本臃肿控制软件问题

3个关键步骤彻底解决华硕笔记本臃肿控制软件问题 【免费下载链接】g-helper Lightweight Armoury Crate alternative for Asus laptops with nearly the same functionality. Works with ROG Zephyrus, Flow, TUF, Strix, Scar, ProArt, Vivobook, Zenbook, Expertbook, ROG Al…

作者头像 李华
网站建设 2026/8/2 15:38:17

ESP32-S3深度睡眠实战:从模式选择到功耗优化全解析

1. 项目概述&#xff1a;为什么ESP32的“睡觉”是个技术活 拿到一块XIAO ESP32S3 Sense开发板&#xff0c;第一反应往往是折腾它的摄像头和麦克风&#xff0c;搞点AI视觉或语音识别。但当你真正想用它做个能跑上几个月甚至一年的低功耗设备时&#xff0c;比如一个无线环境传感器…

作者头像 李华
网站建设 2026/8/2 15:35:27

时间序列预测建模全流程解析:从ARIMA到机器学习实战指南

1. 项目概述&#xff1a;为什么时间序列预测是科研新手的“必修课”&#xff1f; 如果你刚踏入科研领域&#xff0c;无论是经济学、气象学、生物信息学还是工程学&#xff0c;大概率会遇到一个共同的任务&#xff1a;基于历史数据预测未来。这个任务的核心&#xff0c;就是时间…

作者头像 李华