java反序列化笔记
•获取类的⽅法: forName
•实例化类对象的⽅法: newInstance
•获取函数的⽅法: getMethod
•执⾏函数的⽅法: invoke
•obj.getClass() 如果上下⽂中存在某个类的实例 obj ,那么我们可以直接通过
obj.getClass() 来获取它的类
•Test.class 如果你已经加载了某个类,只是想获取到它的 java.lang.Class 对象,那么就直接
拿它的 class 属性即可。这个⽅法其实不属于反射。
•Class.forName 如果你知道某个类的名字,想获取到这个类,就可以使⽤ forName 来获取
⾸先调⽤的是 static {} ,其次是 {} ,最后是构造函数。 其中, static {} 就是在“类初始化”的时候调⽤的,⽽ {} 中的代码会放在构造函数的 super() 后⾯,
但在当前构造函数内容的前⾯。
所以说, forName 中的 initialize=true 其实就是告诉Java虚拟机是否执⾏”类初始化“。
我们经常在一些源码里看到,类名的部分包含 $ 符号,比如fastjson在 checkAutoType 时候就会
先将 $ 替换成 . :https://github.com/alibaba/fastjson/blob/fcc9c2a/src/main/java/com/alibaba/fa stjson/parser/ParserConfig.java#L1038。 $ 的作用是查找内部类
我们正常执行方法是 [1].method([2], [3], [4]...) ,其实在反射里就是
method.invoke([1], [2], [3], [4]...) 。
方法 getConstructor 。和 getMethod 类似, getConstructor 接收的参数是构造函数列表类型,因为构造函数也支持重载,所以必须用参数列表类型才能唯一确定一个构造函数。获取到构造函数后,我们使用 newInstance 来执行
另一种执行命令的方式ProcessBuilder
对于可变长参数,Java其实在编译的时候会编译成一个数组,也就是说,如下这两种写法在底层是等价
的(也就不能重载):
public void hello(String[] names) {} public void hello(String...names) {}
•getMethod 系列方法获取的是当前类中所有公共方法,包括从父类继承的方法
•getDeclaredMethod 系列方法获取的是当前类中“声明”的方法,是实在写在这个类里的,包括私
有的方法,但从父类里继承来的就不包含了
•setAccessible ,这个是必须的。我们在获取到一个私有方法后,必须用
setAccessible 修改它的作用域,否则仍然不能调用。
⼀个RMI Server分为三部分:
1.⼀个继承了 java.rmi.Remote 的接⼝,其中定义我们要远程调⽤的函数,⽐如这⾥的 hello()
2.⼀个实现了此接⼝的类
3.⼀个主类,⽤来创建Registry,并将上⾯的类实例化后绑定到⼀个地址。这就是我们所谓的Server
⾸先客户端连接Registry,并在其中寻找Name是Hello的对象,这个对应数据
流中的Call消息;然后Registry返回⼀个序列化的数据,这个就是找到的Name=Hello的对象,这个对应
数据流中的ReturnData消息;客户端反序列化该对象,发现该对象是⼀个远程对象,地址
在 192.168.135.142:33769 ,于是再与这个地址建⽴TCP连接;在这个新的连接中,才执⾏真正远程
⽅法调⽤,也就是 hello() 。
RMI Registry就像⼀个⽹关,他⾃⼰是不会执⾏远程⽅法的,但RMI Server可以在上⾯注册⼀个Name
到对象的绑定关系;RMI Client通过Name向RMI Registry查询,得到这个绑定关系,然后再连接RMI
Server;最后,远程⽅法实际上在RMI Server上调⽤
Java对远程访问RMI Registry做了限制,只有来源地址是localhost的时候,才能调用rebind、
bind、unbind等方法。
只要目标服务器上存在一些危险方法,我们通过RMI就可以对其进行调用,之前曾经有一个工具 https://github.com/NickstaDB/BaRMIe,其中一个功能就是进行危险方法的探测。
在序列化Java类的时候用到了一个类,叫 ObjectOutputStream 。这个类内部有一个方法
annotateClass , ObjectOutputStream 的子类有需要向序列化后的数据里放任何内容,都可以重写
这个方法,写入你自己想要写入的数据。然后反序列化时,就可以读取到这个信息并使用。
PHP的序列化是开发者不能参与的,开发者调用 serialize 函数后,序列化的数据就已经完成了,你得
到的是一个完整的对象,你并不能在序列化数据流里新增某一个内容,你如果想插入新的内容,只有将
其保存在一个属性中。也就是说PHP的序列化、反序列化是一个纯内部的过程,而其 sleep 、wakeup 魔术方法的目的就是在序列化、反序列化的前后执行一些操作。其实大部分PHP反序列化漏洞,都并不是由反序列化导致的,只是通过反序列化可以控制对象的属性,进而在后续的代码中进行危险操作
Extends可以理解为全盘继承了父类的功能。implements可以理解为为这个类附加一些额外的功能;interface定义一些方法,并没有实现,需要implements来实现才可用。extend可以继承一个接口,但仍是一个接口,也需要implements之后才可用。对于class而言,Extends用于(单)继承一个类(class),而implements用于实现一个接口(interface)。
序列化反序列化示例Person类需加main函数来看输出结果 public static void main(String[] args) throws Exception{ ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("test.txt")); Person p = new Person("312",3); out.writeObject(p); ObjectInputStream in = new ObjectInputStream(new FileInputStream("test.txt")); in.readObject(); }
URLDNS
调试URLDNS过程按p神介绍断点在putvul中第一次计算hash为加载instrument.dll(应该是idea用于debug)
可以断点在ht.put(u, url);可以看到完整调用链
p神的介绍因为是入门比较简单,比较好玩的是su18改版的urldns,用来判断是否存在gadget需要的类
https://mp.weixin.qq.com/s/KncxkSIZ7HVXZ0iNAX8xPA
https://su18.org/post/gadgetor/
https://github.com/kezibei/Urldns
CC1
总体介绍和问题解析
CC链整体可以看这个
https://www.cnblogs.com/litlife/p/12571787.html#transformer
总的来说就是AnnotationInvocationHandler反序列化过程中通过memberValues.entrySet()调用了,这里的memberValues是proxymap,mapProxy.entrySet(),任意proxy的方法调用都会调用proxy代理的invoke方法,这里的proxy设置为AnnotationInvocationHandler,所以调用AnnotationInvocationHandler的invoke方法,他的memberValues是lazymap,调用lazymap的get方法,如果key不存在就调用transfer方法,开启调用ChainedTransformer数组利用链
ObjectInputStream.readObject() -> AnnotationInvocationHandler.readObject() -> this.memberValues.entrySet() = mapProxy.entrySet() -> AnnotationInvocationHandler.invoke() -> this.memberValues.get(xx) = LazyMap.get(not_exist_key) -> ChainedTransformer.transform() -> InvokerTransfomer.transform() -> RCE
cc1不能用的原因
MapstreamVals = (Map)fields.get("memberValues", null); 强制类型转化吧我们的memberValues转换为Map,但实际调试可以发现此时的memberValues是proxy代理对象,所以会出错导致streamVals为null,我们设置的proxy没能成功传入反序列化过程,到了下面取null的entrySet自然取不到值
p牛是用transformedMap复现的,所以他说的高版本不能用的原因是他理解的transformedmap的原因,不是工具中使用lazymap在高版本不能用的原因
ysoserial在构造Transformers时,为什么要用那么多反射?
因为Runtime类没有继承Serializable接口,所以不能被序列化,在序列化时会出错,使用反射可以解决这个问题
反射代码解析
•在实例化这个InvokerTransformer时,需要传⼊三个参数,第⼀个参数是待执⾏的⽅法名,第⼆个参数
是这个函数的参数列表的参数类型,第三个参数是传给这个函数的参数列表。https://www.cnblogs.com/zpchcbd/p/14726562.html
p神的文章中设置如下参数 new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",new Class[0]}), 第二个参数是因为 getMethod 方法的第二个参数类型是 Class[],如果不需要传递任何参数,可以使用一个空的类对象数组。 其实不传输一个空参也可以,默认置空 new InvokerTransformer("getMethod",new Class[]{String.class},new Object[]{"getRuntime"}),
•ConstantTransformer是实现了Transformer接⼝的⼀个类,它的过程就是在构造函数的时候传⼊⼀个
对象,并在transform⽅法将这个对象再返回
•TransformedMap⽤于对Java标准数据结构Map做⼀个修饰,被修饰过的Map在添加新的元素时,将可
以执⾏⼀个回调。我们通过下⾯这⾏代码对innerMap进⾏修饰,传出的outerMap即是修饰后的Map.其中,keyTransformer是处理新元素的Key的回调,valueTransformer是处理新元素的value的回调。
我们这⾥所说的”回调“,并不是传统意义上的⼀个回调函数,⽽是⼀个实现了Transformer接⼝的类。
•Transformer是⼀个接⼝,它只有⼀个待实现的⽅法,TransformedMap在转换Map的新元素时,就会调⽤transform⽅法,这个过程就类似在调⽤⼀个”回调
函数“,这个回调的参数是原始对象。
•ChainedTransformer也是实现了Transformer接⼝的⼀个类,它的作⽤是将内部的多个Transformer串
在⼀起。通俗来说就是,前⼀个回调返回的结果,作为后⼀个回调的参数传⼊。按顺序调用 Transformer 数组 this.iTransformers 中所有 Transformer 对象的 transform 方法,并且每次调用的结果传递给下一个 Transformer#transform() 作为参数
public Constructor<T> getDeclaredConstructor(Class[] parameterType) throws NoSuchMethodException, SecurityException
怎么触发回调呢?就是向Map中放⼊⼀个新的元素
动态代理
https://www.cnblogs.com/yy3b2007com/p/9065303.html
http://1.15.187.227/index.php/archives/457/
•InvocationHandler接口是proxy代理实例的调用处理程序实现的一个接口,每一个proxy代理实例都有一个关联的调用处理程序;在代理实例调用方法时,方法调用被编码分派到调用处理程序的invoke方法
•AnnotationInvocationHandler 判断第一个参数必须是注释类
•JAVA中间件在对序列化的AnnotationInvocationHandler类的对象数据进行反序列化时,会调用其readObject方法并触发漏洞
•因为 sun.reflect.annotation.AnnotationInvocationHandler 是在JDK内部的类,不能直接使用new来实例化。使用反射获取到了它的构造方法,并将其设置成外部可见的,再调用就可以实例化了
•sun.reflect.annotation.AnnotationInvocationHandler 构造函数的第一个参数必须是
Annotation的子类,且其中必须含有至少一个方法,假设方法名是X
•被 TransformedMap.decorate 修饰的Map中必须有一个键名为X的元素。所以,这也解释了为什么我前面用到 Retention.class ,因为Retention有一个方法,名为value;所以,为了再满足第二个条件,我需要给Map中放入一个Key是value的元素
lazymap
•lazymap是懒加载,在get找不到值的时候,它会调用factory.transform 方法去获取一个值。
ConstantTransformer(1)
最后会增加一个ConstantTransformer(1),隐蔽了启动进程的日志特征:
CC6
CC6
GPT侠时间
`org.apache.commons.collections.keyvalue.TiedMapEntry` 是 Apache Commons Collections 库中的一个类,它实现了 `java.util.Map.Entry` 接口。它的作用是将一个已存在的键值对(键和值)绑定到指定的 `Map` 实例。 具体来说,`TiedMapEntry` 类提供了以下功能: 1. 将键值对绑定到 `Map` 实例:通过构造函数,你可以将一个已存在的键值对绑定到 `TiedMapEntry` 对象,并指定要绑定的 `Map` 实例。这样,`TiedMapEntry` 对象就代表了该键值对在指定的 `Map` 中的绑定关系。 2. 实现 `Map.Entry` 接口:`TiedMapEntry` 类实现了 `Map.Entry` 接口,它提供了访问和操作键值对的方法。你可以使用 `TiedMapEntry` 对象来获取键和值,并对其进行读取或修改。 3. 与绑定的 `Map` 实例保持同步:`TiedMapEntry` 对象与绑定的 `Map` 实例保持同步,这意味着对 `TiedMapEntry` 对象的操作实际上会直接影响到绑定的 `Map` 实例中对应的键值对。反之亦然,对绑定的 `Map` 实例中对应的键值对的修改也会反映在 `TiedMapEntry` 对象上。 通过使用 `TiedMapEntry`,你可以方便地将现有的键值对绑定到指定的 `Map` 实例,并使用 `TiedMapEntry` 对象对键值对进行访问和操作。这对于在某些场景下需要绑定和同步键值对的应用程序非常有用。
直接看cc6不如先看cc5,cc6的链子很简单,问题在于要remove我们的键值,p神给的解释是
问题出在第一遍调用put时候触发反序列化链时候lazymap#get方法没有找到key去触发transform后把键值put到map中,在反序列化过程中再次触发get方法直接再map中找到键值了,就不会触发后续的transform方法了
工具中的cc6通过反射方式将entry赋值给HashSet实例中的一个Node的key属性
TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo"); HashSet map = new HashSet(1); map.add("foo");//添加一个键 Field f = null; try { f = HashSet.class.getDeclaredField("map");//获取map属性 } catch (NoSuchFieldException e) { f = HashSet.class.getDeclaredField("backingMap"); } Reflections.setAccessible(f); HashMap innimpl = (HashMap) f.get(map);//获取map实例的map属性。【也就是"foo"->】键值对 Field f2 = null; try { f2 = HashMap.class.getDeclaredField("table"); } catch (NoSuchFieldException e) { f2 = HashMap.class.getDeclaredField("elementData"); } Reflections.setAccessible(f2); Object[] array = (Object[]) f2.get(innimpl);//获取map属性的table属性,里面包含很多Node Object node = array[0]; if(node == null){ node = array[1]; } Field keyField = null; try{ keyField = node.getClass().getDeclaredField("key"); }catch(Exception e){ keyField = Class.forName("java.util.MapEntry").getDeclaredField("key"); } Reflections.setAccessible(keyField); keyField.set(node, entry);//将其中一个Node的key属性改为entry return map;
java字节码
—所有能够恢复成一个类并在JVM虚拟机里加载的字节序列,都在我们的探讨范围内
URLClassLoader 实际上是我们平时默认使用的 AppClassLoader 的父类,所以,我们解释
URLClassLoader 的工作过程实际上就是在解释默认的Java类加载器的工作流程。
正常情况下,Java会根据配置项 sun.boot.class.path 和 java.class.path 中列举到的基础路径(这
些路径是经过处理后的 java.net.URL 类)来寻找.class文件来加载,而这个基础路径有分为三种情况:
•URL未以斜杠 / 结尾,则认为是一个JAR文件,使用 JarLoader 来寻找类,即为在Jar包中寻
找.class文件
•URL以斜杠 / 结尾,且协议名是 file ,则使用 FileLoader 来寻找类,即为在本地文件系统中寻
找.class文件
•URL以斜杠 / 结尾,且协议名不是 file ,则使用最基础的 Loader 来寻找类
直接偷懒使用p牛的字节码会报错
Hello : Unsupported major.minor version 52.0
原因是jdk版本不同,因为调试cc1等低版本链子以及7u21的原生链子,所以我的jdk版本搞得很低是oracle jdk7u21,渣性能虚拟机懒得在idea里来回切jdk
需要自己写个类编译好然后读取并编码
package org.test.ser; import com.sun.org.apache.xml.internal.security.exceptions.Base64DecodingException; import com.sun.org.apache.xml.internal.security.utils.Base64; import java.io.*; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class HelloDefineClass { public static void main(String[] args) throws NoSuchMethodException, Base64DecodingException, InvocationTargetException, IllegalAccessException, InstantiationException, IOException { File file = new File("C:/Users/Administrator/IdeaProjects/test1/target/classes/org/test/ser/Hello.class"); InputStream in = new FileInputStream(file); byte[] content = null; content = new byte[(int)file.length()]; in.read(content); System.out.println(Base64.encode(content)); Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass",String.class,byte[].class,int.class,int.class); defineClass.setAccessible(true); //byte[] code = Base64.decode(content); byte[] code = content; Class hello = (Class)defineClass.invoke(ClassLoader.getSystemClassLoader(),"org.test.ser.Hello",code,0,code.length); hello.newInstance(); } }
注意根据class文件的全限定路径名需要根据自己写的包名修改,不然会报错
我的目录结构如下
下面的TemplatesImpl使用的工具类的setFieldValue,一开始以为是某个lib库中的方法,后来发现是spring的工具类,我这里感觉没有必要起spring,就把源码自己写在一个类中然后去引用
package org.test.ser; import java.lang.reflect.*; /** * 反射的 Utils 函数集合 * 提供访问私有变量, 获取泛型类型 Class, 提取集合中元素属性等 Utils 函数 */ public class ClassUtils { /** * 直接读取对象的属性值, 忽略 private/protected 修饰符, 也不经过 getter * * @param object * @param fieldName * @return */ public static Object getFieldValue(Object object, String fieldName) { Field field = getDeclaredField(object, fieldName); if (field == null) { throw new IllegalArgumentException("Could not find field [" + fieldName + "] on target [" + object + "]"); } makeAccessible(field); Object result = null; try { result = field.get(object); } catch (IllegalAccessException e) { //log.error("getFieldValue:", e); } return result; } /** * 直接设置对象属性值, 忽略 private/protected 修饰符, 也不经过 setter * * @param object * @param fieldName * @param value */ public static void setFieldValue(Object object, String fieldName, Object value) { Field field = getDeclaredField(object, fieldName); if (field == null) { throw new IllegalArgumentException("Could not find field [" + fieldName + "] on target [" + object + "]"); } makeAccessible(field); try { field.set(object, value); } catch (IllegalAccessException e) { //log.error("setFieldValue:", e); } } /** * 通过反射, 获得定义 Class 时声明的父类的泛型参数的类型 * 如: public EmployeeDao extends BaseDao <Employee< span> </Employee<>, String> * * @param clazz * @param index * @return */ @SuppressWarnings("unchecked") public static Class getSuperClassGenricType(Class clazz, int index) { Type genType = clazz.getGenericSuperclass(); if (!(genType instanceof ParameterizedType)) { return Object.class; } Type[] params = ((ParameterizedType) genType).getActualTypeArguments(); if (index >= params.length || index < 0) { return Object.class; } if (!(params[index] instanceof Class)) { return Object.class; } return (Class) params[index]; } /** * 通过反射, 获得 Class 定义中声明的父类的泛型参数类型 * 如: public EmployeeDao extends BaseDao <Employee< span> </Employee<>, String> * * @param * @param clazz * @return */ @SuppressWarnings("unchecked") public static <T> Class<T> getSuperGenericType(Class clazz) { return getSuperClassGenricType(clazz, 0); } /** * 循环向上转型, 获取对象的 DeclaredMethod * * @param object * @param methodName * @param parameterTypes * @return */ public static Method getDeclaredMethod(Object object, String methodName, Class[] parameterTypes) { for (Class superClass = object.getClass(); superClass != Object.class; superClass = superClass.getSuperclass()) { try { return superClass.getDeclaredMethod(methodName, parameterTypes); } catch (NoSuchMethodException e) { //Method 不在当前类定义, 继续向上转型 } } return null; } /** * 使 filed 变为可访问 * * @param field */ public static void makeAccessible(Field field) { if (!Modifier.isPublic(field.getModifiers())) { field.setAccessible(true); } } /** * 循环向上转型, 获取对象的 DeclaredField * * @param object * @param filedName * @return */ public static Field getDeclaredField(Object object, String filedName) { for (Class superClass = object.getClass(); superClass != Object.class; superClass = superClass.getSuperclass()) { try { return superClass.getDeclaredField(filedName); } catch (NoSuchFieldException e) { //Field 不在当前类定义, 继续向上转型 } } return null; } /** * 直接调用对象方法, 而忽略修饰符(private, protected) * * @param object * @param methodName * @param parameterTypes * @param parameters * @return * @throws InvocationTargetException * @throws IllegalArgumentException */ public static Object invokeMethod(Object object, String methodName, Class[] parameterTypes, Object[] parameters) throws InvocationTargetException { Method method = getDeclaredMethod(object, methodName, parameterTypes); if (method == null) { throw new IllegalArgumentException("Could not find method [" + methodName + "] on target [" + object + "]"); } method.setAccessible(true); try { return method.invoke(object, parameters); } catch (IllegalAccessException e) { //log.error("invokeMethod:", e); } return null; } }
https://cloud.tencent.com/developer/article/1536342
原始代码在这里,Spring用Slf4j记录日志,我这里调试没什么用,又省的拉依赖回来,我就直接全注释掉了,然后在源码中import
import static org.test.ser.ClassUtils.setFieldValue;
调试时候建议在这里按照p牛说的链下断点
TemplatesImpl#getOutputProperties() -> TemplatesImpl#newTransformer() -> TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses() -> TransletClassLoader#defineClass()
在obj.newTransformer();下断点后请直接到这里,不然前面会有很折磨的idea debugger调试器的加载过程
p神关于bcel的文章中有一个链接,把fastjson和bcel都讲清楚了
https://kingx.me/Exploit-FastJson-Without-Reverse-Connect.html
还有对应公众号,非常值得一看
CC2
在commons-collections中找Gadget的过 程,实际上可以简化为,找⼀条从 Serializable#readObject() ⽅法到 Transformer#transform() ⽅法的调⽤链
CommonsCollections2实际就是⼀条从 PriorityQueue 到 TransformingComparator 的利用链
传进去的是队列,然后调用Transforming.comparator的compare方法调用传入的chaintransformer的transformer方法
链子比较简单
Shiro550
https://www.jianshu.com/p/f87b6301d668 idea默认使用unicode编码,tomcat用的是utf-8,需要改下idea的配置文件,调试时候日志才不会出现乱码
[L是一个JVM的标记,说明实际上这是一个数组
shiro在tomcat下不能反序列化有数组的payload的原因是调用了ClassUtils的forName,在调用时使用的是URLClassLoader,会去把[Lorg.apache.commons.collections.Transformer当成path去加载类,有了[L,肯定找不到
cc版本shiro和payload对应cc版本要一致,不然会导致static final long serialVersionUID = -4803604734341277543L;]:报错
断点打在shiro-core中
可以看到调用hashmap的readobject
接着调用对key做hash到调用key的hashcode,到调用tidemap的hashcode
TidemapEntry的key是TemplatesImpl对象,他的_byte[]是我们传入的恶意类的byte,他的_class是已经从_byte还原的class
他的value是我们传入的lazymap的get(TemplatesImpl对象)
因为我们的lazy被transform修饰回调,也就是InvokerTransformer
所以会调用InvokerTransformer的transform方法
从而调用TemplatesImpl的newTransformer方法
进而调用getTransletInstance方法,实例化_class中的内容,也就是把我们的类实例化
在实例化过程中会调用初始化中的代码
执行恶意命令
CB1
PropertyUtils.getProperty 这个方法会自动去调用一个JavaBean的getter方法
调用TemplatesImpl利用链
TemplatesImpl#getOutputProperties() -> TemplatesImpl#newTransformer() -> TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses() -> TransletClassLoader#defineClass()
shiro默认带cb依赖
cb 在 BeanComparator 类的构造函数处,当没有显式传入 Comparator 的情况下,则默认使用 ComparableComparator,对cc有依赖
使用java.lang.String的CaseInsensitiveComparator显式传入Comparator ,使利用链不依赖cc,从而出现不需要额外的cc依赖,只要有shiro就能打
queue添加的必须是字符串String,不然会报错