在SQLite数据库中通过SQL语句执行命令的方法

发表时间: 2019-08-12 17:37


SQLite是世界上使用最多的数据库之一。然而,关于其安全方面的研究,都只涉及WebSQL和浏览器开发方面。我们相信这只是SQLite安全的冰山一角。

在对SQLite安全性的长期研究中,我们尝试在任何环境下都只依赖SQL语句,对SQLite进行内存损坏攻击。通过自创的查询劫持(Query Hijacking)和面向查询的编程(Query Oriented Programming),我们确实可在SQLite引擎中发起内存损坏攻击。我们将在几个实际场景中进行演示服务器上的密码窃取器,以及利用更高的权限持久控制iOS。

我们希望通过发布我们的研究进行抛砖引玉,引起更多人对SQLite的研究。特别是考虑到SQLite早已经内置到全球大部分操作系统、桌面或移动设备中,研究前景不可估量。此外,以下所涉及的查询语句并不仅限于SQLite,也适用于其他SQL引擎。欢迎大家一起使用熟悉的结构化查询语言进行开发、利用、攻击。

动机

这项研究始于我和omriher发现的一些臭名昭著的密码窃取器的源代码。虽然全球存在很多密码窃取器(比如Azorult、Loki Bot和Pony),但他们的行为方式基本相同:

感染受害者计算机后,恶意软件要么在用户输入凭证时捕获数据,要么主动收集其他软件存储在计算机上的所有凭证。

而将凭证存储在SQLite中是非常常见的。

恶意软件在收集SQLite存储文件后,将它们发送到攻击者服务器中,使用PHP解析它们,最终获得帐户名和密码。

通过研究这些密码窃取器,我们开始更进一步的推测。

我们能否利用数据库查询来攻击数据库?

如果能行,考虑到SQLite的部署范围,这无疑会造成巨大的影响。就让我们开始吧!

SQLite介绍

你很可能正在使用SQLite但却毫无察觉。

引用作者的话:

SQLite是一个用C语言库实现的小型、快速、高可靠性、功能齐全的SQL数据库引擎。SQLite同时也是世界上使用最多的数据库引擎。SQLite内置在所有手机和大多数计算机中,并和人们每天使用的无数应用进行配合。

与大多数其他SQL数据库不同,SQLite没有单独的服务进程。SQLite直接读写磁盘文件,包含多个表、索引、触发器和视图的完整SQL数据库通常位于单个磁盘文件中。

攻击点

下面的代码片段是密码窃取器的一个通用示例。

基于我们已控制了数据库这一背景,因此可将攻击点分为两方面:数据库的加载和初步解析,以及执行SELECT查询。

sqlite3_open完成的初始加载实际上涉及的点非常有限;它基本上就是指用于打开数据库的大量设置和配置代码。我们的主要关注点是header解析,基本是用AFL(American Fuzzy Lop)进行模糊测试。

另一方面,当我们开始查询数据库时,事情变得更加有趣。

用SQLite作者的话:

SELECT语句是SQL语言中最复杂的命令。

尽管我们无法控制查询流程本身(因为它是硬编码在目标上),但是仔细研究SELECT流程对我们的渗透测试是有益的。

由于SQLite3是一个虚拟机,所以每个SQL语句必须首先使用sqlite3_prepare*例程将其编译成字节码程序。此外,prepare函数会遍历并扩展所有SELECT子查询。这个过程一部分是验证所有相关对象(如表或视图)是否确实存在,并将它们在主模式(master schema)中进行定位。

sqlite_master和DDL

每个SQLite数据库都有一个sqlite_master表,它定义了数据库及其所有对象(如表、视图、索引等)的模式。

sqlite_master表的定义语句为:

我们特别感兴趣的部分是sql这一列。

这个字段是用来描述对象的DDL(数据定义语言)。

在某种意义上,DDL命令类似于C语句中的头文件。DDL命令用于定义数据库中数据容器的结构、名称和类型,就像头文件通常定义类型、结构、类和其他数据结构一样。

如果我们查看数据库文件,会发现这些DDL语句实际上以明文存储:

在查询的准备过程中,sqlite3LocateTable()会试图找到我们感兴趣的表的内存结构。

sqlite3LocateTable()会读取sqlite_master表中可用的模式,如果是第一次这样做,它还会对每个结果进行回调,以验证DDL语句是否有效,并构建必要的内部数据结构来描述所讨论的对象。

DDL Patching

