摘要

随着Web2.0的兴起,NoSQL数据库在网站架构中使用的越来越多。NoSQL固然在应对大规模、高并发的动态网站方面做的很好;但是作为程序,它的安全性始终是不能被忽略的方面。根据参与人员角色的不同,安全性可以基于数据库开发人员/数据库运维人员/第三方程序开发人员引发的问题来讨论。

数据库开发人员角度

数据库开发人员的专业技能决定了他们导致的安全问题会比一般人少,但不是零。软件规模大到一定程度以后,复杂性显著提升,而人毕竟不是机器,所以错误在所难免。我们知道,Windows、Linux、Unix、MacOS之类操作系统每个月都会发布一些重要的安全更新来修复漏洞,数据库也一样会有漏洞。2016年9月28日,CVE漏洞库添加了一个Redis存在的远程代码执行漏洞,下面通过简单介绍这个漏洞,来帮助我们更正确地认识到数据库本身可能存在的安全问题。

CVE-2016-8339

首先要说明,这个漏洞比较新,并且是由安全研究人员发现的,所以目前没有威力大的概念性证明或漏洞利用代码出现。攻击者可以根据具体环境来构造相应漏洞利用程序。

漏洞的根本原因非常简单:数组下标越界导致溢出(Redis是使用标准C语言开发的)。

在不低于2.0.0版本的Redis中,“CONFIG SET parameter value"命令可以动态地调整Redis服务器的配置,无需重启。

使用这条命令可以给当前未连接到服务端的某一类客户端设置"客户端输出缓冲区限制”:

CONFIG SET client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>

下面是Redis源代码中关于"client-output-buffer-limit"的解析代码:

可以看到,getClientTypeByName函数解析客户端类型并返回一个值存储在class变量中,它的取值范围是[-1, 3],接下来client_obuf_limits使用class变量作为下标去访问结构体数组并赋值。然而从client_obuf_limits的定义处可以发现,它的长度是3:

这意味着,如果使用类似于下面这样的命令去给一个master类型的客户端配置,则会导致数组下标越界:

CONFIG SET client-output-buffer-limit "master 3735928559 3405691582 373529054"

进一步探究,client_obuf_limits数组元素的结构体定义如下:

typedef struct clientBufferLimitsConfig {
	unsigned long long hard_limit_bytes;
	unsigned long long soft_limit_bytes;
	time_t soft_limit_seconds;
} clientBufferLimitsConfig;

结构体大小为24字节,这说明攻击者最多向client_obuf_limits数组后写入24字节的数据。参考Redis源码"server.h"部分可以发现,client_obuf_limits数组是redisServer结构体的一个成员,它的后面紧跟着AOF状态域(Redis 将所有对数据库进行过写入的命令记录到 AOF 文件, 以此达到记录数据库状态的目的):

攻击者向这些域中的大多数写入数据是没有意义的,但aof_filename这个字符指针值得注意,通过修改这个指针,在具体的环境下,攻击者可以达到利用AOF数据覆写任意文件的目的;或者加载通过其他途径构造的恶意AOF文件,来进行进一步的攻击。

漏洞修补:很简单,加入一些判断语句就可以了:

总结:

漏洞原理很简单,但是软件复杂性使得开发者和测试者很难意识到问题已经存在,这也是缓冲区溢出漏洞历久不衰的原因。没有一劳永逸的应对措施,我们能做的就是在开发时保持清晰的头脑,增强编译器的安全警告功能等等。

数据库运维人员角度

从这个角度来说,安全问题往往是数据库配置不当导致。对于具体的NoSQL实现,默认配置可能存在安全风险,但这不是运维工程师逃避责任的理由。下面以MongoDB为例,讲解一个安全问题。

MongoDB未授权访问漏洞

MongoDB启动时如果不加任何配置参数,则默认无权限验证,连接用户可以通过默认端口无需密码就对数据库做任意操作,包括添加超级用户。危害可想而知。

我们来进行一个小测试(图中敏感信息已打码,测试仅供学习研究,演示添加用户后删除我们添加的用户,不要对测试对象做任何有损害的操作,遵守我国网络安全法):

使用ZoomEye网络空间搜索引擎搜索开放27017(MongoDB默认端口)端口的主机,其中一些如下:

选择其中一个尝试连接,连接成功,没有报无权限警告。MongoDB默认有一个admin数据库,我们进行下面的添加超级用户的测试:

添加成功。很明显,如果这里我们对数据库进行权限设置,完全可以禁止其他人访问该数据库。但是我们仅仅进行一些查看操作(概念性证明即可):

最后,清理我们添加的用户(重申:不要给测试对象造成麻烦):

防御方法:在把MongoDB投入正式使用前,先在admin.system.users中创建用户;在启动服务时带--auth参数。

