文件上传功能看起来只是一个普通业务入口,实际却同时连接着文件存储、Web 服务器解析、应用权限和脚本执行多个环节。攻击者一旦能上传一份可被服务器执行的脚本,就可能把它当作 Web 后门,也就是常说的 Webshell。

判断木马不能只看某个扩展名,也不能只搜索几个固定函数。更可靠的方法是把下面几条链路串起来:

  1. 用户如何进入上传入口。
  2. 文件最终保存到哪里。
  3. 服务器把文件当成什么类型解析。
  4. 请求参数是否能进入动态执行或文件操作。
  5. 进程、网络和日志中留下了什么行为。

本文面向安全开发和授权排查。文中不提供可直接部署的 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 对文件名有一些容易忽略的规则,例如尾随点、空格和数据流语义。安全检查和最终落盘如果使用不同的名称规范化方式,就可能出现“检查的对象”和“执行的对象”不一致。

因此,保存文件时应统一完成:

  1. Unicode 规范化。
  2. 去除路径分隔符。
  3. 去除尾随点和空格。
  4. 统一大小写策略。
  5. 只保留允许的扩展名。
  6. 检查最终解析后的绝对路径。

不要先检查原文件名,再用另一个库保存原始名称。

四、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 后,单纯删除文件通常不够。还应检查:

  1. 文件首次出现时间。
  2. 上传请求和后续访问记录。
  3. 是否创建了其他后门或计划任务。
  4. 是否读取了配置、密钥或数据库凭据。
  5. 是否发生横向登录或数据外传。
  6. 受影响账号和密钥是否需要轮换。

修复时应同时处理入口漏洞、目录权限和检测缺口,避免只清理当前样本。