NineOnee

导航

 

有关python的ssrf

前言

  • 环境:buuctf中[De1CTF 2019]SSRF Me
  • 知识点:python代码审计,ssrf

做题

审计源码

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):	#python类的构造函数
    	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') 
                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", "")) \\对参数Param进行url解码
    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())		\\打印出一个json对象
@ app.route('/') #显示源代码
def index(): 
	return open("code.txt", "r").read() 
def scan(param): 
	socket.setdefaulttimeout(1) 
    try: 
    	return urllib.urlopen(param).read()[: 50] #造成ssrf的漏洞,对param进行读取,返回前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() \\strip()移除两边的空格
    if check.startswith("gopher") or check.startswith("file"): #字符串开头不能是gopher和file
    	return True
	else :return False
if __name__ == '__main__': app.debug = False app.run(host = '0.0.0.0', port = 80)

根据题目提示这里存在ssrf漏洞

我们找到可以读取文件的函数,urllib.urlopen(param).read ,可以看到没有对param进行过滤,明显存在ssrf漏洞

def scan(param): 
	socket.setdefaulttimeout(1) 
    try: 
    	return urllib.urlopen(param).read()[: 50] #造成ssrf的漏洞,对param进行读取,返回前50个字符
    except: 
    	return "Connection Timeout"

追踪scan,在类Task里定义了Exec函数,如果满足了checkSign这个函数并且action这个字符串里面包含了scan,就会调用scan函数


先追踪哪里使用了这个类,可以看到在De1ta这个路由内,定义了一个Task的对象task,并且调用了Exec方法,将其结果以json的形式打印出来,看来注入点就是这个路由


调用Exec函数前,waf函数会对param参数进行过滤,过滤了协议file和gopher


接下来我们要做的就是满足checkSign函数,调用了getSign函数


而getSian函数,可以看到我们不知道secert_key是什么,也就无法直接构造sign了


但是我们在geneSign这个路由里,看到会返回getSign这个值,所以我们就可以通过这个路由,来获取sign值


根据代码逻辑,这里为什么param是flag.txtread呢

首先我们要搞清楚为什么要加个read,调用scan函数后,会将我们读取的文件写进result.txt内,如果read也在action这个字符串内,那么就会读取result.txt的内容,但是明明有个print,为什么还要read呢?这里print并不会输出,原因我也不晓得。

其次我们要搞清楚read的位置,根据getSign函数,加密顺序是secert_key+param+action,注意传进去是action在前

在geneSign
(scan,flag.txtread)=>flag.txtreadscan

那么在De1ta里

(readscan,flag.txt,sign)=>flag.txtreadscan



值得一试的是:通过urllib.urlopen(param).read(),读取内部文件不要file协议就可以读取,

而php的curl读取内部文件时,要通过file协议才可以,不然会报错


posted on 2020-12-07 22:38  NineOne_E  阅读(1117)  评论(0编辑  收藏  举报