ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Bugku eval题深度复盘:PHP代码执行漏洞与RCE绕过实战

Bugku eval题深度复盘:PHP代码执行漏洞与RCE绕过实战 Bugku平台上的Web题一直是许多新手从“会写代码”跨向“会审代码”的第一道门槛。今天要深度复盘的是Bugku Web分类里那道很经典的 eval 题它属于PHP代码执行漏洞也就是大家常说的RCE入口。别小看这一个函数背后牵扯出来的知识点——eval的危险特性、正则黑名单绕过、命令执行函数差异——几乎能串起CTF Web入门阶段最核心的一条学习线。这篇文章我会从读题开始把完整做题链路、绕过思路、现场踩坑全部摊开讲适合正在刷题找flag的新手也适合想补一轮PHP安全基础的开发同学参考。1. 题目初探Bugku上的eval题到底在考什么1.1 Bugku平台与Web方向刷题建议先简单定位一下场景。Bugku是一个面向CTF新手和老手开放的在线靶场里面的题目覆盖Web、逆向、PWN、密码学、杂项等多个方向。其中Web题是最容易“正反馈”的类别只要你能读懂代码逻辑找到注入点或执行点很快就能拿到flag。很多刚入门的朋友问我“Web安全从哪里开始刷”我通常建议先在Bugku、CTFshow这类平台上把基础类题目过一遍尤其要把代码执行、SQL注入、文件包含、反序列化这几个大主题吃透。而这次要讲的eval题属于Web分类里非常典型的一道“送分题”。它的出现频率极高形式也很统一给一个带参数的PHP页面参数值被传进eval函数执行。表面上看只是“一行代码”实际考查的是你对代码执行漏洞的理解、对过滤规则的敏感程度、对命令执行和文件读取的熟练度。无论从哪个角度看它都是入门阶段的必刷题。我在实际带新人的时候经常说一句话“eval题拿不到flag不是因为不会用函数而是因为看不懂代码里那个过滤条件到底拦住了什么。”这句话放到各类RCE题目里都成立。所以这篇复盘的重点不光是告诉你最终payload长什么样更要把“为什么这样能绕过”讲明白。1.2 eval题的核心考点代码执行漏洞题目名称就叫eval一眼看过去指向性很强。这类题的核心考点就是PHP代码执行漏洞程序接收了用户可控的输入然后把它当成PHP代码去执行。和SQL注入本质上是同一个问题——把不可信的数据放进了可信的执行上下文。区别在于SQL注入拼接的是SQL语句eval注入拼接的是PHP代码。一旦能控制PHP代码基本上就等同于控制了整个服务器的执行环境。你能执行phpinfo()看配置能执行system(ls)列目录能执行file_get_contents()读任意文件能执行file_put_contents()写一句话木马极端情况下还能通过反序列化链、FFI扩展、LD_PRELOAD等技巧绕过禁用函数拿到更高权限。很多新手容易把“代码执行”和“命令执行”混为一谈其实它们有清晰边界代码执行执行的是PHP语法比如eval(phpinfo();)你能调用任意PHP内置函数。命令执行执行的是操作系统命令比如system(whoami)、ls你能直接操作Shell。但实际做题中它们经常联动使用因为PHP里system、exec、shell_exec、passthru这些函数会把命令交给系统Shell处理。CTF里最常见的组合就是eval($_GET[cmd])配合system(cat flag.php)直接读文件这也是我们接下来要走的路径。2. 基础原理拆解为什么eval会成为Web安全的噩梦2.1 eval的本质把字符串当代码执行在PHP里eval是一个语言构造器不是普通函数。它的用法是eval($code)其中$code必须是合法的PHP代码字符串并且通常要以分号结尾。举个例子?php $cmd phpinfo();; eval($cmd); ?这段代码会把$cmd里的字符串当作PHP代码解析执行最终输出PHP的配置信息页面。问题在于如果$cmd来自用户提交的$_GET、$_POST、$_COOKIE、$_REQUEST等超全局变量那攻击者就能把任意PHP代码塞进去执行。用一个生活化类比eval就像把餐厅后厨的锅铲直接递给客人还跟客人说“你来炒个菜”。客人炒什么完全不在你的控制范围内可能是青菜也可能是一锅危险化学品。只要这个“客人输入”没有被严格限制出问题只是迟早的事。需要特别注意的是eval是语言构造器这意味着很多安全措施对它是无效的。比如PHP的disable_functions配置只能禁用自定义函数和内置函数但不能禁用eval、include、require这类语言结构。换句话说你不能靠disable_functions来“封杀eval”只能从代码层面根治不让用户输入接近eval。另外JavaScript里的eval也有同样的风险前端的eval(alert(document.domain))就是典型XSS攻击向量。虽然本次题目是PHP但理解这一点能帮你建立跨语言的通用安全意识凡是“把字符串当代码”的机制都必须极度警惕。2.2 代码执行与命令执行的界限与联动回到题目场景。当我们拿到一个eval执行点时第一步通常会做两件事确认代码是否真的执行了以及确认当前环境有哪些可用函数。phpinfo()就是最经典的验证函数它既能证明代码执行成功又能泄露大量服务器配置信息。如果phpinfo()输出正常接下来要考虑的就是“怎么拿到flag”。大多数情况下flag存放在某个文件里比如flag.php、flag.txt、/flag此时就有两条路用PHP代码读文件echo file_get_contents(flag.php);、print_r(file(flag.php));、highlight_file(flag.php);高亮源码读PHP文件很直观。用系统命令读文件system(cat flag.php);、echo shell_exec(cat flag.php);、echo cat flag.php;。这两条路径都可能被过滤所以解法往往是在它们之间切换。比如黑名单过滤了flag字符串但你依然可以用system(cat fla*)通配符绕过如果过滤了system你可以改走echo file_get_contents(fla.g.php);如果同时过滤了file_get_contents和system那就得组合字符串拼接、变量函数、编码解码等技巧。做题的本质其实就是在“攻击者可控输入”和“程序过滤规则”之间做博弈。每多一层过滤解题就多一步绕过这正是Web安全的魅力所在。2.3 常见危险函数横向对比表在动手写payload之前先用一张表把常用的执行函数理清楚。这个表建议收藏后面做其他RCE题目也能用上。函数/构造器类型返回值/输出典型用法注意点eval($code)代码执行取决于代码eval($_GET[cmd]);需要完整语句并以分号结尾assert($code)代码执行取决于代码assert($_POST[x]);可以执行单表达式老版本常被用作后门system($cmd)命令执行直接输出结果system(ls);最常用的命令执行函数exec($cmd)命令执行只返回最后一行echo exec(ls);需要echo才能看到输出shell_exec($cmd)命令执行返回完整输出字符串echo shell_exec(ls);反引号cmd是它的语法糖passthru($cmd)命令执行原始二进制输出passthru(cat flag.php);适合有二进制输出的场景popen($cmd, $mode)命令执行返回文件指针fread(popen(ls,r),1024);需要配合文件读取函数preg_replace/e代码执行替换后输出preg_replace(/x/e,$code,$input);PHP 5.5以后已废弃别再用pcntl_exec($path,$args)命令执行无输出pcntl_exec(/bin/cat,[flag.php]);需要pcntl扩展环境少见还要补充一个概念PHP 7以后assert的行为有变化默认情况下assert($code)不再执行字符串代码必须启用zend.assertions1且assert.exception0才可能利用。但历史遗留代码里依然大量存在assert后门遇到老题和老系统时要优先想到它。3. 手把手解题实录从读源码到拿到flag的完整链路3.1 准备工具与环境做这类题最朴素的工具组合其实就够了一个现代浏览器加一个Burp Suite或HackBar插件。如果你刚接触CTF暂时没装Burp也没关系浏览器地址栏完全可以承担构造请求的工作。区别只在于地址栏直接输入适合参数简单、payload不复杂的场景。HackBar插件方便对URL进行编码、解码快速构造请求体。Burp Suite适合需要观察完整请求响应、反复重放的场景尤其当目标有WAF或过滤规则时Burp的Repeater模块能帮你快速调试payload。我个人习惯做题时开两个窗口左边浏览器看页面渲染和响应右边Burp Repeater保留每一次请求的完整记录。这样一旦某个payload生效我能立刻知道是哪一步带来的突破。另外要强调URL编码的重要性。当你往地址栏输入?cmdsystem(cat flag.php);时浏览器会帮忙处理一部分符号但有些字符比如空格、引号、分号可能被截断或转换。遇到这种情况推荐把payload整体做一次URL编码或者用curl命令行直接提交比如curl http://your-bugku-instance/index.php?cmdphpinfo();如果payload里有空格最好写成人人都看得懂的编码形式?cmdsystem(cat%20flag.php);这里的%20就是空格的URL编码。搞清楚编码规则能省掉很多“为什么我的payload看起来对了就是不生效”的排查时间。3.2 第一步信息收集从URL参数到源码泄露打开题目页面后我的第一反应永远是先看URL和页面源码。题目一般长这样访问http://your-bugku-instance/index.php页面显示一段PHP源码这是通过highlight_file(__FILE__)把当前文件高亮展示出来。老手称之为“源码审计直接从页面开始”。如果页面没有直接亮出源码也不要慌可以尝试的常规操作包括在URL末尾加?cmd试试看是否报错或有什么提示。用view-source:http://your-bugku-instance/index.php查看HTML源码。用curl拉取首页源码寻找表单、隐藏参数、注释信息。尝试常见备份文件和敏感文件如index.php.bak、www.zip、.git目录等。这道题的典型源码我见过很多变体最常见的是这样一段?php error_reporting(0); if (isset($_GET[cmd])) { $cmd $_GET[cmd]; if (!preg_match(/flag/i, $cmd)) { eval($cmd); die(); } } highlight_file(__FILE__); ?读这段代码很快能梳理出三条关键信息只要GET请求里带了cmd参数它的值就会进入eval执行存在代码执行漏洞。在执行前有一个preg_match(/flag/i, $cmd)检测意味着cmd参数里如果出现flag字符串不区分大小写因为有i修饰符就会被拦截。highlight_file(__FILE__)把源码输出在页面上属于主动的信息泄露说明题目希望你直接从源码开始审计。到这里题目思路已经很清晰绕过flag关键词过滤读取包含flag的文件内容。接下来就是一步步试探。3.3 第二步初步探测phpinfo()与ls先做一个最简单的验证确认eval确实在执行。直接在地址栏提交?cmdphpinfo();如果页面返回PHP配置信息大表格说明代码执行成功。这一步的另一个好处是你能在phpinfo()输出里看到disable_functions的配置内容。如果system、exec这些函数被禁用了后面就不能走命令执行路线只能改用PHP文件读取函数来读flag。确认执行点没问题后继续列目录看当前工作目录下有什么文件?cmdsystem(ls);在Bugku这道题的标准环境里输出通常很清晰当前目录下存在index.php和flag.php或者叫flag。看到flag.php后新手最容易犯的错误就是立刻执行?cmdsystem(cat flag.php);结果被preg_match(/flag/i, $cmd)拦了个正着页面空白什么也不输出。别慌这恰恰是题目设计的核心考点过滤了关键词考验你绕过过滤的能力。3.4 第三步编写绕过payload既然flag字符串不能出现在cmd参数里那就想办法在运行时拼出这个文件名。我把绕过思路分成几类每一类都可以在这道题上直接试验。第一种是通配符绕过。Linux Shell的通配符*、?能匹配文件名而且通配符本身不包含flag字样自然不会被正则拦截?cmdsystem(cat fla*);fla*能匹配flag.php也能匹配flag.txt等所有以fla开头的文件。如果同时存在多个匹配文件cat会把它们全部输出稍微有点噪音但是能看到flag就够了。更精确一点的写法是cat f*lag.php用*替代中间的a。如果连fla*这种写法还觉得不稳可以试试问号匹配?cmdsystem(cat fla?.php);?只匹配一个任意字符fla?.php里?匹配g最终等价于cat flag.php。第二种是字符串拼接绕过。在PHP代码里先把flag拆开再拼起来让cmd参数中不出现完整的flag字符串。例如?cmd$afl;$bag;system(cat .$a.$b..php);这段代码的执行过程是先给$a赋值为fl再给$b赋值为ag然后拼接成flag.php传给system执行。因为cmd参数里只有fl和ag没有连续的flag所以preg_match检测不出来。不过要提醒一点这种多语句payload里有空格和分号放在URL里最好做URL编码。比如空格编码为%20分号在很多场景下不需要编码但为了保险也可以编码为%3B。我实际测试时发现直接提交也是可行的因为现代浏览器和大多数环境对分号兼容性还不错但遇到解析问题时要优先想到编码。第三种是字符函数拼接绕过。如果不直接定义变量也可以用chr()函数把字符拼出来。fl对应ASCII码是102, 108ag对应97, 103payload可以写成?cmdsystem(cat .chr(102).lag.php);或者?cmdsystem(cat .chr(102).chr(108).chr(97).chr(103)..php);这种写法的好处是完全没有flag关键词缺点是payload太长可读性差。适合在过滤更严格、连f和l连续出现都检测的场景下使用。第四种是Base64编码混过过滤。把要执行的命令先做Base64编码再在PHP里base64_decode解码后交给system执行。比如cat flag.php的Base64编码是Y2F0IGZsYWcucGhwpayload为?cmdsystem(base64_decode(Y2F0IGZsYWcucGhw));同样cmd参数里没有出现flag所以正则检测拦不住。这种方法在真实渗透中也很常见很多WebShell的“加密传输”就是这种思路的变体。顺便提一个有迷惑性的点如果preg_match的过滤规则没有i修饰符也就是/flag/而不是/flag/i那么直接提交system(cat FLAG.php);就能绕过因为正则区分大小写。做题时先看清源码里的正则细节非常重要不要一上来就上高难度绕过很多时候简单方案就够了。3.5 最终读取flag并复盘按照上一节的任意一个绕过payload提交页面就会输出flag.php的内容。以通配符方案为例最终请求长得像这样?cmdsystem(cat fla*);响应里除了HTML源码片段还能看到一串类似flag{xxxx-xxxx-xxxx}的字符串。拿到flag后别急着关页面建议回头再做一次复盘把整条利用链路在脑子里过一遍入口cmd参数可控并进入eval。验证phpinfo()证明代码执行成功。探测system(ls)定位flag文件名。绕过preg_match(/flag/i)阻止直接读取使用通配符/拼接/编码绕过。利用system(cat fla*)读取文件内容并回显。把这五步写进你自己的解题笔记里。以后遇到任何代码执行类题目都可以沿着这条链路快速定位问题。4. 实战中的常见问题与排查技巧4.1 黑名单绕过的四种经典姿势上面的题目只是过滤了一个关键词flag属于最简单的情况。实际上CTF和真实漏洞里的黑名单会复杂得多可能同时过滤函数名、命令关键字、危险字符。这里把最常见的绕过姿势整理成一张速查表方便直接套用。绕过类型核心思路示例适用场景通配符绕过用Shell通配符匹配文件名cat fla*、cat f???过滤文件名关键词字符串拼接在PHP层拼出关键词$afl;$a.ag;过滤完整关键词编码解码绕过Base64/Hex编码后解码执行base64_decode(Y2F0IG... )过滤关键词但没过滤函数变量函数/动态调用间接调用被过滤的函数名$fsys.tem;$f(ls);过滤函数名关键词反引号执行用反引号代替shell_execcat fla*过滤system等函数名转义与控制字符利用正则空白符、换行等差异在关键词中插入\t、\n正则过滤不严谨时以变量函数为例如果源代码如下过滤了system字符串if (!preg_match(/system|exec|shell/i, $cmd)) { eval($cmd); }直接写system(ls)肯定不行但可以改写为?cmd$fsys.tem;$f(ls);因为cmd参数里没有连续的system而在PHP执行过程中$f被赋值后再调用效果等价于system(ls)。再配合字符串拼接能突破很多简单黑名单。4.2 为什么我的payload不生效排查三板斧做题时最磨人的不是不会构造payload而是payload看起来没问题结果就是不回显。根据我刷题和带新人的经验90%的“不生效”都能归到下面三个原因。第一URL编码问题。URL里的空格、引号、分号、$符号会被浏览器或服务器解析成特殊含义导致payload被截断。遇到这种情况把整个查询字符串做一次URL编码再提交或者改用curl。比如system(ls)带空格可以先编码成system(%22ls%22)再提交。我习惯把引号也编码这样能减少很多意外。第二执行成功但没有输出。有些函数本身不直接回显结果比如exec只返回最后一行你必须echo exec(ls);才能看到内容shell_exec虽然返回完整输出但如果你不echo结果也不会显示在页面上。还有popen需要配合fread读取。如果你用exec且没加echopayload执行成功但页面空白很容易误判成“被拦截了”。第三被禁用函数拦截。如果phpinfo()里显示disable_functions包含system、exec、shell_exec、passthru那么命令执行路线基本断掉。这时候要切换到PHP层文件读取函数比如file_get_contents、highlight_file、readfile、fopenfread等。例如?cmdecho file_get_contents(fla.g.php);如果file_get_contents也被禁了优先试highlight_file它专门用来高亮PHP文件对读PHP源码非常友好?cmdhighlight_file(fla.g.php);还有一类情况容易被忽略参数名本身被过滤。题目如果检测的是$_GET的键名你换一个参数名当然不行但如果检测的是值那你也可以尝试用$_REQUEST触发因为$_REQUEST同时包含GET、POST、COOKIE的数据。有的题目只检查GET参数你就可以把payload放到POST正文里利用$_REQUEST[cmd]绕过。4.3 竞赛中的进阶变种与联想一道简单的eval题往往可以延伸出好几种变体。我在其他平台上刷题时遇到过的相关考点有这些提前了解能帮你建立体系化认知无回显场景eval执行了但页面上什么也没有。此时可以考虑写文件后访问比如file_put_contents(shell.php,?php phpinfo();?)或者用DNS外带、sleep时间盲注等方法确认执行结果。CTF中常见的是配合curl把数据带出来但要注意目标环境是否允许外联。长度限制cmd参数被限制了最大长度长payload没法用。对策是尽量用短函数名、短文件名、短命令比如system(ls)本身就很短读文件可以echo file_get_contents(flag);文件名尽量短还可以利用$_GET动态传参把参数名和值分离例如cmd$_GET[0];0system(ls)用短下标携带真正内容。与文件包含结合eval执行前先把payload写入临时文件再用包含机制触发这是复杂RCE链的雏形。与反序列化结合eval执行unserialize($_POST[data])后面跟一条反序列化链就能从“代码执行”升级到“任意文件操作”甚至“命令执行”。前端JavaScript的eval虽然本题是PHP但搜索热词里出现过“deep eval框架”它本质是JavaScript里对eval行为的深度封装。前端如果直接用eval渲染用户输入同样会引入XSS。理解“字符串被当代码”这个核心语言差异只是外壳。这些进阶变种说明了一个道理eval题只是入口背后通着的是整个Web安全的知识网络。把一道题吃透胜过稀里糊涂刷十道题。5. 从CTF回到真实业务eval漏洞的防御与加固5.1 真实场景里的eval使用误区CTF题目源于真实漏洞这句话在eval题上体现得淋漓尽致。在真实业务里eval被用错的场景集中在这么几处一类是模板引擎实现。有些老项目为了实现动态模板会把用户编辑的模板内容直接拼进字符串再eval渲染。比如用户在前台编辑了“首页公告”后台代码把公告内容塞进一个PHP模板字符串然后eval输出。只要用户内容里包含;phpinfo();之类的代码整台服务器就沦陷了。另一类是插件系统。部分CMS和框架允许加载“功能插件”插件内容如果是PHP代码然后系统用eval去加载那等于允许任意代码执行。更隐蔽的用法是把配置项甚至数据库字段内容当作代码执行比如某个字段存的是“计算公式”代码里eval(return .$field.;)字段一旦可控就是RCE。还有一类是WebShell工具留下的后门。很多管理工具为了“方便”会把assert($_POST[x])这种格式当作加密后门老版本PHP里这是真实可用的。安全扫描时如果搜到eval(、assert(出现在业务代码里基本可以判定为高风险。5.2 一份可落地的防御清单防御eval漏洞必须从代码层面和运行层面同时下手。这里整理一份检查清单开发和运维都能直接用从根上避免生产代码禁止使用eval、assert执行动态字符串。如果确实要“动态执行逻辑”优先选白名单机制把所有可能执行的动作提前枚举好用户只能选择编号而不是输入代码。严格输入校验如果无法完全避免eval执行前的变量必须做严格的白名单校验。比如只允许字母数字组合的标识符长度限制敏感字符串黑名单等。但要注意黑名单永远是下策因为绕过姿势太多白名单才是靠谱做法。禁用危险函数在php.ini的disable_functions里加上system、exec、shell_exec、passthru、popen、proc_open等命令执行函数。再次强调它禁不了eval但能切断“代码执行到命令执行”的转变增加攻击者利用成本。最小权限运行Web服务运行账号尽量不用root目录权限做到只读为主、写入目录单独隔离。即使被RCE攻击者也没有权限读取密钥、修改系统文件。部署WAF和规则引擎在Nginx或CDN层面对eval(、assert(、system(、cat /etc/passwd这类关键字做拦截。虽然WAF能被绕过但能挡住大部分批量扫描和脚本小子。上RASP运行时应用自我保护在更成熟的基础设施里RASP可以在函数执行层面对敏感调用做检测即使代码里出现了eval也能在运行期判断调用栈是否包含用户输入发现危险行为直接拦截。这是目前对抗代码执行漏洞的强有力手段。5.3 自查方法如何审计自己项目的代码给老代码做安全自查不需要上多复杂的工具先从搜索危险函数开始。用IDE全局搜索下面这些关键词逐一确认它们的调用链eval( assert( preg_replace(.*\/e call_user_func( call_user_func_array( array_map( create_function( system( exec( shell_exec( passthru( popen( proc_open(对搜到的每一处重点问三个问题传给这些函数的字符串里有没有一部分来自用户输入GET、POST、Cookie、Header、文件上传内容如果有用户输入之前有没有经过严格的白名单校验还是只是简单过滤如果被攻击者控制最坏会产生什么后果是信息泄露、代码执行还是命令执行把这三个问题写成表格每一条危险调用背后的数据流都追一遍基本上就能把高风险的eval漏洞揪出来。我在实际做代码审计时还会额外检查框架层有没有把“模板字符串”直接交给eval的隐藏逻辑这类问题比直接搜eval(更难发现。另外提一嘴在代码审查工具的选择上PHP项目可以用PHPStan、Psalm做静态分析也能扫出一部分危险模式。但对eval这种动态调用静态分析工具经常误报或漏报最终还是离不开人工审计。这也是为什么安全圈一直强调“代码审计是手艺活”。我个人在实际操作中体会最深的一点是只要代码里出现“用户输入 eval”这个组合不管它外面套了多少层白名单都默认它是漏洞。因为攻击者能构造的输入空间远比开发者在写代码时想象的大得多——编码、注释、换行、超长字符串、畸形语法随便一个角度都可能击穿看似严密的过滤逻辑。做CTF题时是在跟出题人斗智斗勇写业务代码时则要假设对面是无限制的对手所以最简单可靠的方案永远是别让eval碰到用户输入。这道Bugku的eval题只是开始顺着代码执行漏洞这条路走下去你还会遇到很多更有意思的挑战。每次拿到flag之后多问一句“为什么能打进去”“代码层怎么修”刷过的每一道题都会变成你自己的安全直觉。
返回列表