漏洞信息
漏洞简介
- 漏洞名称:Redis cluster.c clusterLoadConfig数组索引越界漏洞
- 漏洞编号:CVE-2017-15047
- 漏洞类型:缓冲区错误
- CVSS评分:【CVSS v2.0:】【CVSS v3.0:9.8】
- 漏洞危害等级:高危
组件概述
Redis(Remote Dictionary Server ),即远程字典服务,是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。
它通常被称为数据结构服务器,因为值(value)可以是 字符串(String), 哈希(Hash), 列表(list), 集合(sets) 和 有序集合(sorted sets)等类型。
漏洞概述
Redis 4.0.2版本中的cluster.c文件的‘clusterLoadConfig’函数存在安全漏洞。攻击者可利用该漏洞造成拒绝服务(越边界数组索引和应用程序崩溃)。
漏洞利用条件
redis-server以集群形式部署,攻击者可以访问到redis-server。
漏洞影响
Redis up to 4.0.2
漏洞修复
https://github.com/redis/redis/commit/ffcf7d5ab1e98d84c28af9bea7be76c6737820ad
漏洞复现
应用协议
6379/RESP
环境安装/搭建
在环境共享服务器中获取到环境源码\安装包,地址为:\\10.251.0.11\R-Redis\redis-3.2.7.tar.zip文件,解压编译即可。
启动前先更改配置文件redis.conf,开启集群模式。
1 | ################################ REDIS CLUSTER ############################### |
注释bind 127.0.0.1,开启远程访问。
关闭保护模式.
漏洞复现
在攻击机上发送cluster setslot命令,创建一个大于16384的槽号,为了达到拒绝服务目的。
1 | redis-cli -h 10.251.0.36 CLUSTER SETSLOT 16385 NODE node |
复现redis-server报错
配置redis.conf,按照redis.conf配置启动靶机的redis-server。
1 | redis-server redis.conf |
靶机上测试一下命令:
1 | CLUSTER SETSLOT 16385 NODE node |
发现已经修复了此漏洞,redis-server对槽号大小做了边界检查。
远程测试也是报此错误。
漏洞分析
技术背景
Redis是轻量级的,非易失性键值数据存储。 它通过Redis序列化协议(RESP)提供对简单易变数据结构的访问,该协议是基于TCP的协议。 与大多数其他数据库一样,Redis遵循客户端—服务器模型。 客户端能够通过Redis命令在Redis服务器上创建,修改和检索记录。
例如,以下命令创建“ TEST”字符串记录并将其分配给“ 1234”键值,将此记录修改为“ TEST2”并分别检索记录:
1 | SET 1234 TEST |
有关Redis命令的完整列表,请参考 http://redis.io/commands
Redis客户端通过端口6379通过TCP使用Redis序列化协议(RESP)与服务器进行通信。可通过 http://redis.io/topics/protocol获得该协议详细说明。 RESP使用五种数据类型,这些数据类型由相应数据的第一个字节标识:
简单字符串以“ +”字符开头
错误以“-”字符开头
整数以“:”字符开头
批量字符串以“ $”字符开头
数组以“ *”字符开头
批量字符串以“ $”字符开头,后跟相应字符串的长度。 以下重点介绍如何将“ Sangfor”表示为大容量字符串:
1 | $7 CRLF |
其中CRLF表示新的行序列回车(CR),后跟换行(LF)。
RESP数组以“ *”字符开头,后跟数组中的元素数。 下面说明了一个由2个元素组成的大容量字符串数组:
1 | *2 CRLF |
所有Redis命令都通过RESP字符串数组发送到服务器。 例如,上述SET命令将以下形式发送:
1 | *3 CRLF |
Lua是Redis 支持的轻量级脚本语言。 Redis内置了Lua解释器。Lua在Redis中的使用方法,可参考https://www.redisgreen.net/blog/intro-to-lua-for-redis-programmers/
一、Redis Cluster简单概述
1. Redis Cluster特点
- 多主多从,去中心化:从节点作为备用,复制主节点,不做读写操作,不提供服务
- 不支持处理多个key:因为数据分散在多个节点,在数据量大高并发的情况下会影响性能;
- 支持动态扩容节点:这是Rerdis Cluster最大的优点之一;
- 节点之间相互通信,相互选举,不再依赖sentinel:准确来说是主节点之间相互“监督”,保证及时故障转移
2.Redis Cluster与其它集群模式的区别
- 相比较sentinel模式,多个master节点保证主要业务(比如master节点主要负责写)稳定性,不需要搭建多个sentinel实例监控一个master节点;
- 相比较一主多从的模式,不需要手动切换,具有自我故障检测,故障转移的特点;
- 相比较其他两个模式而言,对数据进行分片(sharding),不同节点存储的数据是不一样的;
- 从某种程度上来说,Sentinel模式主要针对高可用(HA),而Cluster模式是不仅针对大数据量,高并发,同时也支持HA。
二、Redis Cluster如何集群实现?
1.Redis Cluster是如何将数据分片的?—-哈希槽Slot
(1)哈希槽介绍
Redis集群使用一种称作一致性哈希的复合分区形式(组合了哈希分区和列表分袂的特征来计算键的归属实例),键的CRC16哈希值被称为哈希槽。比如对于三个Redis节点,哈希槽的分配方式如下:
第一个节点拥有0-5500哈希槽
第二节点拥有5501-11000哈希槽
第三节点拥有剩余的11001-16384哈希槽
一个键的对应的哈希槽通过计算键的CRC16 哈希值,然后对16384进行取模得到:HASH_SLOT=CRC16(key) modulo 16383,Redis提供了CLUSTER KEYSLOT命令来执行哈希槽的计算:
1 | CLUSTER KEYSLOT name |
集群在线重配置(live reconfiguration)
Redis 集群支持在集群运行的过程中添加或者移除节点。
实际上, 节点的添加操作和节点的删除操作可以抽象成同一个操作, 那就是, 将哈希槽从一个节点移动到另一个节点:
- 添加一个新节点到集群, 等于将其他已存在节点的槽移动到一个空白的新节点里面。
- 从集群中移除一个节点, 等于将被移除节点的所有槽移动到集群的其他节点上面去。
因此, 实现 Redis 集群在线重配置的核心就是将槽从一个节点移动到另一个节点的能力。 因为一个哈希槽实际上就是一些键的集合, 所以 Redis 集群在重哈希(rehash)时真正要做的, 就是将一些键从一个节点移动到另一个节点。
要理解 Redis 集群如何将槽从一个节点移动到另一个节点, 我们需要对 CLUSTER
命令的各个子命令进行介绍, 这些命理负责管理集群节点的槽转换表(slots translation table)。
以下是 CLUSTER
命令可用的子命令:
CLUSTER ADDSLOTS slot1 [slot2] ... [slotN]
CLUSTER DELSLOTS slot1 [slot2] ... [slotN]
CLUSTER SETSLOT slot NODE node
CLUSTER SETSLOT slot MIGRATING node
CLUSTER SETSLOT slot IMPORTING node
最开头的两条命令 ADDSLOTS
和 DELSLOTS
分别用于向节点指派(assign)或者移除节点, 当槽被指派或者移除之后, 节点会将这一信息通过 Gossip 协议传播到整个集群。 ADDSLOTS
命令通常在新创建集群时, 作为一种快速地将各个槽指派给各个节点的手段来使用。
CLUSTER SETSLOT slot NODE node
子命令可以将指定的槽 slot
指派给节点 node
。
至于 CLUSTER SETSLOT slot MIGRATING node
命令和 CLUSTER SETSLOT slot IMPORTING node
命令, 前者用于将给定节点 node
中的槽 slot
迁移出节点, 而后者用于将给定槽 slot
导入到节点 node
:
当一个槽被设置为
MIGRATING
状态时, 原来持有这个槽的节点仍然会继续接受关于这个槽的命令请求, 但只有命令所处理的键仍然存在于节点时, 节点才会处理这个命令请求。如果命令所使用的键不存在与该节点, 那么节点将向客户端返回一个
-ASK
转向(redirection)错误, 告知客户端, 要将命令请求发送到槽的迁移目标节点。当一个槽被设置为
IMPORTING
状态时, 节点仅在接收到ASKING
命令之后, 才会接受关于这个槽的命令请求。如果客户端没有向节点发送
ASKING
命令, 那么节点会使用-MOVED
转向错误将命令请求转向至真正负责处理这个槽的节点。上面关于
MIGRATING
和IMPORTING
的说明有些难懂, 让我们用一个实际的实例来说明一下。假设现在, 我们有 A 和 B 两个节点, 并且我们想将槽
8
从节点 A 移动到节点 B , 于是我们:向节点 B 发送命令
CLUSTER SETSLOT 8 IMPORTING A
向节点 A 发送命令
CLUSTER SETSLOT 8 MIGRATING B
每当客户端向其他节点发送关于哈希槽 8
的命令请求时, 这些节点都会向客户端返回指向节点 A 的转向信息:
如果命令要处理的键已经存在于槽
8
里面, 那么这个命令将由节点 A 处理。如果命令要处理的键未存在于槽
8
里面(比如说,要向槽添加一个新的键), 那么这个命令由节点 B 处理。 这种机制将使得节点 A 不再创建关于槽
8
的任何新键。
与此同时, 一个特殊的客户端 redis-trib
以及 Redis 集群配置程序(configuration utility)会将节点 A 中槽 8
里面的键移动到节点 B 。
键的移动操作由以下两个命令执行:
1 | CLUSTER GETKEYSINSLOT slot count |
上面的命令会让节点返回 count
个 slot
槽中的键, 对于命令所返回的每个键, redis-trib
都会向节点 A 发送一条 [MIGRATE host port key destination-db timeout COPY] [REPLACE] 命令, 该命令会将所指定的键原子地(atomic)从节点 A 移动到节点 B (在移动键期间,两个节点都会处于阻塞状态,以免出现竞争条件)。
以下为 [MIGRATE host port key destination-db timeout COPY] [REPLACE] 命令的运作原理:
1 | MIGRATE target_host target_port key target_database id timeout |
执行 [MIGRATE host port key destination-db timeout COPY] [REPLACE] 命令的节点会连接到 target
节点, 并将序列化后的 key
数据发送给 target
, 一旦 target
返回 OK
, 节点就将自己的 key
从数据库中删除。
从一个外部客户端的视角来看, 在某个时间点上, 键 key
要么存在于节点 A , 要么存在于节点 B , 但不会同时存在于节点 A 和节点 B 。
因为 Redis 集群只使用 0
号数据库, 所以当 [MIGRATE host port key destination-db timeout COPY] [REPLACE] 命令被用于执行集群操作时, target_database
的值总是 0
。
target_database
参数的存在是为了让 [MIGRATE host port key destination-db timeout COPY] [REPLACE] 命令成为一个通用命令, 从而可以作用于集群以外的其他功能。
我们对 [MIGRATE host port key destination-db timeout COPY] [REPLACE] 命令做了优化, 使得它即使在传输包含多个元素的列表键这样的复杂数据时, 也可以保持高效。
不过, 尽管 [MIGRATE host port key destination-db timeout COPY] [REPLACE] 非常高效, 对一个键非常多、并且键的数据量非常大的集群来说, 集群重配置还是会占用大量的时间, 可能会导致集群没办法适应那些对于响应时间有严格要求的应用程序。
附所有redis-cluster相关的集群命令:
- cluster info :打印集群的信息
- cluster nodes :列出集群当前已知的所有节点( node),以及这些节点的相关信息。
- cluster meet
:将 ip 和 port 所指定的节点添加到集群当中。 - cluster forget
:从集群中移除 node_id 指定的节点。 - cluster replicate
:将当前从节点设置为 node_id 指定的master节点的slave节点。只能针对slave节点操作。 - cluster saveconfig :将节点的配置文件保存到硬盘里面。
- cluster addslots
[slot …] :将一个或多个槽( slot)指派( assign)给当前节点。 - cluster delslots
[slot …] :移除一个或多个槽对当前节点的指派。 - cluster flushslots :移除指派给当前节点的所有槽,让当前节点变成一个没有指派任何槽的节点。
- cluster setslot
node :将槽 slot 指派给 node_id 指定的节点,如果槽已经指派给 - cluster setslot
migrating :将本节点的槽 slot 迁移到 node_id 指定的节点中。 - cluster setslot
importing :从 node_id 指定的节点中导入槽 slot 到本节点。 - cluster setslot
stable :取消对槽 slot 的导入( import)或者迁移( migrate)。 - cluster keyslot
:计算键 key 应该被放置在哪个槽上。 - cluster countkeysinslot
:返回槽 slot 目前包含的键值对数量。 - cluster getkeysinslot
:返回 count 个 slot 槽中的键 。
详细分析
代码分析
漏洞产生原因是在集群模式部署下,使用cluster相关命令时,对slot值没有做边界校验,导致直接索引migrating_slots_to和migrating_slots_from数组并赋值,而migrating_slots_to和migrating_slots_from数组大小由CLUSTER_SLOTS(16384)确定。一旦slot值大于16384,则会导致数组边界溢出。
src/cluster.h:
在/redis/src/cluster.c的clusterLoadConfig函数,允许一个缓冲区溢出漏洞从用户可控输入数组索引之中。易受攻击的代码是:
slot变量接收atoi的输出,该输出在此处运行:slot = atoi(argv[j]+1);
现在,argv [j]基本上是使用sdssplitargs放入数组进行进一步处理的每行(存储在line []中)的参数。具有有限访问权限的攻击者将可以通过,超大的slot值而强制发生数组错误异常而触发内存损坏问题,甚至可能执行代码。
但是,此漏洞能不能远程触发还有待分析。
全局搜索clusterLoadConfig函数,只有在clusterInit函数中被调用一次,也就是在redis-server启动集群模式之初就开始执行,攻击者很难远程执行。
clusterLoadConfig函数加载cluster配置信息失败时的报错信息,这一点在靶机redis-server启动时可以验证:
故猜测无法远程利用此函数达到远程拒绝服务攻击。
clusterLoadConfig函数,在cluster创建时加载集群的配置文件,在redis.conf中可以找到相关信息:
node-6379.conf虽然此配置的名字叫”集群配置文件”,但是此配置文件不能人工编辑,它是集群节点自动维护的文件,主要用于记录集群中有哪些节点、他们的状态以及一些持久化参数等,方便在重启时恢复这些状态。通常是在收到请求之后这个文件就会被更新。
复现时已经触发一次,node-6379.conf里面记录上一次连接时的一些信息。
再次尝试利用远程命令设置slot值,看能不能远程触发边界索引溢出漏洞,当使用远程cluster命令时,redis-server调用clusterCommand函数。
执行cluster setslot命令时,执行下面内容:
会被getSlotOrReply函数检查提交的slot值与CLUSTER_SLOTS进行比较,若大于16384,则报错。
补丁分析
漏洞产生原因是在集群模式部署下,使用cluster相关命令时,对slot值没有做边界校验,导致直接索引migrating_slots_to和migrating_slots_from数组并赋值,而migrating_slots_to和migrating_slots_from数组大小由CLUSTER_SLOTS(16384)确定。一旦slot值大于16384,则会导致数组边界溢出。
补丁是在/redis/src/cluster.c的clusterLoadConfig函数给migrating_slots_to和migrating_slots_from进行索引后赋值的地方进行了slot值与CLUSTER_SLOTS大小的判断,避免数组越界。
流量分析
尝试使用cluster setslot命令,触发漏洞,redis-server返回ERR信息。