总结:通过使用网络空间搜索引擎,我们可以发现大量把默认端口暴露在外网的MongoDB服务。如果使用ZoomEye API配合Python脚本进行大规模测试,可以找到非常多的MongoDB未授权访问漏洞。这一现象说明部分运维工程师的疏忽。然而,门槛低是IT行业长久以来的特点,所以工程师往往良莠不齐。信息安全与医疗健康的道理是一样的,预防做得好,其作用远大于治疗。运维群体应该提高对自己的要求,提高安全意识,做好一线预防工作。

延伸:Redis也具有类似的安全问题。但Redis的开发者antirez说,Redis如果暴露在外部世界里,的确是不安全的,但是它本应运行在一个安全的,只有程序开发者自己才能接触到的环境中。所以运维工程师(还有程序开发者)在考虑使用这一类软件时,就应该考虑到它们的特点,做好相应的配置工作,而不是把责任推给数据库软件本身。

第三方程序开发人员角度

我们知道,经典的SQL注入是动态网站的后端开发语言在接受用户请求,构造SQL查询语句这个过程没有做到严格的恶意查询过滤(事实上,由于软件的复杂性和查询的未知性,完全杜绝SQL注入是非常困难的事情)导致的。也正因为SQL注入屡禁不止,所以它带来的损失远远超过数据库中其他安全问题。NoSQL一改传统的关系表存储方式,而是往往采用“键:值”结构存储。所以,传统的SQL注入在NoSQL数据库体系下基本不可能成功(严谨地说,目前没有发现这样的事例)。但是,这不意味着NoSQL不存在注入风险。相反,已经有NoSQL注入被安全研究者提出,它的根源与SQL注入相同,都是网站后端语言与数据库交互时没有严格过滤用户的非法输入。下面举出“Python-MongoDB”示例来说明NoSQL注入。

Python-MongoDB

此示例演示了普通用户通过NoSQL注入提权成为管理员用户的过程:

使用Flask框架搭建一个简单的网站,打开后是如下的登录界面:

注册一个用户:

注册成功后到达主页:

示例网站的正常功能是用户输入一个已注册用户的名字,系统查询并返回那个用户的年龄,比如我们分别输入“Bryant”和“Kobe”,提交后返回如下页面:

网站为管理员提供了一个隐藏页面,用于根据用户的key来查找特定用户,隐藏页面需要通过手动输入"127.0.0.1:5000/admin"打开。普通用户尝试打开这个页面则会收到下面的提示:

注意,主页上还提供了“settings”功能,供用户修改自己的信息:

我们知道,MongoDB使用“键:值”模式来存储数据。假如网站开发者把某个“键:值”对的“键”和“值”都开放给用户输入,那么用户就存在操控修改其他数据的可能(这与C语言中经典的格式化字符串漏洞非常像)。示例程序的源代码中有下面这个函数来完成“settings”功能:

def settings():
    if request.method == "POST":
        username = request.form['username']
        firstname = request.form['firstname']
        lastname = request.form['lastname']
        password = request.form['password']
        age = request.form['age']

        db.members.update({"_id":bson.ObjectId(session['_id'])}, {"$set":{"{}".format(firstname):lastname, "account_info.age":age, "username":username}})

可以看到,“firstname”和“lastname”作为同一“键:值”对的“键”和“值”,都是用户可控的。

另外,判断用户是否为管理员是通过下面的代码来执行:

def admin():
    if "_id" not in session:
        return redirect("/login/")

    theUser = db.members.find_one({"_id":bson.ObjectId(session['_id'])})

    if not theUser['account_info']['isAdmin']:

        return "You do not have access to this page."

如果我们在“settings”页面进行如下的输入:

再次打开"127.0.0.1:5000/admin"页面:

可以看到,我们成功打开了需要管理员权限才能够打开的特权页面,说明提权成功。

防御方法:开发者尽量不要使用这种允许用户同时能够控制“键”和“值”的代码逻辑。另外,就是要对用户输入做好过滤,对一些敏感“键”名称设置黑名单。

总结:

也许有人会说攻击者不一定知道网站开发者使用了account_info.isAdmin这个变量,这个观点有一定道理。但是,信息安全的防御不能基于“攻击者无知”的假设。如果网站使用的是开源代码,任何人都可以查看到呢?如果网站采用了Git来做版本控制,而网站开发者为了方便又把源码托管到了Github上的公开仓库了呢?再如果这个网站的安全做的非常好,但攻击者通过攻陷旁站来获得了本网站对的源码了呢?信息安全领域最应该重视的就是“短板效应”。所以,既然NoSQL注入可能存在,那么就值得重视。

总结

大数据时代已经到来。我们在享受数据服务带来的便利的同时,一定要更加重视信息安全。我们对计算机及网络的依赖程度越高,危机到来时受到的伤害就越大。从本文的探讨可以看出,安全不是一个人的事情,也不是一个群体的事情。攻击者无处不在,永远会挑选最薄弱的环节下手。信息安全没有绝对的事情,但是只要全民综合素质不断提高,安全意识不断增强,我们就能够增加攻击难度,抬高攻击成本,从而更加有效地遏制网络攻击。

参考文献