开始SSRF漏洞学习
SSRF(Server - Side Request Forgery,服务器端请求伪造)
一种网络安全漏洞,它允许攻击者通过操纵服务器端发起的请求,以服务器的身份访问内部或外部资源。
1.原理
正常情况下,服务器会根据用户请求,从外部获取数据,比如获取某个图片、文件的内容。但如果服务器对用户输入的请求地址未进行严格过滤和验证,攻击者就可以修改这个请求地址,让服务器去访问其他任意地址。
2.危害
- 内部网络探测:攻击者能利用服务器探测内部网络结构,发现内部存活主机、开放端口等信息。比如通过向不同的内部 IP 和端口发送请求,根据响应判断是否开放。
- 敏感数据泄露:若内部存在数据库、文件服务器等,攻击者可构造请求获取敏感数据。例如访问内部数据库的特定接口获取用户账号密码。
- 攻击内部服务:可对内部服务发起攻击,如利用 SSRF 触发内部的拒绝服务(DoS)攻击,或者执行一些恶意的命令(如果服务存在命令执行漏洞且被 SSRF 利用)。
- 外网恶意利用:攻击者也可以让服务器访问一些需要认证的外网服务,冒用服务器身份进行恶意操作,如利用服务器的身份发送垃圾邮件等。
3.常见攻击场景
- 利用 file 协议读取本地文件:在支持file://协议的服务器环境中,攻击者可通过构造file:///etc/passwd(Linux 系统下获取用户信息文件)这样的地址,让服务器读取本地敏感文件。
- 利用 gopher 协议攻击 Redis:通过gopher://协议构造特定的 Redis 命令,如写入恶意配置,甚至获取服务器权限。例如构造命令写入 SSH 公钥,实现远程登录。
- 绕过防火墙限制:一些服务器出于安全考虑设置了防火墙,限制外部对内部的访问,但服务器自身访问内部不受限。攻击者利用 SSRF 可突破防火墙限制访问内部资源。
理解:访问A网站,用A网站去访问B网站,A为跳板,目的是B
豆包举的例子更好理解
题目来源:buuctf [De1CTF 2019]SSRF Me
一、打开靶机,整理信息
贴心,给了源码和提示,提示中告诉了我们flag信息藏在flag,txt文件中
./表示再当前目录
打开靶机很乱,依稀可以看出来是python代码,应该是源码,跟大佬学到一招:可以放在pycharm里Ctrl+Alt+L将代码格式化一下
二、解题思路
step 1:源码分析
#! /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')
源码很多很陌生,自己读不懂,跟着大佬的wp一步步来吧
1.先看路由,涵盖的面比较广
路由(Routing)
用于将不同的 URL 请求映射到对应的处理函数或类上,从而决定当用户访问特定 URL 时,执行哪些代码逻辑来生成响应。
理解:
路由就像交通导航系统,核心作用是根据不同的请求信息,将其精准地引导到对应的处理程序上。
@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())
主要功能:从请求的Cookie和查询参数中获取数据,对查询参数进行安全检查,然后创建一个Task对象并执行其Exec方法,最后执行结果以JSON格式返回给客户端。
分解功能:
- urllib.unquote 用于对URL编码的字符串进行解码
- request.cookies.get("action") 从请求的cookie中获取名为action的值
- request.args.get("param","") 从请求的查询参数中后去名为param的值,不存在在返回空,饭后解码后赋值给param变量
- request.remote._addr 获取客户端的IP地址,并赋值给ip变量
- challenge里,用参数构造一个Task类对象,并且执行它的Exec方法
通过get方法传入param参数值,在cookie里面传递action和sign的值,然后将传递的param通过waf这个函数进行安全检查。
2.接着看waf函数
def waf(param):
check=param.strip().lower()
if check.startswith("gopher") or check.startswith("file"):
return True
else:
return False
主要功能:对输入的参数param进行安全检查
分解:
(1)预处理函数check=param.strip().lower()
- param.strip() 会去除参数param首位的空白字符,如空格、制表符、换行符等
- .lower() 会将处理后的字符串转换为小写形式,防止大小写绕过
(2)检查协议前缀 if check.startswith("gopher") or check.startswith("file"):
- check.startswith("gopher") 是检查处理后的字符是否以gopher开头,gopher协议可以被用来构造复杂请求,从而发起SSRF攻击
- check.startswith("file") 检查处理后的字符是否以file开头,file协议允许访问本地文件系统
所以我们不能通过协议读取文件
3.看Exec方法
def Exec(self):
result = {}
result['code'] = 500 //将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
Exec方法先通过checkSign方法检测登录,所以要看checkSign方法
4.看checkSign方法
def checkSign(self):
if (getSign(self.action, self.param) == self.sign):
return True
else:
return False
判断我们传入的action、param参数通过getSign函数处理后是否和sign相等
3.看看getSign函数
def getSign(action, param):
return hashlib.md5(secert_key + param + action).hexdigest()
根据输入的action和param参数生成一个签名(sign),并且将三个东西拼接起来然后进行md5哈希算法加密。
.hexdigest()意为将哈希对象的结果以十六进制字符串的形式返回。
签名通常用于验证数据的完整性和真实性,防止数据在传输过程中被篡改。
这里好像没路了,那就看看完整源码里,哪里还用到了getSign函数,发现是/geneSign这个路由调用了getSign函数
4./geneSign路由
#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)
主要功能:为特定的scan操作生成签名,即可以生成我们需要的md5。这里的action只有“scan”
没有进一步信息了,回到Exec方法
5.回归Exec方法
- 初始化一个空字典result用于存储执行结果信息。将result字典的code字段初始化为500,表示默认执行出现错误
- 调用① if (self.checkSign()): 方法检查签名是否有效,有效则继续执行,否则跳转到else分支
- ①if分支内嵌套②if分支。打开result.txt文件,该文件位于self.sandbox目录下
- 调用scan函数,传入self.param作为参数,得到扫描结果resp
- ②if分支内嵌套③if分支。如果resp为Connection Timeout,表示连接超时,将该结果赋值给result字典的data字段。否则,将扫描结果写入文件,并关闭文件
- 将result字段的code字段设置为200,表示操作成功
- 仍为①if内嵌套④if分支进行读取操作
- 打开文件,将code字段设置为200,表示操作成功
- 将文件内容读取出来并赋值给data字段
- 若均未匹配,则code字段仍为500,data则为Action Error
- 若检查签名返回无效,code字段仍未500,并将msg字段设置为Action Error
- 最后返回包含执行结果的result字典。
step 2:源码信息总结
- scan函数使用了urllib.urlopen访问了外部资源
- 有两个路由:/De1ta和/geneSign(用来生成md5)
- 有了sign的生成过程:三个部分拼接而成
- Exec()函数中判断语句用的是in而不是==
- action中必须要有scan和read两个,才能读取flag
step 3:思路整理
突破点在于scan函数访问了外部资源,而检查签名时,调用了scan函数
而且没有对self.param进行严格过滤,仅仅只过滤了gopher协议和file协议。如果能控制param为./flag.txt,则能读取其中内容,若能成功读取flag.txt,则将其中内容写入result.txt中。
如果 'read' in self.action,则将result.txt读取并存到result['data']中,Exec()函数的最后,还返回了result,而在challenge()函数中,则返回了json.dumps(task.Exec()),即我们的此处的result。
先进入/De1ta绑定的challenge()函数,将在Exec中的scan部分中将flag.txt的内容存入result.txt,然后从read部分中将其存到result字典中读出,再以json形式返回到客户端,我们就能得到flag。
参考:[De1CTF 2019]SSRF Me之愚见 - 简书
step 4:开始解题
解法一:字符串拼接
尝试访问/geneSign?param=flag.txt,给了一个md5:da5e952b257d2f4e24759a8afc366e52
但是geneSign只有scan的功能,要加入read功能才可以,参考其他师傅的wp
因为geneSign中action只有scan,需要拼接read,而md5加密中拼接了三个东西,第一部分用到了scan,第二部分是flag.txt,第三部分是read,三个内容都有了,所以访问/geneSign?param=flag.txtread
拿到了我们要的md5,然后访问/De1ta?param=flag.txt,构造cookie action=readscan;sign=beb0d1508ca91ede850d2b160d635d87,即可拿到flag
解法二:哈希拓展攻击
这个还没学明白,先把大佬的wp贴在这文章 - De1CTF ssrf_me 的三种解法 - 先知社区
解法三:local_file
同上文章 - De1CTF ssrf_me 的三种解法 - 先知社区
三、小结
1.做的我脑子缠在一起了,看的我好乱,感觉问题出在了代码审计,审不明白,有代码解释但是没办法形成完整逻辑链
2.源码中包含urlopen字样,或者靶机中有提交文件名(说明可以将url作为文件名),要考虑SSRF漏洞
3.本题中学到了路由、签名等小知识点