偶然看到了CVE-2022-31197,是由于ResultSet.refreshRow()
引发的SQL注入,感觉有点小有意思,正好之前学习了JDBC attack,决定分析一下漏洞造成的原因
在官方的描述中,被修复版本有42.2.26 42.4.1
这里我们选用42.2.23
版本的postgresql
数据库依赖
<dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>42.2.23</version> </dependency>
在连接中,他给出了一个payload
CREATE TABLE refresh_row_example ( id int PRIMARY KEY, "1 FROM refresh_row_example; SELECT pg_sleep(10); SELECT * " int );
这个cve的漏洞点主要是在PgResultSet#refreshRow
方法中,在该方法中打下断点,跟进代码
在这个方法中我观察到有一处执行sql语句的地方,或许那里就是漏洞触发点吧?
言归正传,如果我们需要到达漏洞触发点位置第一个拦路虎就是else if语句中的判断
拆分开来,第一个是需要使得this.isBeforeFirst()
为false,跟进代码逻辑
其中需要使得this.rowOffset + this.currentRow < 0
但是前者为0
只能从后者做文章了,那么this.currentRow
是什么捏?
怎么才能使得其不为-1
捏?
通过全局搜索对currentRow
属性的赋值,我发现在next
方法中,存在有其赋值操作
所以我们需要使得这个表有数据并在调用ResultSet.refreshRow
之前调用ResultSet.next
才能满足条件
回到else if语句,继续分析,isAfterLast
的调用
要想返回false,因为之前需要表中存在数据,所以if语句就不能返回false, 我们就只能使得currentRow
属性小于rows_size
才能满足条件
接下来就是Nullness.castNonNull(this.rows, "rows")
使得其返回不为空,跟进
很简单,前面就已经满足了,只需要满足有数据就OK
接下来就是sql语句的拼接逻辑,简单看看,了解payload的构造原理
在最开始就创建了select
开头的StringBuilder类,之后通过一个for循环获取表中的列名,并加以拼接
接下来又在后面添加了from
/表名/where等关键词
之后又是获取了primary key
修饰的列名,并且在后面添加了= ?
这类预编译手法,如果有多个primary key,将添加and逻辑词处理
接着就在最后调用了查询语句,执行了恶意SQL
从上面的分析中可以知道,在获取的列名前面加上了select column1,
所以我们首先需要闭合前面的,所以payload中的第二列名是1 from dbName;
开头,值得注意的是这里使用了分号进行sql语句之间的分割,那么列名中间部分就是我们需要执行的sql语句了,同样的根据上面的分析,我们知道,在列名的后面同样加上了from dbName where id = ?
,所以,在payload的最后我们需要闭合后面部分,使用select *
就能成功闭合
docker运行postgresql数据库
远程连接,首先创建一个表
CREATE TABLE refresh_row_example3 ( id int PRIMARY KEY, "1 FROM refresh_row_example3;CREATE TABLE test(id int);SELECT * " int );
这里我直接创建一个表展示能够成功利用,当然还有更多的利用姿势
随便添加一组数据
运行测试程序
可以发现成功创建test表
环境代码示例上传到了github
根据代码对比
https://github.com/pgjdbc/pgjdbc/commit/739e599d52ad80f8dcd6efedc6157859b1a9d637
在修复版本中,不在直接将列名写入sql语句中,而是经过了Utils.escapeIdentifier
的处理
https://github.com/pgjdbc/pgjdbc/security/advisories/GHSA-r38f-c4h4-hqq2