CVE-2015-8080_Redis getnum整数溢出漏洞

Catalogue
  1. 1. 漏洞信息
    1. 1.1. 漏洞简介
    2. 1.2. 组件概述
    3. 1.3. 漏洞概述
    4. 1.4. 漏洞利用条件
    5. 1.5. 漏洞影响
    6. 1.6. 漏洞修复
  2. 2. 漏洞复现
    1. 2.1. 应用协议
    2. 2.2. 环境安装/搭建
      1. 2.2.1. Linux下安装Redis 6.0.0
      2. 2.2.2. 安装Lua
    3. 2.3. 漏洞复现
  3. 3. 漏洞分析
    1. 3.1. 技术背景
    2. 3.2. 详细分析
      1. 3.2.1. 漏洞利用过程
      2. 3.2.2. 代码分析
      3. 3.2.3. 漏洞触发过程
      4. 3.2.4. 补丁分析
    3. 3.3. 流量分析
      1. 3.3.1. 攻击包分析
      2. 3.3.2. 正常包分析
  4. 4. 漏洞检测和防御
    1. 4.1. 漏洞检测
    2. 4.2. 漏洞防御
      1. 4.2.1. 漏洞修复
  5. 5. 参考资料

漏洞信息

漏洞简介

  • 漏洞名称:Redis缓冲区溢出漏洞
  • 漏洞编号: CVE-2015-8080/CVE-2020-14147
  • 漏洞类型:缓冲区溢出
  • CVSS评分:【CVSS v3.0:7.7】
  • 漏洞危害等级:高危

组件概述

​ Redis(Remote Dictionary Server ),即远程字典服务,是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。它通常被称为数据结构服务器,因为值(value)可以是字符串(String), 哈希(Hash), 列表(list), 集合(sets) 和 有序集合(sorted sets)等类型。

漏洞概述

​ Redis 6.0.3之前的lua_struct.c中的getnum函数中存在整数溢出漏洞,这允许具有上下文相关权限的攻击者在Redis会话中运行Lua代码,从而导致拒绝服务(内存损坏和应用程序崩溃)。

漏洞利用条件

​ Redis部署在服务器端,开启6379端口;攻击者,通过构造RESP协议包,利用struct.pack方法用于转换指定为第一个参数的非常大的整数(2^31),从而能触发整数溢出,再导致缓冲区溢出。

漏洞影响

​ Redis 小于 6.0.3

漏洞修复

https://github.com/antirez/redis/commit/ef764dde1cca2f25d00686673d1bc89448819571

漏洞复现

应用协议

6379/RESP(Redis的序列化协议)

环境安装/搭建

Linux下安装Redis 6.0.0

​ 首先,Redis对全版本进行了补丁修复,为了复现漏洞,将Redis 6.0.0源码改为未打CVE-2020-14147补丁之前的状态。

​ 进入路径,找到lua_struct.c文件

1
\redis-6.0.0\deps\lua\src、lua_struct.c

​ 用编辑器,注释以下98-99行代码:

1
2
if (a > (INT_MAX / 10) || a * 10 > (INT_MAX - (**fmt - '0')))
luaL_error(L, "integral size overflow");

​ 使用以下命令下载,提取和编译Redis 6.0.0:

1
2
3
4
$ wget https://download.redis.io/releases/redis-6.0.0.tar.gz
$ tar xzf redis-6.0.0.tar.gz
$ cd redis-6.0.0
$ make

PS:中间make会报错

​ 在源码的README.md文件中看到解释

​ 解决办法(针对2.2以上的版本)
​ 清理上次编译残留文件,重新编译

1
make distclean  && make

​ 这个错误的本质是我们在开始执行make 时遇到了错误(大部分是由于gcc未安装),然后我们安装好了gcc 后,我们再执行make ,这时就出现了jemalloc/jemalloc.h: No such file or directory。这是因为上次的编译失败,有残留的文件,我们需要清理下,然后重新编译就可以了。

src 目录 中现在提供了已编译的二进制文件 。使用以下命令运行Redis服务:

1
$ src/redis-server

​ 可以使用内置客户端与Redis进行交互:

1
2
3
4
5
$ src/redis-cli
redis> set foo bar
OK
redis> get foo
"bar"

​ Redis服务端,配置远程连接密码:

​ Redis客户端,远程连接:

安装Lua

​ Linux安装 Lua ,下载源码包并在终端解压编译即可

1
2
3
4
5
curl -R -O http://www.lua.org/ftp/lua-5.3.0.tar.gz
tar zxf lua-5.3.0.tar.gz
cd lua-5.3.0
make linux test
make install

