特性还是漏洞?滥用 SQLite 分词器

1 SQLite3 的全文检索

SQLite 是一款嵌入式关系型数据库,在 Android、Webkit 等流行软件中被广泛使用。

为支持全文检索,SQLite 提供了 FTS(Full Text Search) 扩展的能力。通过在数据库中创建虚拟表存储全文索引,用户可以使用 MATCH 'keyword' 查询而非 LIKE '%keyword%'子串匹配的方式执行搜索,充分利用索引,可获得极大的速度提升。如果对搜索引擎原理有初步了解,可以知道在实现全文检索中,对原始内容的分词是一个必须的过程。SQLite 内置的几个分词器,如 simple 和 porter,都只支持基于 ASCII 字符的英文分词。从 SQLite 3.7.13 开始引入了 unicode61 分词器,加入了对 unicode 的支持。但这几个内置的分词器仍不足以满足日常需求,例如中文搜索。因此 SQLite 提供了自定义分词扩展的功能,让开发者自行实现分词算法。

自定义分词器需要实现几个回调函数,其对应的生命周期如下:

  • xCreate 初始化分词器
  • xDestroy 销毁分词器
  • xOpen 初始化分词游标
  • xClose 销毁分词游标
  • xNext 获取下一个分词结果

为了注册这些回调,需要注册一个 sqlite3_tokenizer_module 结构体。其原型如下:

分词器的具体实现可以参考 simple_tokenizer(非官方 SQLite 仓库)的例子。完成了分词器的配置初始化之后,就可以通过创建虚拟表的方式为数据库建立全文索引,并使用MATCH 语句执行更高效的检索。搜索功能的具体细节与本文要讨论的内容没有太大关系,不做赘述。

2 危险的 fts3_tokenizer

SQLite3 中注册自定义分词器用到的函数是 fts3_tokenizer。该函数调用方式如下:

 

当只提供一个参数的时候,该函数返回指定名字的分词器的 sqlite3_tokenizer_module 结构体指针,以 blob 类型表示。例如在 sqlite3 控制台中输入:

 

将会返回一个以大端序 16 进制表示的内存地址,可以用来检查特定名称的分词器是否已注册。

函数的第二个可选参数用以注册新的分词器,只要执行如下 SQL 查询,即可注册一个名为 mytokenizer 的分词器:

 

等等,直接把指针放进了 SQL 查询?没错,就是这么设计的。

在前文中已经提到 sqlite3_tokenizer_module 中包含数个回调函数指针。攻击者仅仅需要构造一个合适的结构体,获取其内存地址,使用 SQL 注入等手段让目标注册构造好的“分词器”,再通过 SQL 触发特殊回调就可以实现劫持 IP 寄存器,执行任意代码。

接下来进一步分析这个攻击面是否可以被利用。

2.1 基地址泄漏

只提供一个参数执行 select fts3_tokenizer(name),如果 name 是一个已经注册过的分词器,将会返回这个分词器对应的内存地址。在 fts3.c 中可以看到 SQLite3 默认注册了内置分词器 simpleporter

 

以 simple 分词器为例,其注册的指针指向静态区的 simpleTokenizerModule

 

通过获得这个指针,即可通过简单的计算获得 libsqlite3.so 的基地址,从而绕过 ASLR:

这个基地址可以利用 SQL 注入,通过 union 查询或盲注的手段获取。

2.2 任意代码执行

经过以上的分析,实现拒绝服务的 PoC 非常简单。运行 64 位的 SQLite3 控制台,输入如下查询:

 

但其实现在还不能简单地控制 eip。指针指向的是结构体而不是代码,需要在内存中写入跳转的目的地址。既然已有基址泄露,那么能不能寻找一个通过纯 SQL 向 .bss 段写入的办法?PRAGMA 语句也许会是个不错的选择。使用此语句可以在数据库打开的过程中修改一些全局的状态,以及访问数据库元数据。

通过对源代码的阅读,找到了不二之选:PRAGMA soft_heap_limit。在 e36e9c520a7fa35c2dd46eb92aee7822580132e0 中引入的这个功能用来显式限制 SQLite 内存池的大小,支持传入一个 64 位整数。其最终将会调用 sqlite3_soft_heap_limit64,将全局变量 mem0alarmThreshold 成员设置为 SQL 传入的值。而 alarmThreshold 的地址可以通过前文泄漏的地址直接计算出来,结合两个条件,这真是一个完美的可以布置跳转指针的地方。

