登录后更精彩...O(∩_∩)O...
您需要 登录 才可以下载或查看,没有账号?立即注册
×
BUUCTF靶场59 -- [De1CTF 2019]SSRF Me
源码:https://github.com/CTFTraining/delta_2019_web_ssrfme
提示:flag is in ./flag.txt
Hint中写道flag在./flag.txt中,引导我们考虑如何利用SSRF读取flag.txt。 点开靶机,可见代码,其使用了Python的Flask框架。 Flask框架代码审计题
[Python] 纯文本查看 复制代码 #! /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)): #如果没有该文件夹,则创立一个文件夹
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') #注意w,可以对result.txt文件进行修改
resp = scan(self.param)
if (resp == "Connection Timeout"):
result['data'] = resp
else:
print resp
tmpfile.write(resp) #这个将resp中的数据写入result.txt中,可以利用为将flag.txt中的数据放进result.txt中
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() #读取result.txt中的数据
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
@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']) #注意这个绑定,接下来的几个函数都很重要,这个相当于c语言里面的主函数,接下来是调用其他函数的过程
def challenge():
action = urllib.unquote(request.cookies.get("action")) #cookie传递action参数,对应不同的处理方式
param = urllib.unquote(request.args.get("param", "")) #传递get方式的参数param
sign = urllib.unquote(request.cookies.get("sign")) #cookie传递sign参数sign
ip = request.remote_addr #获取请求端的ip地址
if(waf(param)): #调用waf函数进行过滤
return "No Hacker!!!!"
task = Task(action, param, sign, ip) #创建Task类对象
return json.dumps(task.Exec()) #以json的形式返回到客户端
@app.route('/')
def index():
return open("code.txt","r").read()
def scan(param):
socket.setdefaulttimeout(1)
try:
return urllib.urlopen(param).read()[:50] #这个可以利用为访问flag.txt。读取然后为下一步将flag.txt文件中的东西放到result.txt中做铺垫
except:
return "Connection Timeout"
def getSign(action, param): #getSign的作用是拼接secret_key,param,action,然后返回拼接后的字符串的md5加密值
return hashlib.md5(secert_key + param + action).hexdigest()
def md5(content): #将传入的字符串进行md5加密
return hashlib.md5(content).hexdigest()
def waf(param): #防火墙的作用是判断开头的几个字母是否是gopher 或者是file 如果是的话,返回true
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=9999)
代码审计:一个flask框架,先说一些需要注意的地方 1. /De1ta和/geneSign分别绑定不同的函数,具有不同的功能,接下俩会具体分析。 2. Task类这个类中有不同的参数action,对应不同的函数执行,但是需要注意到 [Python] 纯文本查看 复制代码 if "scan" in self.action:
if "read" in self.action:
判断action中的值的时候,用的是in,而不是==,所以如果action中是scanread或者是reanscan的话,if语句同时满足,相应的代码都执行。
3. python文件操控有许多关于python的文件操控的代码,所以在本地进行了复现 [Python] 纯文本查看 复制代码 #因为是在windows系统下复现,所以文件路径和源码中有些不同,但是原理一样,都是将flag.txt中的文件放在result.txt中
#! /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
f = open("D:/phpstudy_pro/WWW/test/result.txt",'w') #注意切换为写的功能
resp = open("D:/phpstudy_pro/WWW/test/flag.txt").read()
f.write(resp)
4. 得到flag的大致思路有了首先绕过self.checkSign(),并且传入的action需要同时包含scan和read,然后if "scan" in self.action:执行将flag.txt中的数据写入result.txt中,继续if "read" in self.action:执行读取result.txt中的数据,并且放在 result['data'] 中 , return json.dumps(task.Exec()) 接着返回以json的形式返回到客户端。
构造payload的步骤:
1. 首先需要绕过self.checkSign()分析一下相关源码,源码分别截取,方便分析
[Python] 纯文本查看 复制代码 @app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)
def checkSign(self):
if (getSign(self.action, self.param) == self.sign):
return True
else:
return False
def getSign(action, param): #getSign的作用是拼接secret_key,param,action,然后返回拼接后的字符串的md5加密值
return hashlib.md5(secert_key + param + action).hexdigest()
需要满足self.checkSign(),
就需要getSign(self.action, self.param) == self.sign,(而sign值通过cookie传值)
就需要hashlib.md5(secert_key + param + action).hexdigest() == self.sign,
说白了也就是hashlib.md5(secert_key + 'flag.txt' + 'readscan').hexdigest() == self.sign,即我们需要得到
secert_key + 'flag.txtreadscan'的哈希值
[Python] 纯文本查看 复制代码 @app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)
但是我们不知道secret_key的值是多少,它只存在于服务端,但是我们可以通过上面截取的源码中/geneSign,来返回我们所需要的编码之后的哈希值
注意到/geneSign中已经将action定为scan,所以我们传入的param可以为flag.txtread,这样的话还是会拼接为secert_key + 'flag.txtreadscan'
payload1:
[Python] 纯文本查看 复制代码 /geneSign?param=flag.txtread
返回哈希值
cd515232727a86ad8fd53caa3903eb0a
2. 将flag.txt中的数据读入result.txt,然后读取result.txt
[Python] 纯文本查看 复制代码 if "scan" in self.action:
tmpfile = open("./%s/result.txt" % self.sandbox, 'w') #注意w,可以对result.txt文件进行修改
resp = scan(self.param)
if (resp == "Connection Timeout"):
result['data'] = resp
else:
print resp
tmpfile.write(resp) #这个将resp中的数据写入result.txt中,可以利用为将flag.txt中的数据放进result.txt中
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() #读取result.txt中的数据
if result['code'] == 500:
result['data'] = "Action Error"
payload2:如下,注意修改cookie中参数action和参数sign的值

接着看上面的/De1ta路由,task实例化了一个对象,调用了一次对象中的Exec()函数,以json.dumps()输出,json.dumps()输出格式如下,
[Python] 纯文本查看 复制代码 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
直接看Exec()函数,第一步要绕checkSign(),看看checkSign(),
[Python] 纯文本查看 复制代码 def checkSign(self):
if (getSign(self.action, self.param) == self.sign):
return True
else:
return False
看到getSign()函数了,这是我们最开始审计的那个MD5加密的那个,也就是说我们需要判断action与param传入进去的值与密匙配合MD5加密过后的值与本身GET传入的sign值是否相匹配。 继续往下看,
[Python] 纯文本查看 复制代码 if "scan" in self.action:
if "read" in self.action:
这里我们笃定action中需要存在这两个段字符串也就是“scanread”或者“readscan”。到这里基本就分析完了,因为如果能绕过action这两个判断,就会执行如下,
[Python] 纯文本查看 复制代码 resp = scan(self.param)
[Python] 纯文本查看 复制代码 def scan(param):
socket.setdefaulttimeout(1)
try:
return urllib.urlopen(param).read()[:50]
except:
return "Connection Timeout"
因为hint给了,flag就在/flag.txt中
OK,现在我们需要做的事就是给sign找到一个MD5的值绕过checkSign(),因为不知道密匙所以我们可以利用/geneSign,但是action在/geneSign中的值不能修改为‘scan’,而我们要用密匙+param+action,而且我们能控制param的值所以我们赋值给param=flag.txtread,最后得到的MD5就是我们想要的,演示如下:
from:https://blog.csdn.net/qq_51295677/article/details/124449080
referer: https://blog.csdn.net/RABCDXB/article/details/115412359
|