​ make报错,找不到readline/readline.h

​ 原因是缺少 libreadline-dev 依赖包,安装依赖包:

1
apt-get install libreadline-dev

​ 再次编译安装后,成功:

漏洞复现

​ 利用Python脚本poc.py来重现该漏洞。

1
python poc.py <host> [<port>]

​ 初次连接,发送poc时,提示redis运行保护模式,只允许本地访问,有三种关闭方式,此处选择第一种关闭方式:

​ 根据提示关闭,保护模式,并且取消密码设置。

1
2
CONFIG SET protected-mode no
CONFIG SET requirepass ''

​ 再次发送poc,无数据回显,拒绝服务攻击成功。

​ Redis服务端报段错误,说明漏洞触发。

漏洞分析

技术背景

​ Redis是轻量级的,非易失性键值数据存储。 它通过Redis序列化协议(RESP)提供对简单易变数据结构的访问,该协议是基于TCP的协议。 与大多数其他数据库一样,Redis遵循客户端—服务器模型。 客户端能够通过Redis命令在Redis服务器上创建,修改和检索记录。

​ 例如,以下命令创建“ TEST”字符串记录并将其分配给“ 1234”键值,将此记录修改为“ TEST2”并分别检索记录:

1
2
3
SET 1234 TEST
GETSET 1234 TEST2
GET 1234

​ 有关Redis命令的完整列表,请参考 http://redis.io/commands

​ Redis客户端通过端口6379通过TCP使用Redis序列化协议(RESP)与服务器进行通信。可通过 http://redis.io/topics/protocol获得该协议详细说明。 RESP使用五种数据类型,这些数据类型由相应数据的第一个字节标识:

  • 简单字符串以“ +”字符开头

  • 错误以“-”字符开头

  • 整数以“:”字符开头

  • 批量字符串以“ $”字符开头

  • 数组以“ *”字符开头

    ​ 批量字符串以“ $”字符开头,后跟相应字符串的长度。 以下重点介绍如何将“ Sangfor”表示为大容量字符串:

1
2
$7 CRLF
TELUS

​ 其中CRLF表示新的行序列回车(CR),后跟换行(LF)。

​ RESP数组以“ *”字符开头,后跟数组中的元素数。 下面说明了一个由2个元素组成的大容量字符串数组:

1
2
3
4
5
*2 CRLF
$7 CRLF
Sangfor CRLF
$4 CRLF
TEST CRLF

​ 所有Redis命令都通过RESP字符串数组发送到服务器。 例如,上述SET命令将以下形式发送:

1
2
3
4
5
6
7
*3 CRLF
$3 CRLF
SET CRLF
$4 CRLF
1234 CRLF
$4 CRLF
TEST CRLF

​ Lua是Redis 支持的轻量级脚本语言。 Redis内置了Lua解释器。Lua在Redis中的使用方法,可参考https://www.redisgreen.net/blog/intro-to-lua-for-redis-programmers/

​ Redis客户端可通过EVAL命令使用此解释器。 Lua脚本允许用户管理和操纵Redis服务器上的记录。 例如,以下Lua脚本可用于执行上述SET命令:

1
EVAL "redis.call('set','1234','TEST')" 0

​ 执行此Lua脚本的另一种方法是使用SCRIPT LOAD和EVALSHA命令。 SCRIPT LOAD命令在服务器上缓存Lua脚本,并返回SHA1摘要。 EVALSHA命令可以与此SHA1摘要一起用作执行脚本的参数。 下面是一个示例:

1
2
SCRIPT LOAD "redis.call('set','1234','TEST')" 0
EVALSHA <SHA1 digest from above>

​ Lua还提供了分别使用struct.pack和struct.unpack方法将Lua变量与C类型的Stucts相互转换的方法。 例如,以下内容可用于包装整数:

1
struct.pack ("<I2", 10)
  • “I”指定整数是无符号的(“ i”用于有符号整数)
  • “<”指定应以小端格式表示(“>”用于大端格式)
  • “2”指定整数为2个字节长
  • “10”指定要打包的整数的值

详细分析

漏洞利用过程

​ Redis中存在堆栈缓冲区溢出漏洞。 在攻击情况下,struct.pack方法用于转换指定为第一个参数的非常大的整数(2^31)。 lua_struct.c尝试通过调用getnum()将此字符串参数转换为整数。 这导致整数溢出,导致大小被错误地计算为负数,从而绕过边界检查。 然后,此负数通过optsize()强制转换为size_t类型的变量,这将其解释为大正数。 之后,putinteger()尝试将由这个大数字表示的字节数复制到32字节的缓冲区中,从而导致缓冲区溢出。

