飞牛 NAS OS app-center-static 漏洞修复逆向分析报告
飞牛 NAS OS app-center-static 路径穿越漏洞修复逆向分析报告
1. 执行摘要
本报告针对飞牛 NAS OS 1.1.19 版本中 app-center-static 路径穿越漏洞的修复情况进行深入逆向分析。通过对比旧版本(存在漏洞)和新版本(已修复)的二进制代码,识别出开发者实施的多层安全加固措施。
关键发现:
- 新增 4 个安全校验函数
- 实施白名单验证机制
- 引入路径规范化与基目录校验
- 增加文件类型验证(Magic Number 检查)
2. 漏洞背景回顾
2.1 原始漏洞(旧版本)
PoC: /app-center-static/serviceicon/myapp/%7B0%7D/?size=../../../../
漏洞成因:
数据流:
请求 → GetStatic → 解析参数 → 替换 {0} 为 size 参数值 → ServeFile
↓
未对 size 进行过滤
↓
size="../../../../" 直接拼入路径
↓
路径穿越漏洞graph TD
A[请求] --> B[GetStatic]
B --> C[解析参数]
C --> D["替换 {0} 为 size 参数值"]
D --> E[ServeFile]
C --> F[未对 size 进行过滤]
F --> G["size=../../ 直接拼入路径"]
G --> H[路径穿越漏洞]
H -.-> E
style A fill:#e0e0e0,color:#333
style B fill:#4a90e2,color:#fff
style C fill:#4a90e2,color:#fff
style D fill:#f5a623,color:#fff
style E fill:#7ed321,color:#fff
style F fill:#d0021b,color:#fff
style G fill:#d0021b,color:#fff
style H fill:#d0021b,color:#fff,stroke:#900,stroke-width:3px
旧版本关键代码逻辑:
- 从 URL 参数获取
appname、filename、type - 从 Query 获取
size参数 - 使用
strings.Replace将路径中的{0}替换为size值 - 直接调用
net_http_ServeFile返回文件
问题: 未对 size 参数进行任何验证,攻击者可传入 ../../../ 等路径穿越序列。
3. 新版本安全修复分析
3.1 修复概览
| 修复层面 | 具体措施 | 函数地址 |
|---|---|---|
| 输入验证 | 安全段校验 | 0x12dbe80 (isSafeSegment) |
| 路径拼接 | 安全路径连接 | 0x12dbfc0 (safeJoin) |
| 文件访问 | 基目录验证 | 0x12dc220 (validateFileInBase) |
| 文件类型 | 图片格式验证 | 0x12dc560 (validateImageFile) |
3.2 详细代码分析
3.2.1 isSafeSegment - 安全段校验函数
函数地址: 0x12dbe80
功能: 验证路径段是否包含危险字符
_BOOL8 appstore_core_web_controller_isSafeSegment(
__int64 segment_ptr, // 路径段指针
__int64 segment_len, // 路径段长度
...
)安全检查逻辑:
// 1. 空值检查
if (segment_len == 0) return FALSE;
// 2. 长度限制(最大128字符)
if (segment_len > 128) return FALSE;
// 3. 危险字符检查 - 禁止以下字符
// 空格、制表符、回车、换行符
if (strings_IndexAny(segment, " \t\r\n") >= 0) return FALSE;
// 4. 空字节检查
if (internal_stringslite_Index(segment, "\x00") >= 0) return FALSE;
// 5. 绝对路径检查 - 禁止以 / 开头
if (internal_stringslite_Index(segment, "/") >= 0) return FALSE;
// 6. 路径穿越检查 - 禁止 ".."
if (internal_stringslite_Index(segment, "..") >= 0) return FALSE;
return TRUE;安全价值:
- 阻止包含路径分隔符的输入
- 阻止路径穿越序列 (
..) - 限制输入长度,防止缓冲区相关攻击
3.2.2 safeJoin - 安全路径连接函数
函数地址: 0x12dbfc0
功能: 安全地连接基础路径和子路径
__int64 appstore_core_web_controller_safeJoin(
__int64 base_path, // 基础路径
__int64 base_len,
__int64 sub_path, // 子路径
__int64 sub_len,
...
)安全检查逻辑:
// 1. 空值检查
if (sub_path == NULL) return 0;
// 2. 危险字符检查 - 禁止空字节
if (internal_stringslite_Index(sub_path, "\x00") >= 0) return 0;
// 3. 路径规范化
cleaned_path = internal_filepathlite_Clean(sub_path);
// 4. 绝对路径检查 - 禁止以 / 开头的绝对路径
if (cleaned_path[0] == '/') return 0;
// 5. 路径穿越检查 - 检查 ".." 前缀
if (sub_len == 2 && cleaned_path == "..") return 0;
if (sub_len >= 3 && strncmp(cleaned_path, "../", 3) == 0) return 0;
// 6. 执行路径连接
joined_path = path_filepath_join(base_path, cleaned_path);
// 7. 验证结果路径仍在基础路径内
cleaned_base = internal_filepathlite_Clean(base_path);
if (!path_starts_with(joined_path, cleaned_base)) return 0;
return joined_path;安全价值:
- 确保子路径是相对路径
- 阻止路径穿越尝试
- 验证最终路径不超出基础目录
3.2.3 validateFileInBase - 基目录验证函数
函数地址: 0x12dc220
功能: 验证目标文件是否在允许的基目录内
__int64 appstore_core_web_controller_validateFileInBase(
__int64 base_dir, // 基础目录
__int64 base_len,
__int64 file_path, // 目标文件路径
__int64 file_len,
...
)安全检查逻辑:
// 1. 获取文件状态信息
file_stat = os_Lstat(file_path);
if (error) return 0;
// 2. 检查是否为符号链接 (ModeSymlink = 0x8000000)
if (file_stat.mode & 0x8000000) return 0;
// 3. 检查文件类型 - 只允许常规文件
// 屏蔽位: 0x8F280000 (设备文件、管道、socket等)
if (file_stat.mode & 0x8F280000) return 0;
// 4. 解析符号链接(获取真实路径)
real_file_path = path_filepath_EvalSymlinks(file_path);
if (error) return 0;
// 5. 解析基础目录的真实路径
real_base_dir = path_filepath_EvalSymlinks(base_dir);
if (error) {
// 如果解析失败,使用规范化路径
real_base_dir = internal_filepathlite_Clean(base_dir);
}
// 6. 验证文件路径以基础目录开头
// 方法: real_file_path 必须等于 real_base_dir 或以 real_base_dir + "/" 开头
if (!path_is_within(real_file_path, real_base_dir)) return 0;
return file_path;安全价值:
- 阻止通过符号链接跳出目录
- 确保只访问常规文件(非设备、管道等)
- 基于真实路径的边界检查
3.2.4 validateImageFile - 图片格式验证函数
函数地址: 0x12dc560
功能: 验证文件是否为有效的图片格式
__int64 appstore_core_web_controller_validateImageFile(
__int64 file_path,
__int64 path_len,
...
)安全检查逻辑:
// 1. 打开文件(只读模式)
file = os_OpenFile(file_path, O_RDONLY, 0);
if (error) return FALSE;
// 2. 读取文件头部(前512字节)
buffer = make([]byte, 512);
n, err = file.Read(buffer);
// 3. 检测 MIME 类型
mime_type = net_http_DetectContentType(buffer, n);
// 4. 检查 MIME 类型是否在白名单中
// 白名单包括: image/png, image/jpeg, image/gif, image/webp 等
allowed_types = map[string]bool{
"image/png": true,
"image/jpeg": true,
"image/gif": true,
"image/webp": true,
...
};
if (!allowed_types[mime_type]) return FALSE;
return TRUE;安全价值:
- 基于文件内容(Magic Number)验证类型
- 阻止非图片文件的访问
- 防止通过扩展名伪装攻击
3.3 GetStatic 函数流程对比
旧版本(存在漏洞)
GetStatic (0x11542e0)
│
├── 解析路由参数 (appname, filename, type)
│
├── 解析 Query 参数 (size)
│
├── 根据 type 分支处理
│ │
│ └── serviceicon 分支:
│ │
│ ├── 构建基础路径: /var/apps/${appname}/...
│ │
│ ├── 拼接 filename (含 {0} 占位符)
│ │
│ ├── 替换 {0} 为 size 参数值 ← 漏洞点!
│ │ └── size="../../../" 直接拼入
│ │
│ └── ServeFile(最终路径) ← 读取任意文件
│
└── 返回文件内容flowchart TD A["GetStatic (0x11542e0)"] --> B["解析路由参数
(appname, filename, type)"] B --> C["解析 Query 参数
(size)"] C --> D["根据 type 分支处理"] D --> E["serviceicon 分支:"] E --> F["构建基础路径:
/var/apps/${appname}/..."] F --> G["拼接 filename
(含 {0} 占位符)"] G --> H["替换 {0} 为 size 参数值"] H -.->|漏洞点!| I["size='../../../'
直接拼入"] I -.-> J["ServeFile(最终路径)"] J -.->|读取任意文件| K["返回文件内容"] H -->|"正常路径"| K style A fill:#636e72,color:#fff style B fill:#74b9ff,color:#fff style C fill:#74b9ff,color:#fff style D fill:#74b9ff,color:#fff style E fill:#a29bfe,color:#fff style F fill:#fd79a8,color:#fff style G fill:#fdcb6e,color:#2d3436 style H fill:#e84393,color:#fff,stroke:#d63031,stroke-width:3px style I fill:#d63031,color:#fff,stroke:#ff7675,stroke-width:3px style J fill:#d63031,color:#fff style K fill:#00b894,color:#fff linkStyle 7 stroke:#d63031,stroke-width:3px,stroke-dasharray: 5 5 linkStyle 8 stroke:#d63031,stroke-width:3px,stroke-dasharray: 5 5 linkStyle 9 stroke:#d63031,stroke-width:3px,stroke-dasharray: 5 5
新版本(已修复)
GetStatic (0x12dc760)
│
├── 解析路由参数 (appname, filename, type)
│
├── 【新增】验证 appname: isSafeSegment(appname)
│ └── 失败 → 返回 404,记录日志
│
├── 解析 Query 参数 (size)
│
├── 【新增】验证 filename: strings.TrimLeft(filename, "/0ml")
│ └── 移除前导斜杠和空字节
│
├── 根据 type 分支处理
│ │
│ ├── icon 分支 (type="icon"):
│ │ │
│ │ ├── 【新增】白名单检查: size 必须是 "32"/"64"/"128"/"256"
│ │ │ └── 失败 → 使用默认值 "256"
│ │ │
│ │ ├── 构建路径: /var/apps/${appname}/ICON_{size}.PNG
│ │ │
│ │ ├── 【新增】validateFileInBase(base_dir, file_path)
│ │ │ └── 失败 → 返回 404
│ │ │
│ │ └── ServeFile
│ │
│ └── poster/wizard 分支:
│ │
│ ├── 【新增】safeJoin(base_dir, filename)
│ │ └── 失败 → 返回 404
│ │
│ ├── 【新增】validateFileInBase(base_dir, file_path)
│ │ └── 失败 → 返回 404
│ │
│ └── ServeFile
│
└── 返回文件内容flowchart TD
A["GetStatic (0x12dc760)"] --> B["解析路由参数
(appname, filename, type)"]
B --> C["【新增】验证 appname:
isSafeSegment(appname)"]
C -->|失败| C1["返回 404
记录日志"]
C -->|通过| D["解析 Query 参数 (size)"]
D --> E["【新增】验证 filename:
strings.TrimLeft(filename, '/0ml')"]
E -->|移除前导斜杠
和空字节| F["根据 type 分支处理"]
F --> G["icon 分支
(type='icon'):"]
F --> H["poster/wizard 分支:"]
G --> G1["【新增】白名单检查:
size 必须是 '32'/'64'/'128'/'256'"]
G1 -->|失败| G2["使用默认值 '256'"]
G1 -->|通过| G3["构建路径:
/var/apps/${appname}/ICON_{size}.PNG"]
G2 --> G3
G3 --> G4["【新增】validateFileInBase
(base_dir, file_path)"]
G4 -->|失败| G5["返回 404"]
G4 -->|通过| G6["ServeFile"]
H --> H1["【新增】safeJoin
(base_dir, filename)"]
H1 -->|失败| H2["返回 404"]
H1 -->|通过| H3["【新增】validateFileInBase
(base_dir, file_path)"]
H3 -->|失败| H4["返回 404"]
H3 -->|通过| H5["ServeFile"]
G6 --> I["返回文件内容"]
H5 --> I
style A fill:#2d3436,color:#fff,stroke:#00b894,stroke-width:3px
style B fill:#74b9ff,color:#fff
style C fill:#00b894,color:#fff,stroke:#00b894,stroke-width:2px
style C1 fill:#d63031,color:#fff
style D fill:#74b9ff,color:#fff
style E fill:#00b894,color:#fff,stroke:#00b894,stroke-width:2px
style F fill:#74b9ff,color:#fff
style G fill:#fdcb6e,color:#2d3436
style G1 fill:#00b894,color:#fff,stroke:#00b894,stroke-width:2px
style G2 fill:#e17055,color:#fff
style G3 fill:#fdcb6e,color:#2d3436
style G4 fill:#00b894,color:#fff,stroke:#00b894,stroke-width:2px
style G5 fill:#d63031,color:#fff
style G6 fill:#74b9ff,color:#fff
style H fill:#a29bfe,color:#fff
style H1 fill:#00b894,color:#fff,stroke:#00b894,stroke-width:2px
style H2 fill:#d63031,color:#fff
style H3 fill:#00b894,color:#fff,stroke:#00b894,stroke-width:2px
style H4 fill:#d63031,color:#fff
style H5 fill:#74b9ff,color:#fff
style I fill:#00b894,color:#fff,stroke:#00b894,stroke-width:3px
linkStyle 0 stroke:#636e72,stroke-width:2px
linkStyle 1 stroke:#636e72,stroke-width:2px
linkStyle 2 stroke:#d63031,stroke-width:2px
linkStyle 3 stroke:#00b894,stroke-width:3px
linkStyle 4 stroke:#636e72,stroke-width:2px
linkStyle 5 stroke:#00b894,stroke-width:3px
linkStyle 6 stroke:#636e72,stroke-width:2px
linkStyle 7 stroke:#fdcb6e,stroke-width:2px
linkStyle 8 stroke:#a29bfe,stroke-width:2px
linkStyle 9 stroke:#00b894,stroke-width:3px
linkStyle 10 stroke:#e17055,stroke-width:2px
linkStyle 11 stroke:#fdcb6e,stroke-width:2px
linkStyle 12 stroke:#00b894,stroke-width:3px
linkStyle 13 stroke:#d63031,stroke-width:2px
linkStyle 14 stroke:#74b9ff,stroke-width:2px
linkStyle 15 stroke:#00b894,stroke-width:3px
linkStyle 16 stroke:#00b894,stroke-width:3px
linkStyle 17 stroke:#d63031,stroke-width:2px
linkStyle 18 stroke:#74b9ff,stroke-width:2px
linkStyle 19 stroke:#00b894,stroke-width:3px
linkStyle 20 stroke:#00b894,stroke-width:3px
4. 关键安全改进总结
4.1 输入验证层
| 检查项 | 旧版本 | 新版本 | 改进 |
|---|---|---|---|
| appname 验证 | ❌ 无 | ✅ isSafeSegment | 阻止特殊字符和路径穿越 |
| filename 清理 | ❌ 无 | ✅ TrimLeft | 移除前导斜杠和空字节 |
| size 白名单 | ❌ 无 | ✅ 严格白名单 | 只允许 “32”/“64”/“128”/“256” |
4.2 路径处理层
| 检查项 | 旧版本 | 新版本 | 改进 |
|---|---|---|---|
| 路径规范化 | ❌ 无 | ✅ filepath.Clean | 规范化路径表示 |
| 相对路径强制 | ❌ 无 | ✅ safeJoin | 阻止绝对路径 |
| 路径穿越防护 | ❌ 无 | ✅ “..” 检测 | 阻止父目录引用 |
| 基目录边界 | ❌ 无 | ✅ validateFileInBase | 确保不越界 |
4.3 文件访问层
| 检查项 | 旧版本 | 新版本 | 改进 |
|---|---|---|---|
| 符号链接检查 | ❌ 无 | ✅ Lstat + EvalSymlinks | 阻止符号链接攻击 |
| 文件类型验证 | ❌ 无 | ✅ 模式位检查 | 只允许常规文件 |
| 图片格式验证 | ❌ 无 | ✅ Magic Number 检测 | 基于内容验证 |
5. 修复效果评估
5.1 针对原始 PoC 的防护
原始攻击: GET /app-center-static/serviceicon/myapp/%7B0%7D/?size=../../../../
修复后的处理流程:
- appname 验证:
isSafeSegment("myapp")→ ✅ 通过 - size 白名单检查:
size="../../../../"不在{"32", "64", "128", "256"}中 → ❌ 拒绝 - 结果: 返回 404,记录安全日志
即使攻击者尝试其他绕过方式:
绕过 size 检查使用 filename:
filename="../../../etc/passwd"safeJoin()会检测到../前缀并拒绝
使用绝对路径:
filename="/etc/passwd"safeJoin()会检测到前导/并拒绝
使用符号链接: 创建指向
/etc/passwd的符号链接validateFileInBase()会检测并拒绝符号链接
使用空字节截断:
filename="icon.png\x00/../../../etc/passwd"isSafeSegment()会检测到空字节并拒绝
5.2 安全等级评估
| 评估维度 | 评分 | 说明 |
|---|---|---|
| 输入验证 | ⭐⭐⭐⭐⭐ | 多层白名单和黑名单检查 |
| 路径安全 | ⭐⭐⭐⭐⭐ | 规范化 + 基目录边界校验 |
| 文件类型 | ⭐⭐⭐⭐⭐ | 基于 Magic Number 的验证 |
| 日志审计 | ⭐⭐⭐⭐ | 包含 IP 和参数信息的日志 |
| 纵深防御 | ⭐⭐⭐⭐⭐ | 多层独立的安全检查 |
6. 技术实现亮点
6.1 纵深防御策略
新版本实施了多层独立的安全检查,即使某一层被绕过,其他层仍能提供保护:
攻击尝试 → 输入验证层 → 路径处理层 → 文件访问层 → 文件类型层
↓ ↓ ↓ ↓
isSafeSegment safeJoin validateFileInBase validateImageFile
↓ ↓ ↓ ↓
字符检查 路径规范化 符号链接解析 Magic Number
长度限制 基目录检查 真实路径验证 MIME 白名单flowchart TD A[攻击尝试] --> B[输入验证层] B --> C[路径处理层] C --> D[文件访问层] D --> E[文件类型层] B --> B1[isSafeSegment] B1 --> B2[字符检查] B1 --> B3[长度限制] C --> C1[safeJoin] C1 --> C2[路径规范化] C1 --> C3[基目录检查] D --> D1[validateFileInBase] D1 --> D2[符号链接解析] D1 --> D3[真实路径验证] E --> E1[validateImageFile] E1 --> E2[Magic Number] E1 --> E3[MIME 白名单] style A fill:#ff6b6b,color:#fff style B fill:#4ecdc4,color:#fff style C fill:#45b7d1,color:#fff style D fill:#96ceb4,color:#fff style E fill:#feca57,color:#333 style B1 fill:#4ecdc4,color:#fff style C1 fill:#45b7d1,color:#fff style D1 fill:#96ceb4,color:#fff style E1 fill:#feca57,color:#333
6.2 安全默认值
- size 参数: 不在白名单中时使用默认值 “256”,而非直接使用用户输入
- 验证失败: 统一返回 404,不暴露具体错误原因(信息隐藏)
- 日志记录: 记录客户端 IP 和尝试的参数值,便于安全审计
6.3 符号链接防护
validateFileInBase 函数通过以下方式防护符号链接攻击:
- 使用
Lstat而非Stat,获取符号链接本身的信息 - 检查文件模式,明确拒绝符号链接 (
ModeSymlink) - 使用
EvalSymlinks解析出真实路径后再进行边界检查
7. 结论与建议
7.1 修复结论
飞牛 NAS OS 1.1.19 版本对 app-center-static 路径穿越漏洞实施了全面且有效的修复:
- ✅ 彻底修复了原始漏洞(size 参数路径穿越)
- ✅ 预防了多种潜在的绕过方式
- ✅ 实施了纵深防御策略
- ✅ 增加了安全审计能力
7.2 安全建议
虽然修复非常全面,但仍建议:
- 定期安全审计: 对其他类似的静态资源接口进行安全审查
- 模糊测试: 对 GetStatic 接口进行 fuzzing,验证边界情况
- 监控告警: 对频繁的 404 响应进行监控,可能表明攻击尝试
- 代码审查: 将相同的安全模式应用到其他文件操作接口
7.3 最佳实践参考
本次修复展示了 Web 应用文件访问安全的最佳实践:
// 安全文件访问模式(基于本次修复推导)
func SecureFileAccess(baseDir, userInput string) (string, error) {
// 1. 输入验证
if !isSafeSegment(userInput) {
return "", errors.New("invalid input")
}
// 2. 安全路径连接
fullPath, err := safeJoin(baseDir, userInput)
if err != nil {
return "", err
}
// 3. 基目录验证
if !validateFileInBase(baseDir, fullPath) {
return "", errors.New("path traversal detected")
}
// 4. 文件类型验证(如适用)
if !validateImageFile(fullPath) {
return "", errors.New("invalid file type")
}
return fullPath, nil
}8. 附录:关键函数地址
| 函数名 | 地址 | 功能 |
|---|---|---|
| GetStatic | 0x12dc760 | 主处理函数 |
| isSafeSegment | 0x12dbe80 | 安全段校验 |
| safeJoin | 0x12dbfc0 | 安全路径连接 |
| validateFileInBase | 0x12dc220 | 基目录验证 |
| validateImageFile | 0x12dc560 | 图片格式验证 |
| setSecurityHeaders | 0x12dc420 | 安全响应头设置 |
| setCacheHeaders | 0x12dc4c0 | 缓存控制头设置 |
报告基于 IDA Pro 与 IDA-Pro-MCP 对飞牛 NAS app-center 相关二进制的逆向分析整理。 分析日期:2026-02-06
