BUUCTF[De1CTF 2019]SSRF Me 1

考点:

  1:python代码审计

  2:SSRF理解

已知:flag在./flag.txt中

进入靶场:

为一串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
import importlib,sys
importlib.reload(sys)
sys.setdefaultencoding('latin1') #编码转换

app = Flask(__name__) #Flask框架

secert_key = os.urandom(16) #返回一个有n个byte那么长的一个string,然后很适合用于加密。


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)): #SandBox For Remote_Addr os.path.exists判断括号里的文件是否存在的意思,括号内的可以是文件路径。
            os.mkdir(self.sandbox) #用于以数字权限模式创建目录。

    def Exec(self):# 定义的命令执行函数。
        result = {}
        result['code'] = 500
        if (self.checkSign()):
            if "scan" in self.action:#action的值包括scan
                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:#action的值包括read
                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):#判断
            #hashlib.md5(secert_key + param + action).hexdigest()
            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", "")) #urllib.unquote字符串被当作url提交时会被自动进行url编码处理,  当需要获取前端页面表单传过来的id值的时候,我们就需要用request.args.get,而不能用request.form
    action = "scan"
    return getSign(action, param)
    #hashlib.md5(secert_key + param + action).hexdigest()
    #即: hashlib.md5(secert_key + param + 'scan').hexdigest()

@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()) #将一个Python数据结构转换为JSON

@app.route('/') # 根目录路由,就是显示源代码得地方
def index():
    return open("code.txt","r").read()


def scan(param):# 这是用来扫目录的函数
    socket.setdefaulttimeout(1)#代表经过t秒后,如果还未下载成功,自动跳入下一次操作,此次下载失败。
    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):#wef,需要绕过
    check=param.strip().lower() #用于移除字符串头尾指定的字符(默认为空格或换行符)或字符序列。
    if check.startswith("gopher") or check.startswith("file"):#.startswith用于检查字符串是否是以指定子字符串开头,
        return True
    else:
        return False


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

发现代码中中有两处地方可以传参(/geneSign  和 /De1ta):

@app.route("/geneSign", methods=['GET', 'POST'])#此路由用于测试
def geneSign():
    param = urllib.unquote(request.args.get("param", "")) #urllib.unquote字符串被当作url提交时会被自动进行url编码处理,  当需要获取前端页面表单传过来的id值的时候,我们就需要用request.args.get,而不能用request.form
    action = "scan"
    return getSign(action, param)
    #hashlib.md5(secert_key + param + action).hexdigest()
    #即: hashlib.md5(secert_key + param + 'scan').hexdigest()

@app.route('/De1ta',methods=['GET','POST'])#此路由用于注入
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
    ip = request.remote_addr
    if(waf(param)):
        return "No Hacker!!!!"
    task = Task(action, param, sign, ip)
    return json.dumps(task.Exec()) #将一个Python数据结构转换为JSON

 先看一下调用函数Task的函数(/De1ta的函数)

@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()) #将一个Python数据结构转换为JSON

可以传入可控的action,param,sign三个函数

先经过wef判断

  param中不能有'gopher'和'file',然后最后return json.dumps(task.Exec())

  而task.Exec()最后return result,所以我们判断,最后应该要将return的值等于flag,即return flag

看一下Exec()的构成

    def Exec(self):# 定义的命令执行函数。
        result = {}
        result['code'] = 500
        if (self.checkSign()):

溯源checkSign()

    def checkSign(self):
        if (getSign(self.action, self.param) == self.sign):#判断
            #hashlib.md5(secert_key + param + action).hexdigest()
            return True
        else:
            return False

hashlib.md5(secert_key + param + action).hexdigest() == self.sign

即让(secert_key + param + action)的md5值等于sign

但secert_key的值不知道,所以要用到第一个路由(/geneSign ):

@app.route("/geneSign", methods=['GET', 'POST'])#此路由用于测试
def geneSign():
    param = urllib.unquote(request.args.get("param", "")) #urllib.unquote字符串被当作url提交时会被自动进行url编码处理,  当需要获取前端页面表单传过来的id值的时候,我们就需要用request.args.get,而不能用request.form
    action = "scan"
    return getSign(action, param)
    #hashlib.md5(secert_key + param + action).hexdigest()
    #即: hashlib.md5(secert_key + param + 'scan').hexdigest()

action的值已经给定,我们只能控制param的值。然后得到secert_key + param + action的md5值。

所以我们只要让/De1ta中的secert_key + param + action的值等于/geneSign 中的secert_key + param + 'scan'就可以使secert_key + param + action == sign

if "scan" in self.action:#action的值包括scan
                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:#action的值包括read
                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

第一个if要求传入的action中含有scan,然后调用scan函数从文件中查找param这个文件,将文件名给resp,然后通过tmpfile函数写入result.txt中,且令它连通。即(code=200);第二个if要求传入的action中含有read,然后从result.txt中查找连通的文件作为f,并让result['data']=f.read得到它的内容。最后return result输出内容。

所以此处的action必须为含read和scan。

但/geneSign路由中规定的action必须为scan。但可以控制param

@app.route("/geneSign", methods=['GET', 'POST'])#此路由用于测试
def geneSign():
    param = urllib.unquote(request.args.get("param", "")) #urllib.unquote字符串被当作url提交时会被自动进行url编码处理,  当需要获取前端页面表单传过来的id值的时候,我们就需要用request.args.get,而不能用request.form
    action = "scan"
    return getSign(action, param)

但/De1ta路由中两个值都可以控制。

且两者最后的secert_key + param + action相等。我们已知flag在flag.txt中,而且最终要通过/De1ta中的param来得到,所以/De1ta中的param=flag.txt

所以

/geneSign路由中,我们令param=flag.txtread,此时action=scan

/De1ta路由中,我们令param=flag.txt,action=readscan

这样,最后secert_key + param + action的值是相同的。/De1ta中的param=flag.txt

 

1.用/geneSign路由get传入param=flag.txtread得到一个值,让它等于sign

 即sign=e8157ddb99ef0fb8d5513fd4a2e7b3a6

2.用/De1ta路由get传入param=flag.txt,cookie传入action=readscan,sign=e8157ddb99ef0fb8d5513fd4a2e7b3a6

 得到flag

 

posted @ 2021-12-08 20:14  LoYoHo00  阅读(351)  评论(0编辑  收藏  举报
levels of contents