代码分析

​ 以下代码段摘自lua_struct.c,Redis版本6.0.0。

​ 分析lua_struct.c中b_pack()函数,size变量存放optsize()函数的返回值,optsize()函数将有符号的int类型转换为size_t(无符号的int类型)整数溢出发生字此函数内,之后size变量传入putinteger()函数,缓冲区溢出发生在此函数内。

​ 先跟进optsize()函数,看如何处理opt变量和fmt二级指针(指向指针的指针),发现当opt变量为字符’i’或’I’时,将fmt二级指针传入

getnum()函数,将返回的值存入sz变量中,if语句判断sz变量是否大于MAXINTSIZE;当sz发生整数溢出时,值为负值且小于MAXINTSIZE(0x7fffffff),所以跳过此报错判断。

​ 跟进getnum()函数,字符串转整型,缺少边界检查,当字符串表示的数大于MAXINTSIZE(0x7fffffff)时,直接返回负值。

​ 跟进putinteger()函数,变量size是无符号的整数,经过int类型转换后,负值变为一个大于0x7fffffff的大数,buff数组范围为MAXINTSIZE(0x7fffffff),size变量范围大于MAXINTSIZE(0x7fffffff),故导致缓冲区溢出。

漏洞触发过程

​ struct.pack指定为第一个参数的非常大的整数(2^31),b_pack()———>optsize()———>getnum()导致整数溢出;返回的无符号变量size传入,putinteger()导致缓冲区溢出。

​ 发送poc后,到达崩溃现场,ESP与EBP中间地址被覆盖:

​ Redis服务端尝试恢复内存数据失败后退出进程,达到拒绝服务目的。

补丁分析

​ 官方已经发布补丁,在getnum()函数处,增加lua_State变量指针L,来存放错误信息,在98-99行增加代码:

1
2
if (a > (INT_MAX / 10) || a * 10 > (INT_MAX - (**fmt - '0')))
luaL_error(L, "integral size overflow");

​ 对当前变量a的值做判断,若当前a大于INT_MAX/10或者当前a*10大于INT_MAX-当前位的值,则返回报错信息L,提示size值超过缓冲区的值。从根本上避免了整数溢出,也避免了后续因为有符号上溢,而导致的缓冲区溢出。

流量分析

攻击包分析

​ 攻击包为RESP协议,Redis默认监听在6379,使用Redis命令,Redis命令详解请看 3.1技术背景。

1
2
3
4
5
6
7
*3表示创建一个大小为3的数组
$4
EVAL 表示创建长度为4的字符串,值为EVAL
$42
struct.pack('<4294967295', '1', '2', '3') 同上
$1
0 同上

​ 流量结构等同于在Redis客户端输入以下命令:

1
EVAL "struct.pack('<4294967295', '1', '2', '3')" 0

​ 之后无返回数据,则说明拒绝服务攻击成功。

正常包分析

​ Redis客户端发送RESP命令:

1
EVAL "struct.pack('<I1', '1', '2', '3')" 0

​ 正常包是符合RESP协议,会有回显。

漏洞检测和防御

漏洞检测

​ 缓冲区溢出漏洞只能通过版本对比检测。

​ 运行Redis-server后,控制台会打印版本信息,通过版本比对,若版本低于6.0.3,则有漏洞。

漏洞防御

​ 匹配6379端口的流量,匹配字符串的RESP数组。 由于RESP数组始终以“ *”字符开头,对该字符进行字符串匹配。 检测数组的元素是否有EVAL或SCRIPT LOAD命令。

​ 然后,检测调用struct.pack()方法的数组中匹配Lua脚本代码,并提取该方法的第一个参数。 必须确保此字符串参数的前两个字节包含以下任何字符:“ I”,“ i”,“ <”或““>”。 如果此值大于0x7FFFFFFF,则应将流量视为恶意流量,并且可能正在利用此漏洞进行攻击。

  • 必须以区分大小写的方式对Lua脚本代码中的对象和方法名称进行字符串匹配。
  • 必须以不区分大小写的方式执行RESP协议上的字符串匹配。
  • 由于脚本语言的性质,使用各种脚本技术很容易混淆代码的真实攻击。

漏洞修复

https://github.com/antirez/redis/commit/ef764dde1cca2f25d00686673d1bc89448819571

参考资料