ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

不安全文件上传(Client)绕过与修复实战:前端校验识别到服务端加固

不安全文件上传(Client)绕过与修复实战:前端校验识别到服务端加固 这一课是我这期黑客笔记里比较靠后的一课了第33课主题是“不安全文件上传Client”。很多人一听到文件上传漏洞脑子里蹦出来的都是服务端过滤怎么绕、后缀怎么改、解析漏洞怎么打——但这一课特意把 Client 写在标题里就是想先补上一个概念在你遇到的上传点里有一大类“看着很严、实际一戳就破”的情况问题根本不复杂只是校验逻辑只写在了浏览器这一侧。我见过不少刚入行的朋友拿到一个上传点就急着上大payload结果连最简单的前端限制都没绕干净还一脸懵地问我为什么抓包改了Extension还是失败。这课说白了就是解决两件事第一让你一眼认出哪些校验是 Client 侧的“纸老虎”第二给你一套无论前端口味怎么变都能直接套用的绕过和验证流程。做渗透测试的、写业务后端的、搞CTF Web方向的都能从中找到自己能用的东西。下面直接进入正题。1. 课程定位为什么单把“Client”拎出来讲1.1 上传校验的两层世界前端JS与后端程序任何文件上传功能本质上都有两道“安检门”。第一道门在浏览器里由 HTML、JavaScript 负责它拦的是普通用户不小心传错文件属于用户体验优化第二道门在服务器上由 PHP、Java、Python、Go 这类后端代码负责它才真正决定哪些文件能落到磁盘上、能不能被当作代码执行。用个经常说的类比前端校验像小区门口的保安你进门时他抬头看你一眼眼熟就挥手放行后端校验才是物业系统进门之后刷没刷卡、你是住客还是临时工、有没有权限进机房都是后面这套系统说了算。问题在于小区保安是可以“收买”的——而前端的 JavaScript 本身就是公开交付给用户的代码用户随时可以改、可以停、可以绕过。前端校验做得再花哨只要后端没做校验就等于保安一眼都没看就放人进了机房。我一直强调看到上传功能先别急着分析后缀和 Content-Type先分清你面对的校验到底在哪一层。判断标准很简单请求是浏览器发出的响应是服务器返回的。前端校验只是浏览器在你提交前自己执行的逻辑它根本不参与网络请求所以任何仅存在于前端的限制对于改包者来说都是透明的。这一点是这一课理解上的关键前提后面所有绕过动作都是围绕它展开的。1.2 一个“不安全文件上传(Client)”的完整画像这类漏洞的典型画像是什么样的我把它归纳成三个特征。第一个特征页面上有明显的类型限制提示比如“只支持 JPG/PNG/GIF 格式”但抓包之后你会发现请求里除了文件名和 Content-Type 之外没有任何额外的安全校验字段服务器就默默把文件收下了。第二个特征你用浏览器正常上传一张图片能成功但把图片内容换成 PHP/Python/JSP 脚本、只把文件名改成 .jpg 再抓包放行服务器依然能收下甚至还能直接访问到。第三个特征响应包或者 UI 提示里出现“上传成功”但页面没有提示任何文件内容、文件头、服务器端重命名之类的结果——说明后端基本处于“裸奔”状态。危害就不用我多说了。一旦攻击者能把可控内容的文件写进服务器就等于往你家门缝里塞了一个能自己挪动的东西它可能是个可执行脚本可能是个钓鱼页面也可能是伪装成正常资源的恶意文件。如果这个上传点所在的服务还能解析脚本那后果就彻底升级了从“能传文件”变成“能执行命令”后续就是大家熟悉的提权、横向、留后门一条龙。有一点这里必须说清楚以上所有讨论包括后面的实操你都只能在自己的靶机、授权的测试环境或者漏洞赏金项目中去做。拿不授权的网站练手那是给自己挖坑不是搞安全该有的样子。心里这条红线立住了再往下看。2. 客户端校验的常见形态与识别方法2.1 三类最常见的客户端校验前端校验虽然写法千奇百怪但翻来覆去不外乎三类。把这三类认熟你在判断一个上传点是不是“纸老虎”时效率会高很多。第一类是扩展名白名单/黑名单校验。页面上通常会写“仅支持图片”JS 里则用正则或者字符串截取去判断文件名后缀。这类代码我在真实业务里见得太多了典型写法长这样function checkFile(file) { const ext file.name.split(.).pop().toLowerCase(); const allowList [jpg, jpeg, png, gif]; if (!allowList.includes(ext)) { alert(只允许上传图片格式); return false; } return true; }第二类是 MIME 类型校验。它检查的是文件对象的 type 属性比如 image/jpeg、image/png。这里有个很容易让人误判的细节浏览器里的 file.type 值来源于操作系统对这个文件的识别并不完全等于文件真实内容。把一个 .txt 改名成 .jpg大多数浏览器照样会把它识别成 image/jpeg因为很多系统只根据扩展名推断 MIME。所以这类校验比第一类还要虚。第三类是文件大小与数量校验。限制只能传一张、不能超过 2MB 之类的一般用 File.size 或 Blob 属性判断。这类限制主要是为性能和业务服务跟安全边界几乎不搭边但我在测试中见过把大小校验和类型校验写在一起的情况所以也归进来。我把它们整理成一张表方便对照校验类型典型实现抓包时观察点实际安全性扩展名校验JS 正则、accept 属性文件名后缀完全可控MIME校验file.type 判断Content-Type 字段完全可控大小/数量校验File.size、文件数量判断请求体大小、文件个数仅影响正常用户混合校验以上两两组合多个字段都可能是假的只要服务端不校验全等于02.2 怎么快速判断当前上传点是否只做了Client校验判断一个上传点是否只依赖前端校验不需要什么高深工具浏览器加一个代理就够。我平时按下面这个顺序来速度很快。先从最简单的看起在页面上右键查看源代码或者用开发者工具直接看上传表单关联的 JS找找有没有 checkFile、checkType、beforeSubmit 这类函数。找到之后跟着代码走一遍看它是不是只做了上面说的那几类检查有没有发起任何额外的校验接口。如果前端 JS 里拦完就直接让表单提交了那就说明这个上传点大概率没走服务端校验流程得重点测试。再看服务器到底校验了什么最快的办法是“作弊”正常上传一个 .jpg 图片成功后再传一个把扩展名改成 .jpg 的文本文件也用正常的表单方式提交看服务器接不接受。如果服务器接受了说明后端要么没有校验要么只校验了扩展名。如果服务器拒绝就再结合返回包判断看它报的是“格式不支持”还是“内容不是图片”。还有一个我常用的终极大法直接禁用 JavaScript 再上传。绝大多数浏览器都有禁用 JS 的入口禁用后刷新页面如果上传功能照样能弹出来、照样能提交成功那你面前这个上传点前端校验就等于不复存在了。有些人会问这么做会不会影响页面本身确实会影响一部分动态页面但对绝大多数上传功能来说禁 JS 之后只是少了弹窗提示提交动作本身不受影响这恰好证明了前端限制不是生效的边界。判断这步走完基本就能给上传点定性了是“裸奔型”还是“穿了铠甲”。但很多人的误区是觉得判断完就可以直接上payload了。别急我们先把绕过前面这层“纸窗户”的手法练扎实。3. 绕过前端限制的三板斧实操3.1 在本地搭一个练习靶场实操之前先准备环境我推荐用 Docker 直接拉起一个 upload-labs 或者 DVWA几分钟就完事而且能随便折腾。upload-labs 是老牌的靶场库每一关都是独立的上传校验场景非常适合练前面说的判断手法。# 拉取并启动 upload-labs docker run -d --name upload-labs -p 8080:80 c0ny1/upload-labs跑起来之后浏览器访问 http://127.0.0.1:8080/选一个关卡进去就能看到带前端校验的上传表单。DVWA 也可以在 Docker 里搜 dvwa 官方镜像就能拉起来它的文件上传模块 Low 级别就是一个非常典型的只做客户端校验的例子。环境这东西我建议每个人都在本地备一套。测试手法这东西光看永远学不会很多细节只有亲手操作才会形成肌肉记忆。而且本地靶场可以让你放心大胆地去试试错了也不怕这对建立“请求包视角”很重要。3.2 第一斧直接禁用JavaScript这是最原始但很有效的一招。在浏览器设置里把 JavaScript 关闭刷新上传页面你会发现原本“只支持图片”的限制提示不再弹出使用文件选择器选中一个 .php 文件上传表单照样能提交。为什么有效因为前端校验这段 JS 根本就没执行。禁用了 JS等于把小区门口的保安调走了你直接大摇大摆走到物业前台如果物业前台不问你问题你就能进去。在靶场上这一招的基本玩法是先正常上传一张图片看响应再禁用 JS 传一个改了后缀的文件对比两次响应。但现实中这招效率不算高因为很多现代前端框架的交互逻辑强依赖 JS禁用 JS 后页面可能直接白屏或报错而且如果目标环境里有服务端校验禁用 JS 并不能替你绕过它。所以这一斧更多是验证性质告诉你“这个上传点在前端这一侧没有任何真防线”真正的重头戏在后面两斧。3.3 第二斧从开发者工具里改JS逻辑既然前端 JS 是在浏览器里执行的那直接让这段校验逻辑失效也是一种路线。打开开发者工具找到上传按钮绑定的事件监听器把调用校验函数的地方注释掉或者把校验函数的返回值强行改成 true再触发上传结果和禁用 JS 相似。以我们前面贴的那段 JS 为例你甚至不用改代码直接在 Console 里重新定义这个函数function checkFile(file) { return true; }后面再上传文件时浏览器调用的就是被覆盖后的函数。这个技巧在测试老系统时偶尔会省事因为它不用动整个页面的 JS 开关。但说句实在话这一招在安全测试里更多是教学意义。因为浏览器前端代码怎么改都是你自己这一侧的修改最终还是得靠发出的 HTTP 请求说话。改 JS 本质上是“让前端放行”但真正重要的是“让服务器收下”。明白这一点你就自然能理解为什么下一斧才是核心。3.4 第三斧Burp抓包直接改请求彻底绕开前端核心来了。不管前端校验写了多少行最终上传动作都会变成一次 HTTP 请求。Burp Suite 这类代理工具能让你在请求到达服务器之前拦截下来把里面的字段改到你想要的样子。这是绕过所有客户端校验的根本手段因为它绕开的不是某一层规则而是整个“客户端”这个角色。具体操作流程我拆成步骤写清楚。第一步配置好 Burp 的代理监听浏览器的代理指向 127.0.0.1:8080确保 Burp 能抓到目标上传页面的流量。第二步在靶场上正常选择一个图片文件比如新建一个 text.txt 文件改成 test.jpg先不过分追求内容目的是让请求产生。点击上传Burp 里立刻就能拦到一个 multipart/form-data 类型的 POST 请求。第三步找到请求体里的关键字段。典型的上传请求长这样POST /upload.php HTTP/1.1 Host: 127.0.0.1:8080 Content-Type: multipart/form-data; boundary----WebKitFormBoundary7MA4YWxkTrZu0gW ------WebKitFormBoundary7MA4YWxkTrZu0gW Content-Disposition: form-data; namefile; filenametest.jpg Content-Type: image/jpeg [文件内容] ------WebKitFormBoundary7MA4YWxkTrZu0gW Content-Disposition: form-data; namesubmit upload ------WebKitFormBoundary7MA4YWxkTrZu0gW--第四步修改 filename 字段把 test.jpg 改成 test.php。同时把相邻的 Content-Type 改成 image/jpeg 或 application/octet-stream——这一步很有讲究因为有些后端会检查 MIME你先伪装成图片等服务器把文件收进去再看它是否按图片处理。第五步点击 Forward 放行回到浏览器看响应。如果响应显示上传成功再直接访问上传目录下对应路径比如 http://127.0.0.1:8080/uploads/test.php看服务器是否真的把文件按 PHP 解析了。说句很多人会惊讶的事实相当一部分老系统在这一步就已经失守了。因为它们把前端那几行 JS 当成了全部安全措施后端什么都没做。这也是为什么我一直强调拿到上传点第一反映应该是“抓包看看”而不是“猜它怎么过滤的”。3.5 后端没有任何校验时会发生什么如果我们抓包改完文件后端直接收下并返回了路径说明这个上传点已经踩过了“收文件”这一关。为了确认它是否还能被当成代码执行我们需要做一个无害验证不要一上来就上保真木马那样既危险又没有必要。安全测试里常用的做法是上传一个只输出指定内容的测试脚本比如?php echo upload-ok; ?扩展名改成 .php内容就这一行没有任何有害逻辑。上传成功之后访问上传地址如果页面输出 upload-ok说明服务器真的把 PHP 当成代码执行了——到这一步漏洞的影响级别就非常清晰了攻击者后续可以做的就是往同一条链路上放置任何他想要的脚本文件。这里必须强调这个测试文件本身是干净的它不会帮你做任何越权行为只是帮你验证“可执行”这个结论。从这往后如果你继续深入那已经超出了本课作为“不安全文件上传Client”这个主题的防御和识别范畴而且非常容易走偏。我的建议是验证到这里你已经能给漏洞定性上传点无服务端校验、可上传任意类型文件、可能被当作脚本执行。这个结论足以驱动后续修复了。4. 修复与自检从源头上堵住这个漏洞4.1 服务端必须接管完整的校验职责既然客户端校验可以被绕过那么唯一可靠的防线只能是服务端。服务端要做的事情描述起来就一句话不信任任何浏览器传上来的内容。这句话执行起来需要分成几步。第一步是扩展名白名单。注意是白名单不是黑名单黑名单永远列不全。常见图片扩展名就那几种jpg、jpeg、png、gif、webp用数组固定住即可。第二步是文件内容和 MIME 类型校验。用服务端函数读取文件的真实类型不要信请求头里那个 Content-Type。第三步是限制文件大小。前端限制不管用后端在接收文件时同样要卡一道大小上限。第四步是对文件内容做深度检查比如用图片库尝试重新解码能正常解码才认为是合法图片。一个能体现这几步思路的 PHP 示例大概长这样$file $_FILES[file] ?? null; if (!$file) { die(未收到文件); } // 检查是否上传出错 if ($file[error] ! UPLOAD_ERR_OK) { die(上传失败); } // 1. 用真实文件头判断 MIME而不是请求里的 Content-Type $finfo finfo_open(FILEINFO_MIME_TYPE); $realMime finfo_file($finfo, $file[tmp_name]); finfo_close($finfo); $extMap [ image/jpeg jpg, image/png png, image/gif gif, image/webp webp, ]; if (!isset($extMap[$realMime])) { die(不允许的文件类型); } // 2. 校验扩展名是否匹配 $ext strtolower(pathinfo($file[name], PATHINFO_EXTENSION)); if ($extMap[$realMime] ! $ext) { die(扩展名与文件内容不一致); } // 3. 限制大小200KB以内 if ($file[size] 204800) { die(文件过大); } // 4. 重命名避免用户控制文件名 $newName date(YmdHis) . _ . bin2hex(random_bytes(8)) . . . $ext; if (!move_uploaded_file($file[tmp_name], __DIR__ . /uploads/ . $newName)) { die(保存失败); } echo 上传成功: . htmlspecialchars($newName);这段代码不是唯一标准答案但四个关键点都覆盖了真实 MIME、扩展名白名单、大小限制、不可预测的文件名。尤其是最后一点很多开发者会忘。只要文件名由服务器随机生成攻击者就很难提前猜出上传后的地址这能显著降低上传成果被利用的概率。4.2 存储与执行分离别让上传点变成运行点代码层面校验做足了还有一个容易被忽视的隐患上传目录可执行脚本。很多系统把上传目录放在 Web 根目录里后缀校验又刚好被绕过那文件就能直接通过 URL 访问并执行。修复思路是存储和解析彻底隔离。具体做法有三类。一类是把上传目录配置成禁止执行脚本。以 Nginx 为例如果你把上传目录放在 /uploads 下可以在配置里显式禁掉该目录下的脚本解析location ^~ /uploads/ { location ~ \.php$ { deny all; } }Apache 的话可以用 .htaccess 或者在虚拟主机配置里加 FilesMatch 规则FilesMatch \.(?i:php|php5|phtml)$ Require all denied /FilesMatch另一类是直接把上传目录放到 Web 根目录之外文件不通过 URL 直接访问而是由后端程序按需读取并输出到页面。这样可以彻底杜绝脚本执行因为请求根本不会经过文件落盘目录。还有一类是额外的纵深防御对上传的图片做二次渲染。用 PHP 的 GD 库或 Java 的 ImageIO 把图片重新解码再编码原本藏在图片里的代码往往在这个过程中被破坏掉无法再正常解析执行。我个人做项目复盘时见过太多“校验全做了但忘了改上传目录权限”的例子。尤其是 Windows IIS 环境解析配置和历史遗留目录的问题更容易炸。所以上传目录的执行权限必须当作单独一项写进自检清单。4.3 开发与测试的最终自检清单不管你是开发自测还是安全测试复测下面这张表基本可以作为文件上传点的验收标准。我把它贴出来直接照着打勾就行。自检项测试方法通过标准服务端扩展名校验抓包把 filename 改成 .php/.jsp/.asp请求被拒绝或文件不落盘服务端内容校验上传一个改名 .jpg 的文本文件请求被拒绝真实文件头校验上传带图片头脚本内容的文件请求被拒绝或重编码后失效文件大小限制上传超大文件请求被拒绝文件名随机化连续上传两次观察文件名文件名不可预测上传目录禁止执行脚本成功上传后访问 .php 路径不解析代码或返回403存储目录隔离检查文件落盘路径目录不可被 URL 直接访问这张表看着简单但每一条都能对应到现实漏洞案例。我在实际工作中发现很多团队做完前三项就认为“上传安全了”结果一测目录解析漏洞还开着前面等于白做。所以自检一定要按整条链路来不能只看单一环节。5. 踩坑记录与排障技巧5.1 改了filename还是失败不要只怀疑前端很多人在抓包时改了 filename 为 test.php放行后发现响应报错第一反应是“我这改法不对”。其实这时候要冷静下来报错的原因可能有好几种第一种是服务端有白名单改了后缀直接被拒绝第二种是服务端检查了文件真实内容改了后缀但文件内容还是图片所以被拦第三种是服务端在你上传后做了重命名你就算传 .php 进去出来也可能变成一串随机字符串加 .jpg。正确做法是先看响应包的内容和状态码。如果返回 200 且提示“上传成功”那说明服务端收下了只是可能重命名了如果返回 200 但提示“格式不支持”那基本可以判定后端有白名单校验可以再去试大小写、双扩展名。如果返回 500大概率是文件内容触发了服务端的异常处理或者安全组件。一层层排查比反复乱改文件名要快得多。5.2 Content-Type改成了image/jpeg还报错这又是一个经典误区。很多人以为只要把请求里的 Content-Type 改成 image/jpeg后端就会认为这是图片。对一部分没经验的后端程序来说确实如此但现在的安全组件很多会用文件内容识别真实类型最常用的就是 magic bytes也就是文件开头的几个十六进制字节。比如 JPEG 文件开头通常是 FF D8 FFPNG 是 89 50 4E 47GIF 是 47 49 46 38。如果后端用这类信息校验你光改请求头是没用的因为你上传的文件内容第一个字节还是 3C对应 一看就不是图片。这种情况下如果你确定要验证这个上传点是否存在更深的漏洞常见的思路是把脚本代码拼到一张合法图片后面比如在图片末尾追加一段 PHP 代码再上传。但这属于更复杂的绕过场景而且需要环境确实存在包含漏洞才能利用。在入门阶段你至少应该知道请求头里的 Content-Type 是“自报家门”服务器信不信是服务器的事。所以遇到报错先看返回信息再看服务器日志不要上来就怀疑自己改包工具坏了。5.3 靶场与生产环境的差异很多人用靶场练完手换到实际授权测试环境时会有落差最常见的就是靶场里改个后缀就成功了生产环境上传后访问总是 404 或者被拦截。原因通常是生产环境多了 CDN、WAF、云锁、主机安全 Agent 之类的防护组件它们可能在多个层面对上传做检测包括文件名、文件内容、请求频率、UA 指纹等。遇到这类情况先把测试收敛到“验证防护是否存在”这一步正常上传一张图片看是否成功再上传一个无害的探测文件看是否被拦截。如果探针文件被拦不要立刻怀疑自己手法不对而是要考虑是不是安全产品生效了这本身也是一个有效结论。另外 CDN 缓存会造成“上传成功但访问 404”的假象遇到时先绕开 CDN直接解析回源地址测试或者等待缓存过期再确认。5.4 一个排障顺序的速查表测试过程中容易一头扎进细节忘了全局排障。我总结了一个通用顺序每次遇到上传点异常就按这个走基本不会卡太久现象优先怀疑下一步动作前端弹窗拦截只做了客户端校验直接抓包检查请求是否发出请求发出但提示格式错服务端白名单看响应内容试大小写/双扩展名请求发出且提示成功但访问404文件名被服务端重命名查看响应中的路径字段和实际落盘目录请求发出且提示成功访问不解析上传目录禁止脚本执行确认响应头、目录权限、解析配置上传成功但请求明显变慢有安全软件在扫描文件内容查看后端日志和杀软日志确认这张表不是万能的但它能帮你快速定位 80% 的卡壳场景。剩下的 20%大概率出在业务逻辑的特殊分支比如某些接口只接收 base64某些平台会把上传文件同步到第三方存储这些需要针对具体业务单独分析。最后分享一个我自己的小习惯无论页面上提示我上传什么类型、前端校验做得多么花哨我第一件事永远是打开代理工具抓包看实际请求。因为页面给你的所有提示都是 Client 在跟你聊天而真正决定你能不能拿到结果的是 Server 收到请求之后做了什么。牢记这一点你在文件上传这条路上踩的坑至少能少一半。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表