文件上传功能看起来只是一个普通业务入口,实际却同时连接着文件存储、Web 服务器解析、应用权限和脚本执行多个环节。攻击者一旦能上传一份可被服务器执行的脚本,就可能把它当作 Web 后门,也就是常说的 Webshell。
判断木马不能只看某个扩展名,也不能只搜索几个固定函数。更可靠的方法是把下面几条链路串起来:
- 用户如何进入上传入口。
- 文件最终保存到哪里。
- 服务器把文件当成什么类型解析。
- 请求参数是否能进入动态执行或文件操作。
- 进程、网络和日志中留下了什么行为。
本文面向安全开发和授权排查。文中不提供可直接部署的 Webshell 代码;可疑样本应在隔离环境中处理。
一、文件上传的三个关键问题
审计上传功能时,可以先问三个问题:
- 文件实际保存到了哪里?
- 保存后的文件名和扩展名是什么?
- Web 服务器最终用什么规则解析这个文件?
前端显示“只允许图片”并不能证明后端安全。浏览器端的文件选择器、JavaScript 校验和 MIME 限制都可以被修改,真正有效的控制必须发生在服务端。
上传链路通常包括:
客户端选择文件
-> 后端接收请求
-> 校验类型与大小
-> 规范化文件名
-> 写入存储目录
-> 记录元数据
-> 后续访问或下载
任何一步只依赖客户端提供的信息,都可能产生安全缺口。
二、扩展名黑名单为什么容易失效
不少系统会维护一份“危险扩展名黑名单”,再删除文件名中的对应后缀。这种方法有几个天然问题。
2.1 解析规则比后缀更复杂
开发者看到一个文件名,服务器看到的可能是另一个文件名。以下因素都可能影响最终结果:
- 大小写处理。
- 多重扩展名。
- 尾随点或空格。
- URL 解码和 Unicode 规范化。
- Windows 与 Linux 的路径和文件名规则差异。
- 不同 Web 服务器的脚本映射配置。
- 文件系统或容器层对特殊名称的处理。
黑名单往往只覆盖开发者想到的写法,而服务器的解析器却可能支持更多组合。
2.2 应该使用允许列表
上传目录只允许业务真正需要的文件类型,例如:
允许:jpg、jpeg、png、webp、pdf
禁止:其他全部类型
允许列表仍不是唯一控制。还需要同时校验:
- 文件头与真实内容。
- MIME 类型。
- 文件大小。
- 图片解码是否成功。
- 扩展名与内容是否匹配。
- 最终保存路径是否仍在上传目录内。
2.3 文件名不能直接作为存储路径
原始文件名可能包含路径分隔符、控制字符、保留名称或超长内容。更稳妥的方式是:
- 服务端生成随机文件名。
- 只保留经过白名单处理的扩展名。
- 将文件存放在 Web 根目录之外。
- 通过受控的下载接口返回文件。
- 不在文件名中保留用户提供的路径结构。
三、服务器解析配置也是攻击面
文件“被上传”不等于“会执行”。很多风险来自服务器配置和上传目录权限。
3.1 上传目录不应执行脚本
合理的隔离方式是:
/srv/app/code 应用代码,只读
/srv/app/uploads 用户文件,不可执行
/srv/app/private 敏感文件,禁止直接访问
Web 服务器应明确拒绝在上传目录中执行 PHP、JSP、ASP 等脚本。仅通过扩展名判断脚本类型,通常不够稳固。
3.2 限制目录级配置覆盖
某些服务器允许目录中的局部配置文件改变脚本解析方式。如果上传目录可以写入这类配置,原本“只允许图片”的目录也可能改变执行规则。
可选的控制包括:
- 禁止上传目录覆盖服务器配置。
- 关闭不必要的动态解析。
- 上传和下载使用独立域名。
- 将文件交给对象存储,由独立服务提供访问。
- 对上传内容进行异步扫描。
3.3 Windows 文件名细节
Windows 和 NTFS 对文件名有一些容易忽略的规则,例如尾随点、空格和数据流语义。安全检查和最终落盘如果使用不同的名称规范化方式,就可能出现“检查的对象”和“执行的对象”不一致。
因此,保存文件时应统一完成:
- Unicode 规范化。
- 去除路径分隔符。
- 去除尾随点和空格。
- 统一大小写策略。
- 只保留允许的扩展名。
- 检查最终解析后的绝对路径。
不要先检查原文件名,再用另一个库保存原始名称。
四、Webshell 的典型行为特征
Webshell 的核心不是某种固定语法,而是“远程请求能够影响服务端执行”。常见行为包括:
- 从请求参数读取命令、路径或代码。
- 使用动态函数调用执行字符串。
- 读写任意文件。
- 列出目录、探测权限和运行环境。
- 建立网络连接或下载额外文件。
- 对输出进行编码、压缩或加密。
- 使用随机参数名规避固定规则。
- 在响应中拼接固定标记,方便客户端识别结果。
从抽象流程看,危险链路通常长这样:
外部请求参数
-> 解码或拼接
-> 动态函数选择
-> 命令执行 / 文件写入 / 网络请求
单独看到 eval、system 或文件读取函数,不一定能证明存在后门。需要结合参数来源、访问控制和调用上下文判断。
4.1 常见高危函数
在 PHP 代码审计中,可以重点关注以下能力:
| 能力 | 常见函数或机制 |
|---|---|
| 动态执行代码 | eval、动态函数调用 |
| 执行系统命令 | system、shell_exec、exec |
| 文件读取与写入 | 文件操作函数 |
| 网络请求 | 远程请求相关函数 |
| 进程与环境探测 | 系统信息、权限和目录函数 |
这些函数有正常用途,不能只靠名称判定恶意。关键是检查参数是否来自不可信输入,以及调用是否受到授权和路径约束。
五、混淆与沙箱
后门样本经常通过编码、字符串拆分和运行时拼接隐藏特征。静态搜索可能会漏掉:
- Base64 或其他编码后的字符串。
- 分段存储后重新拼接的函数名。
- 动态回调。
- 加密配置。
- 运行时生成的临时脚本。
沙箱可以在隔离环境中观察文件、进程、网络和注册表等行为。但沙箱也不是绝对可靠:
- 样本可能检测虚拟环境后停止运行。
- 恶意行为可能需要特定参数才会触发。
- 加密通信可能隐藏真实请求内容。
- 供应链样本可能延迟加载。
沙箱结果适合与静态分析、网络日志和主机日志互相印证。
六、管理工具留下的线索
蚁剑、冰蝎、哥斯拉等工具经常在通讯层做编码和加密,不同版本的特征也不断变化。与其依赖某个固定字符串,不如观察组合特征:
- 请求参数名异常随机。
- 同一接口的请求体长度和响应长度呈规律变化。
- 响应包含编码后的结构化内容。
- 访问集中在很少使用的脚本路径。
- 请求后伴随异常文件写入或子进程创建。
- 客户端访问频率与正常业务明显不同。
6.1 日志关联
一次可疑上传应至少关联以下日志:
WAF 日志
request_id
source_ip
path
action
Web 访问日志
request_id
method
status
response_size
应用日志
user_id
upload_result
storage_path
主机与文件日志
process
file_write
network_connection
只有把请求、落盘和后续访问串起来,才能区分正常文件上传和真正的后门部署。
七、可执行的防御设计
7.1 上传校验
建议按以下顺序处理文件:
1. 验证登录身份和上传权限
2. 检查请求大小和字段数量
3. 读取实际文件头,不信任客户端 MIME
4. 允许列表判断文件类型
5. 使用服务端随机文件名
6. 写入不可执行目录
7. 记录哈希、大小和归属
8. 异步进行病毒和内容扫描
7.2 目录隔离
上传存储与代码目录分离
上传目录关闭脚本执行
文件通过受控接口下载
敏感目录不允许外部直接访问
7.3 最小权限
Web 进程和文件处理服务都不应以管理员权限运行。即使某个组件失陷,也要限制它能够访问的目录、网络和系统资源。
7.4 运行时监控
可以监控以下异常:
- Web 进程启动 shell 或系统命令。
- 上传目录出现新的脚本文件。
- Web 进程读取系统敏感文件。
- Web 进程连接陌生外部地址。
- 很少访问的路径突然产生大量请求。
- 同一用户连续上传文件名差异很大的文件。
八、排查清单
- 上传类型使用允许列表。
- 同时检查扩展名、MIME 和文件内容。
- 保存时使用随机文件名。
- 上传目录无法执行脚本。
- 上传目录不能覆盖服务器配置。
- Web 根目录不包含敏感备份和临时文件。
- 应用账号遵循最小权限。
- 生产环境禁用不必要的动态执行功能。
- 文件哈希、上传者和访问日志可以关联。
- 可疑文件只在隔离环境中分析。
- 发现后门后同时排查横向移动和持久化。
九、事件响应
确认存在 Webshell 后,单纯删除文件通常不够。还应检查:
- 文件首次出现时间。
- 上传请求和后续访问记录。
- 是否创建了其他后门或计划任务。
- 是否读取了配置、密钥或数据库凭据。
- 是否发生横向登录或数据外传。
- 受影响账号和密钥是否需要轮换。
修复时应同时处理入口漏洞、目录权限和检测缺口,避免只清理当前样本。