了解以上这个准备过程后,有一个问题出现了,我们是否简单地替换文件中以纯文本形式出现的DDL?如果可以,我们就能将自己定义的SQL语句注入到文件中,造成一些破坏。

根据上面的代码片段,我们发现DDL语句似乎必须以“create”开头。

考虑到这个限制,以及查阅SQLite文档,我们发现这些可能是我们能创建的对象:


CREATE VIEW命令给了我们启发。简单地说,视图(VIEW)就是预打包的SELECT语句。如果我们用兼容的视图替换目标软件的目标表,就会有神奇的事发生。

劫持任何查询

设想以下场景:

原数据库有一个名为dummy的表,定义为:

目标软件使用的查询语句如下:

如果我们制作一个dummy视图,则可以劫持这个查询:

这个“陷阱”视图使我们能够劫持查询——这意味着我们生成一个完全由我们控制的查询功能。


以上这个劫持极大扩展了我们的攻击面,我们可以尝试篡改DDL,与SQLite解释器进行交互。

既然现在我们可以与SQLite解释器进行交互,那接下来的问题就是SQLite内置哪些开发语言?能否执行系统命令,并对文件系统进行读取或写入?

由于我们并不是第一个从开发的角度研究SQLite的人,因此让我们先回顾一下前辈的成果。

SQL注入

SQLite到底支持系统命令吗?我们可以加载任意库吗?

目前看起来,最简单的操控文件的方法如下:

我们选定一个新数据库,创建一个表并插入一行文本。然后,这个新数据库就成了一个包含webshell的新文件(SQLite中数据库是一个文件)。

PHP解释器的容错性非常高,能顺利捕获到PHP内容的开始标记<?。

但是,正如前面所述,DDL不能以“ATTACH”开头。

另一个值得注意的是load_extension函数。虽然这个函数应该允许我们加载任意的共享对象,但默认情况下它是被禁用的。

SQLite中的内存损坏

与用C语言编写的任何其他软件一样,在评估SQLite的安全性时,内存安全绝对是评估的重点。

在Michał Zalewski的博客中,详细描述他是如何利用AFL对被SQLite进行一些fuzz,并得到了令人印象深刻的结果:短短30分钟内得到了22个漏洞。

有趣的是,SQLite已经将AFL作为其安全测试套件的一部分。

以上内存损坏漏洞都得到了修复。而从攻击者的角度来看,如果没有一个合适的框架,这些漏洞很难被利用。

现代一些通用的防御方法在实施内存损坏攻击方面造成了极大障碍,攻击者需要找到另一个更灵活的方法。

Web SQL

Web SQL Database是一个用于往数据库中存储数据的Web页面API,它可以通过javascript使用变种SQL语句查询数据库中的数据。W3C Web应用工作组在2010年11月停止了对该技术规范的探讨,理由是除了SQLite之外独立应用较少。

目前,谷歌Chrome、Opera和Safari仍然支持该API。

它们都使用SQLite数据库作为这个API的后端支持。

在一些最流行的浏览器中,任何网站都可以操控SQLite中的数据,这引起了安全社区的注意,相关漏洞的数量也开始上升。

突然之间,SQLite中的漏洞可被用来攻击javascript解释器,间接影响了浏览器。

以下是几个影响较大的研究报告:

  • CVE-2015-7036,fts3_tokenizer()的不可信的指针引用取消
  • Chaitin team在Blackhat 17中展示的fts3OptimizeFunc()中的类型混淆
  • Exodus发现的fts3SegReaderNext()的整数溢出

从过去有关WebSQL研究中,我们发现一个名为“FTS”的虚拟表模块可能是我们研究的重点目标。

FTS

全文搜索(Full-Text Search)是一个虚拟表模块,它可对一组文档进行文本搜索。

从SQL语句的角度来看,虚拟表对象与任何其他表或视图看起来一样。但在原理上,虚拟表上的查询调用影子表上的回调方法,而不是普通地对数据库文件进行读写。

一些虚拟表的应用,例如FTS,涉及使用真实的(非虚拟的)数据库表来存储内容。

例如,当将字符串插入FTS3虚拟表时,必须生成一些元数据,以便进行有效的文本搜索。这些元数据最终会存储在名为%_segdir和%_segments的表中,而字符串本身会存储在% _content中,其中%代表原始虚拟表的名称。

这些包含虚拟表数据的辅助表被称为影子表。

由于其本身特性,影子表之间传递数据的接口滋生了很多漏洞。例如CVE-2019-8457,一个在RTREE虚拟表模块中的一个越界读漏洞。