2.3 PoC

通过以上分析,这个攻击面可以通过如下方式触发:

  1. 通过 SQL 注入泄漏 libsqlite3 的地址,注意结果是大端序
  2. 通过 select sqlite_version() 函数泄漏版本,针对具体版本调整偏移量
  3. 执行 PRAGMA soft_heap_limit 语句布置需要 call 的目标指令地址
  4. 将 libsqlite3 的 .bss 段中的结构体地址转成大端序的 blob,注册分词器
  5. 创建虚拟表,触发 xCreate 回调,执行代码

3 利用场景分析

使用到 SQLite3 且能控制 SQL 语句的场景较多,以下分析几个常见的案例。

3.1 SQL 注入 PHP 应用远程执行代码

使用 union 或盲注可以泄露 libsqlite3 基址。在使用 mod_php 方式执行 php 的服务器上,得到的地址可以在多次请求中保持不变。接着计算可用的地址,触发代码执行。因为 PHP 的 SQLite3 扩展中 exec 方法支持使用分号分隔多个语句,因此可以完全使用注入的方式触发任意代码执行。

需要指出的是,虽然理论上可以发起远程任意代码执行,但实际利用的效果可能不如 load_extension 加载远程 dll(仅 Windows)或者利用 attach 特性导出 webshell 那样好用。

3.2 绕过 php 安全配置执行任意命令

实际环境中使用 php 和 SQLite,同时还具有 SQLi 的案例很少。就 php 而言,这个问题还有一种利用场景——利用任意代码执行来绕过 php.ini 的 open_basedirdisable_functions 配置,以进一步提权。

劫持 eip 的 POC 已经给出,可以获得一次 call 任意地址的机会。不过只能执行一次任意代码,也没有合适的栈迁移指令来实现 rop,实现系统 shell 还需要解决一些问题。在调用 xCreate 的上下文中存在多个可控的参数,但单纯靠 libsqlite3 找不到合适的 gadget 进行组合利用。本文的 exploit 中采用了迂回的做法,使用另一处 xOpen 回调和 php 中一处调用了 popen 的 gadget 来实现任意命令执行。

测试环境如下:

 

PHP 不是以独立进程执行,而是被作为模块加载到 Apache 的进程中。Apache 的保护如下:

 

然而 fts3_tokenizer “好心”地泄漏了一个共享库的基地址,导致 ASLR 可以被直接绕过。在实际利用中,攻击者没有办法直接获取目标 Apache 进程的 maps,从而得到其中任意两个 lib 之间的地址偏移。可行(但不完全靠谱)的方案是利用 phpinfo / apache_get_modules / get_loaded_extensions 三个函数提供的信息复制一个一样的环境,强行获取共享对象的基址的相对位置。

再看 xOpen 的函数原型:

 

第二个参数为需要分词的文字片段,正好是 char * 类型,可以用来放置命令参数。xOpen 回调可以通过 SELECT...MATCHINSERT 两种语句触发。为了让 xCreate 能正常返回,可以将其设置为 simple 分词器自带的 simpleCreate 函数指针。但问题随之出现,PRAGMA 语句只能修改一个指针,而现在需要至少 3 个连续的 QWORD 可控。可以通过堆喷射的方式实现,有一定命中概率;也可以重操旧业,再次寻找可以修改的 .bss 段。

通过向内存表插入大量数据来 HeapSpray 的方式已实测成功,以下分析覆写 PHP 配置的方法。

在 php 的每个模块中都可以见到 ZEND_BEGIN_MODULE_GLOBALS 宏包裹的结构体,用来管理作用域为模块的变量。这些结构体正好在各种 lib 的 .bss 段上,且有多个连续可控的数值。不幸的是,这些变量大多数来源于 php.ini 的配置,而直接修改 ini 配置的 ini_set 函数通常会被 disable_functions禁止。还好 php.ini 的配置还支持使用每个目录独立的 .htaccess 文件覆盖,只要 httpd.conf 开启了 AllowOverride,且选项的访问控制标志为 PHP_INI_PERDIRPHP_INI_ALL 即可(How to change configuration settings ¶)。既然能够上传和执行 WebShell,那么这一目录肯定是可写的。因此通过在脚本目录中写入 .htaccess 的方式来修改内存完全可行,需要发起两次 HTTP 请求。