RTREE虚拟表,用于地理数据索引,应当以整数列开始。因此,其他RTREE接口默认RTREE中的第一列是整数。但是,如果我们创建一个表,其中第一列是一个字符串(如下图所示),并将其传递给rtreenode()接口,就会发生越界读取。

现在我们既可以劫持查询,又知道在哪里可以找到漏洞,那么就可以正式开始漏洞利用开发了。

SQLite内部结构

关于SQLite开发,无论是说明文档,还是实际利用,都可以看到它依赖于各种外部环境,例如关于滥用SQLite标记器中涉及的PHP解释器,以及和Web SQL有关的javascript解释器。

但从攻击利用的角度来看,最好有一种通用的方法,别受限于外部环境。因此我们开始探索使用SQLite内部构件进行漏洞利用开发。

我们希望能用存粹的SQL语句进行以下方面攻击:

  • 内存泄漏
  • 打包和解包64位指针
  • 指针算法
  • 在内存中创建复杂的假对象
  • 堆喷射

我们将一个接一个地实现以上利用,仅使用SQL语句实现。

为了在PHP7上实现RCE,我们将使用仍未被修复的CVE-2015-7036。

等等?为什么一个长达4年的漏洞还没被修复?因为只有同意执行来自不可信源(Web SQL)的任意SQL语句才能触发漏洞,所以一直未被修复。

但是,SQLite的使用是如此的变幻莫测,我们仍可在许多场景触发漏洞。

开发计划

CVE-2015-7036是一个非常好用的漏洞。

简单地说,就是fts3_tokenizer()函数在插入单个参数(如“simple”、“porter”或任何其他被注册的分词器)时会返回分词器地址。

当函数调用两个参数时,fts3_tokenizer将用第二个参数提供的地址覆盖第一个参数的分词器地址。

在某个分词器被覆盖后,fts表中任何使用该分词器的新实例都能被我们劫持程序流。

我们的目标:

  • 分词器地址
  • 计算基址
  • 伪造一个分词器,执行我们的恶意代码
  • 我们的分词器覆盖另一个分词器
  • 实例化fts3表以执行恶意代码

现在回到我们的开发上来。

面向查询的编程(QOP)

我们很自豪向外展示了我们研究出的基于结构化查询语言所进行开发的独特方法。我们希望其他研究人员能在此方面进行深入探究。

我们的最终会将以下漏洞利用语句植入sqlite_master表中,劫持目标软件所发出的查询,达到加载并查询我们构造的恶意SQLite db文件这一目的。

内存泄漏 – 二进制

诸如ASLR之类的防御手段无疑提高了内存破坏的难度,而战胜它的一个常见方法就是去了解我们周围的内存布局。

这就是众所周知的内存泄漏。

在我们的示例中,内存泄漏指的是SQLite返回一个BLOB对象。

之所以瞄准BLOB,是因为它们有时包含内存指针。

如上所示,fts3_tokenizer()函数会返回所请求的分词器的内存地址。所以我们最终得到了一些被反转内存地址。

当然,我们可以使用一些SQLite内置的函数来再次翻转。

QOP利用链

为了存储数据,我们还需要一条INSERT语句。但由于sqlite_master表会对语句进行严格过滤,所以我们不能使用“INSERT”,所有语句都必须以“CREATE”开头。而我们应对这一挑战的方法就是将查询集中在一个视图中,将它们串联在一起。

下面的例子清楚说明了这一点:

随着我们的链条变得越来越复杂,使用伪变量肯定更加方便。

解压缩64位指针

如果你曾经做过二进制方面的挖洞挑战,那么对指针的压缩和解压应该并不陌生。

以下语句可以很容易地将十六进制值(就像我们刚刚实现的内存泄漏)转换为整数。这样就可以在接下来的步骤中用这些值进行各种计算。

以上sql语句一个字符一个字符地迭代计算十六进制字符串。只需稍微调整一基于1的instr()函数即可完成。

指针算法

指针算法因值已转化为整数而变得非常简单。例如,从泄漏的分词器指针中提取图像库:

64位指针压缩

在获得泄漏的指针并转换计算后,需要再把它们复原,这样我们就可以把它们写到某个地方。

此处应使用函数char(),不过它只在能有限的整数范围内工作。

在多次仔细阅读SQLite文档后,我们突然有了一个奇怪的感觉:我们的exp实际上是一个数据库。

预先准备一个将整数映射到其期望值的表。

而指针压缩语句如下:

在内存中制造复杂的虚假对象

写一个指针肯定有用,但仍然不够。许多内存漏洞的利用场景要求攻击者在内存中伪造一些对象或结构,甚至编写一个ROP链。

现在,我们将把前面介绍的几块串起来。

例如,我们自己构造的分词器,解释如下。

我们的虚假分词器应该符合SQLite所期望的接口:

使用上面描述的方法和一个简单的连接查询,我们可以很容易地伪造所需对象。

通过调试器验证,我们确实创建了一个虚假的分词器对象。

堆喷射

上步所制作的虚假对象可参与堆喷射。理想情况下,这应该是后者的某种重复形式。

但不幸的是,sqlite没有像mysql那样的REPEAT()函数。

然而,我们还有一个优雅的解决方案。

zeroblob(N)函数会返回一个由n个字节组成的BLOB对象,然后我们使用replace()将这些0替换为虚假对象。

搜索这些0x41,结果也显示了完美的一致性,每0x20字节重复一次。

内存泄漏——堆

我们已经知道二进制图像的位置,我们能够推断出必要的函数在哪里,并用我们的恶意分词器喷射堆。

现在,是时候用我们的分词器去覆盖另一个分词器了。然而,堆地址也是随机的,我们不知道我们的堆喷射物在哪里。

我们需要一个堆泄露漏洞。

于是,我们瞄准虚拟表接口。

由于虚拟表使用底层的影子表,所以它们在不同的SQL接口之间传递原始指针是很常见的。

注意:这类问题在sqlite 3.20中得到了解决。但PHP7是用早期版本进行编译的。如果版本不对,这里也可以使用CVE-2019-8457。

为了泄漏堆地址,我们需要预先生成一个fts3表,并利用其MATCH接口。


正如我们在第一次内存泄漏中看到的,得到的指针需要反转。幸运的是,我们依旧可以使用SUBSTR()。

现在,我们知道堆位置,可以正确地喷射,我们终于可以用自己的分词器覆盖另一个分词器!

放在一起

有了以上所有利用条件,现在是时候回到我们开始的地方:一个密码窃取器。

如上所述,我们需要建立一个“陷阱”视图。因此,我们需要研究我们的目标,准备好正确的视图。


如上面的代码片段所示,我们的目标期望数据库中有一个名为Notes的表,其中包含一个名为BodyRich的列。为了劫持这个查询,我们创建了以下视图:


在Notes被查询后,会执行3个QOP链。先分析第一个。

堆喷射

我们的第一个QOP链应该用大量恶意分词器来填充堆。

p64_simple_create,p64_simple_destroy和p64_system基本上是组成三条QOP链的基础。

例如,p64_simple_create的构造如下:


由于这些链变得非常复杂、且重复度很高,所以我们创建了脚本。



Demo

视频地址:https://www.youtube.com/embed/cPfYoxLOi1M?feature=oembed

现在,让我们研究SQLite的另一个有趣用例。

iOS

在iOS上通常很难实现持久控制,因为所有可执行文件必须被签名。幸运的是,SQLite数据库不包括在内。

我们将常用的数据库替换成另一个版本。在设备重新引导并查询恶意数据库之后,我们将实现代码执行。

为了演示这个概念,我们替换了Contacts DB “AddressBook.sqlitedb”。正如在我们在PHP7中所做的,我们创建了两个额外的DDL语句。一个DDL语句覆盖默认的分词器“simple”,另一个DDL语句试图实例化被覆盖的分词器,从而触发崩溃。现在,我们所要做的就是将原始数据库的每个表重写为一个视图,该视图将劫持所执行的任何查询,并将其重定向到恶意的DDL中。

将contacts db替换为我们的恶意contacts db并重新引导,会导致以下情况(注意崩溃信息):

与预期一样,contacts进程在0x4141414141414149处崩溃,它希望在这里找到我们的虚假分词器的xCreate构造部分。

此外,contacts db实际被许多进程共享。联系人、Facetime、Springboard、WhatsApp、Telegram和XPCProxy仅仅只是一部分。其中一些进程比其他进程的权限更高,因此这种攻击技术还能用于提升权限。

我们已向苹果公司报告了漏洞,CVE编号如下:

  • CVE-2019-8600
  • CVE-2019-8598
  • CVE-2019-8602
  • CVE-2019-8577
本文由白帽汇整理并翻译,不代表白帽汇任何观点和立场来源:https://research.checkpoint.com/select-code_execution-from-using-sqlite/