在源代码中搜索宏 STD_PHP_INI_ENTRY,找到访问标记为 PHP_INI_SYSTEM 或者 PHP_INI_ALL,用 OnUpdateLong 获取数值的配置。在 32 位系统上也可以使用 OnUpdateLong 的选项,或者直接调用 assert_options 函数直接修改 assert 模块中连续的一块内容。满足要求的选项不少,在这里使用了 mysqlnd 的 net_cmd_buffer_sizelog_mask

演示视频:

Expolit 完整代码(适用环境见前文):

这也是 HITCON CTF 2015 资格赛中 Web 500 – Use After Flee 的另一种解题思路。

3.3 Android Content Provider

虽然 ContentProvider 的注入点满天飞,只要数据不是特别敏感,且 load_extension 得到封堵的情况下也并不能实现什么效果。如果这个问题能在 ContentProvider 上触发,那么是否可以通过这一途径实现跨越 App 执行代码,实现权限提升?

经过测试发现,无论是 SQLiteDatabase 的 executeSQL 还是 query 方法,都不支持使用分号分隔一次执行多个语句。而触发的关键语句如创建虚拟表等都不能通过子查询进行构造。因此从 ContentProvider 的注入点上只能实现注册,既不能触发回调也不能使用 PRAGMA。唯一能实现的跨进程地址泄漏,对于 Android 的 ASLR 机制来说毫无意义。由于每个 App 都由 Zygote fork 而来,只需要读取自身进程的 maps 就可以得到其他进程的内存布局。

不过 AOSP 还是出于安全考虑,在 Android 4.4 之后封禁了 fts3_tokenizer 函数(commit f764dbb50f2bfe95fa993fa670fae926cf36abce)。

3.4 Webkit 上的 WebSQL

Webkit 提供了 WebSQL 数据库,可以在浏览器内创建供客户端使用的关系数据库存储。虽然最终没有被 HTML5 标准采纳,但这个功能被保留了下来。通过阅读源码可以发现,WebSQL 背后的实现也是基于 SQLite3。那么 fts3_tokenizer 能不能通过 Javascript 触发呢?这恐怕是这一攻击面最激动人心的案例。

在 Webkit 的源代码(src/third_party/WebKit/Source/modules/webdatabase/DatabaseAuthorizer.cpp)中看到其通过 SQLite3 的 Authorizer 机制对 SQL 可使用的函数设置了访问控制规则,仅允许白名单的函数可以被查询。

CVE-2015-3659 中提到有方法可以绕过 authorizer 的限制执行任意 SQL 语句。笔者未能找到绕过的细节,而且 Webkit 对此的补丁没有改动访问控制策略,而是在白名单的基础上进一步对 rtreenode/rtreedepth/eval/printf/fts3_tokenizer 函数进行了重载,禁止黑名单函数的调用。猜想与 SQLite3 中 printf 函数实现存在缓冲区溢出漏洞有关。

4 缓解和修补

以上提到的 AOSP、WebKit 等开源项目对此设计了不同的缓解方案,具有参考价值。

  1. 如果用不到全文检索,可通过关闭 SQLITE_ENABLE_FTS3 / SQLITE_ENABLE_FTS4 / SQLITE_ENABLE_FTS5 选项禁用之,或者使用 Amalgamation 版本编译;
  2. 如果需要使用 MATCH 检索,但不需要支持多国语言(即内置分词器可以满足要求),找到 ext/fts3/fts3.c 中注释掉如下一行代码关闭此函数:

     
  3. 使用 SQLite3 的 Authorization Callbacks 设置访问控制

5 参考资料

  1. SQLite
  2. Chromuim
  3. AOSP
  4. About the Security Content of iOS8.4
  5. CVE-2015-7036, CVE-2015-3659
  6. Offset2lib

转载自:http://blog.chaitin.com/abusing_fts3_tokenizer/    原文作者:长亭科技