解剖PHP Web服务器后门程序:一场针对F5 BIG-IP的无文件渗透行动

导语:SophosLabs(Sophos旗下的威胁研究团队)捕获了一款针对F5 BIG-IP访问策略管理器(Access Policy Manager,简称APM)环境的Linux植入器。它把传统Web shell从磁盘彻底抹掉,改成活在Apache进程内存里——通过劫持libc(Linux标准C语言库)启动流程抢先执行,再钩住(hook,拦截替换原函数)Apache的模块加载机制,等PHP模块加载才动手,最后用内存映射(mmap)钩子在三个特定PHP文件前偷塞Web shell。F5把这波关联活动(编号c05d5254)与CVE-2025-53521联系在一起——那是BIG-IP APM上一个已被野外利用的无认证远程代码执行漏洞。


一、为什么这个案例值得关注

防守方一听到”Web shell”,脑子里通常浮现的是那种写死在Web可访问目录里的小脚本——PHP、JSP(Java服务器页面)、ASP(微软动态服务器网页)都行。它躺在磁盘上,最多加点混淆或藏在合法文件堆里,但终究是个文件。这套假设塑造了多年的检测逻辑:扫描Web根目录找可疑脚本、盯HTTP请求里的特征参数名、用文件完整性监控抓异常变更。

有些Web shell家族之所以出名,正是因为这种简单攻击模型足够凶猛。中国菜刀(China Chopper)就是典型——MITRE(美国知名威胁情报机构)描述它通过Web服务器提供访问能力,支持HTTP POST命令执行、文件操作和终端访问。

但这次捕获的样本在三个维度上颠覆了传统模型:

Web shell无文件化投递:Web shell能力依然存在,但不再锚定在磁盘上的静态脚本文件。植入器在内存映射阶段拦截特定PHP文件的加载,把Web shell拼接到内存视图的前面。

进程级沦陷,不止应用层:植入器重定向Apache worker进程(Apache处理请求的工作单元)里libc和PHP模块库的特定函数调用,所以这个进程里跑的每一个PHP组件——插件、扫描器、本地脚本——都活在被人动了手脚的运行时里。

双通道访问:攻击者既能走HTTP驱动的PHP载荷,又能通过本地UNIX套接字(一种进程间通信的本地接口)后门拿到交互式shell,全程不需要开TCP监听端口。

这意味着攻击者的访问原语(最底层的访问能力)用起来像个Web shell,但基于文件或纯PHP的检测手段很难逮到它。在一台沦陷的主机上,任何通过被篡改的PHP运行时执行的程序,看到的目标PHP文件内容都可能和磁盘上的真实文件不一样——这直接动摇了传统文件检查工具的基本假设。

样本SHA256哈希值(文件唯一指纹):26bd5b0722d1dbab5db749a063c49bc8638653ac2addfead7a9cb3d6d57bccc9

虽然我们暂时没有足够证据把这款恶意软件归因到具体威胁组织,但它的定向选择和实现细节透露出相当高的操作成熟度。值得一提的是,ESET(另一家欧洲知名安全公司)在同期独立分析中把这款恶意软件命名为PoisonedRefresh(毒化刷新),两家观察到的行为高度重叠。


二、Web shell的总体设计

从概念层面看,这个样本把三件事捏在一起:

抢在main之前运行:植入器通过拦截libc启动序列中的__libc_start_main函数,在宿主页面的正常逻辑执行前获得控制权。

等PHP现身再动手:它钩住Apache可移植运行时(APR,Apache跨平台运行库)里的apr_dso_load函数(动态加载共享库的接口),等Apache加载完PHP模块库才改变进程行为。

提供多条访问路径:通过拦截PHP模块库里的内存映射调用(mmap),把Web shell注入到特定PHP文件的内存表示里;同时开一个本地UNIX域套接字,认证通过后把标准输入输出错误流重定向到/bin/bash,无TCP端口也能拿交互shell。

这三招单独拎出来都不算稀奇,真正高明的是它们组合在一起,专门针对Apache/PHP部署形成一套稳定隐蔽的访问机制。


三、技术剖析

二进制文件被剥掉符号表(stripped,即抹掉调试信息)并采用静态链接(把所有库直接打包进可执行文件),传统的导入表分析和字符串搜索基本失效。样本还绕过常规动态链接器(ld.so,负责运行时加载共享库的程序)启动路径,自己带加载逻辑和启动钩子。

我们没有从导入表入手,而是聚焦在几个关键点:进程启动阶段的控制流转折点、运行时字符串解密例程、符号解析和钩子安装逻辑、通过/proc文件系统做进程自省、以及加密数据变成明文可执行的转换节点。这种分析方法随着Linux恶意软件从磁盘植入器转向自定义加载器、运行时代码生成、内存执行和进程操纵,变得越来越重要。

虽然这些技术仍会产生防守方可用的可观察信号,但它们常常把明显的文件系统痕迹换成需要更多上下文或更深检查才能识别的行为链。

这个样本就是典型——它不在传统位置丢常规Web shell,而是修改运行时特定PHP文件向运行进程的呈现方式。攻击者拿到的能力与传统Web shell类似,但实现抹掉了防守方依赖的大量痕迹。这凸显出纵深防御(多层防线组合)的重要性,必须同时覆盖文件系统、内存、进程和行为四个维度。

3.1 RC4字符串:跟着面包屑走

理解样本的第一道坎是它大量使用运行时字符串解密。最关键的字符串都加密存储在只读数据段(.rodata,即程序里存放常量的区域)里,需要时用一段紧凑的RC4流密码(一种对称加密算法)解密。

恶意软件用了一个硬编码的16字节RC4密钥:TrswBWIl90Z5e38n。这把运营字符串藏起来,简单的静态分析很难看穿。在分析过程中,我们识别并解密了一批加密字符串,包括函数名、库名和目标文件名:

  • /proc/self/exe/proc/self/maps,用于进程自省
  • __libc_start_main,libc启动例程
  • apr_dso_loadapr_time_now,Apache里的APR函数
  • libphp,Apache进程内的目标PHP模块
  • openclosemmap等文件和内存接口
  • pthread_createpthread_detach等线程原语

单独看每条字符串都不刺激,但拼在一起,能看出这个样本深度理解Linux进程内部机制、Apache运行时和PHP执行模型。

防守提示:在这个样本里,RC4主要充当混淆机制,把运营字符串藏起来、拖到运行时才解密。Linux服务器排查时,运行时字符串解密、动态接口解析、延迟暴露的配置数据,正在成为能力更强的一类Linux恶意软件的标配。

3.2 抢在main前面执行

在Linux上,大多数程序并不直接调用main。执行流先经过libc初始化代码准备进程环境,然后才调用main。这套流程里的关键组件是__libc_start_main,它做初始化并调用程序的入口点。

普通Linux ELF(Linux系统的可执行文件格式)二进制走的是一条老路:内核加载二进制,动态链接器解析依赖,然后libc的启动代码准备环境调用main。很多安全和可观测性工具都搭这套流程的便车来获取进程启动可见性。

我们的样本在两个地方偏离常规。第一,在原始入口点,它直接跳进一段自定义加载器例程,而不是按常规调用__libc_start_main。这段加载器不只是执行额外代码——它通过/proc/self/exe重新打开自己的映像,定位到保留的原始可执行文件起始偏移,然后把那个嵌入的ELF手动加载进内存。

图1:典型_start例程

图1:一个剥离符号的32位静态链接ELF里的典型_start(程序入口)例程。即使没有符号,入口点也有熟悉的C运行时启动行为:栈对齐、设置启动参数、调用libc启动例程。这让被感染的httpd(Apache主程序)入口点格外显眼——它没有遵循这个模式,而是把原始栈直接传给自定义加载器,例程返回就停。

图2:自定义加载器

图2:程序入口点(_start)直接把执行权交给自定义加载器例程。常规Linux可执行文件的start通常会进入_libc_start_main,最终调用main()。分析时一发现这个熟悉的启动序列缺失,就立刻摸到了植入器的自定义ELF加载和启动钩子逻辑。

加载器解析嵌入的ELF,遍历PT_LOAD段(可加载的程序段),映射新内存区域,把段内容拷到对应位置,应用合适的内存保护,必要时处理原始解释器,最后把执行流引到自己的__libc_start_main包装函数。

图3:劫持libc启动例程

图3:植入器解密并解析__libc_start_main,保留原始libc启动例程,用一个包装函数(被钩的__libc_start_main)覆盖它,让植入器代码在程序真正的主函数之前执行。

实操层面,这套”自带加载器”打法给攻击者带来几个好处:

  • 绕过常规动态链接器启动路径,降低依赖该路径作为监测点的控制手段的可见性
  • 让攻击者精确控制拦截libc启动的时机和方式
  • 提供一个干净、单一的撬入点切入植入器其余部分

从防守方角度看,这又提醒我们:把正常加载器路径当作拦截点,对更高级的Linux威胁已经不够用了。

样本明确针对__libc_start_main并替换为包装函数。流程概念上从:

_start -> __libc_start_main(main, ...) -> main()

变成:

_start -> 自定义加载器 -> 真实__libc_start_main(wrapper, ...) -> wrapper() -> 真实main()
wrapper() 跑植入器初始化
wrapper() 然后调用真实main

这给植入器一个早于宿主应用做实事之前的稳定执行窗口。无论后续程序行为如何,它都能拿到一个稳定的撬入点,初始化一次然后融入到看似合法的进程里。

取证角度看,这也把攻击指标往前推到了进程时间线的早期,常常早于日志框架完全初始化。

防守提示:进程生命周期早期的不寻常行为——比如访问/proc或修改内存权限——比后期活动更有信息量。

3.3 通过APR定向Apache

拿到早期执行权后,植入器没有立刻试图操纵进程里所有东西。它在等一个特定条件:Apache加载PHP模块。

APR提供模块动态加载接口,包括apr_dso_load(运行时加载共享对象的接口)。钩住这个函数,植入器就拿到了模块初始化的天然观察点,不用猜加载顺序,也不用硬编码。恶意软件可以等到目标环境就位再激活,不用没差别地给每个Apache进程打补丁。

我们的样本钩住apr_dso_load,检查每个被加载的模块路径。一旦检测到libphp,就修改该模块内部的行为。

图4:APR模块钩子

图4:植入器钩住APR的apr_dso_load,明确把行为门控在PHP模块上,除非加载的是libphp,否则立即返回。

同时,样本钩住apr_time_now,APR的”当前时间”函数。这个接口看似无害,但在活的Apache进程里被调用得非常频繁。我们评估,植入器把这个高频接口当作延迟触发器,用来启动负责本地UNIX套接字后门的worker线程。

这种做法体现出运营层面的成熟:

  • 植入器把作用域限制在它设计的目标环境
  • 避免过早或过广动手造成服务不稳定
  • 利用目标平台自身的运行时抽象而不是硬刚

这种选择性激活贯穿整个植入器。它不急着在进程一启动就改,而是反复等到特定运行时条件才启用新功能。

防守提示:在Apache进程里,调查libphp刚加载后不久的内存保护变更或可执行页修改。

3.4 在内存里定位libphp

PHP加载完后,植入器得知道它住在进程内存哪里。Linux通过/proc/self/maps暴露这些信息——列出所有内存映射及其地址范围和后备文件。

植入器解析这个文件,定位libphp映射的起始和结束地址。这些地址用于约束后续对目标模块的修改。它不盲目打补丁,而是先识别可执行的libphp映射,再把改动限制在那个区域内。具体步骤是:

读取 /proc/self/maps -> 找到 libphp -> mprotect 设为 RWX -> 修补重定位 -> mprotect 改回 RX

读取/proc/self/maps本身并不恶意。调试器、性能分析工具、内存管理工具都可能合法地检查进程映射。但紧接着就改内存权限、做重定位修补(relocation patching,调整函数调用地址指向新代码)、往目标共享库的可执行区域写东西,那就相当可疑了。

这套动作序列在Apache worker进程里特别反常——worker本来就没理由检查自己的内存映射,然后立刻改可执行页。

防守提示:/proc/self/maps访问后紧跟的mprotect(修改内存保护属性)活动、重定位修补或往可执行内存区域写入,都是高价值信号,特别在Apache worker进程里值得深挖。

3.5 在libphp里重定向执行流

很多防守方对通过LD_PRELOAD(一个允许在程序启动前预加载自定义库的环境变量)或修改PLT/GOT(程序链接表/全局偏移表,程序调用外部函数的跳转表)做钩子已经很熟悉。我们的样本走得更精细——结合基于重定位的钩子和直接重写libphp里的调用目标,让选定操作先绕进植入器控制的代码。

应用这些修改之前,植入器临时改变目标libphp映射的内存保护,修补选定的重定位和调用目标,然后恢复原始保护。

实现层面,样本遍历libphp相关的重定位数据,调整一小撮函数的PC相对(PC-relative,以程序计数器为基准的相对寻址)调用目标。防守方视角看,精确的重定位机制没那么重要,重要的是结果。

在PHP模块上下文里,样本悄悄重定向本应调用openclosemmap__fxstat等文件和内存接口的调用,从而控制PHP如何打开、测量大小、映射并最终执行目标脚本文件。这些钩子就是后面要说的内存Web shell投递机制的基础。这还给了攻击者另一个好处——他们不用往常规加载器路径注入新的共享库。

3.6 通过内存映射投递Web shell

在我们看来,这是样本最特别的一面。植入器拦截PHP对三个特定脚本文件的打开动作:

  • apm_css.php3
  • full_wt.php3
  • webtop_popup_css.php3

选这三个文件名,是因为它们在目标BIG-IP APM webtop环境里真实存在,不太会触发怀疑。

PHP打开其中一个文件时,植入器记录下文件描述符。文件随后被内存映射时,植入器构造一个修改过的内存视图,里面既有嵌入的Web shell,也有原始脚本内容。磁盘文件根本不需要包含最终的Web shell内容——执行跟着植入器运行时构造的修改版内存表示走。

图5:内存映射钩子

图5mmap()钩子检查映射是否属于被跟踪的PHP脚本文件描述符。如果是,调用原始的mmap(),把嵌入的PHP Web shell拼接到映射内容前面,磁盘文件原封不动。

嵌入的PHP载荷行为和经典Web shell差不多,但有几个显著特征:

  • php://input读取原始请求字节
  • 检查请求体开头的短魔数前缀BSOHAzPB
  • 用一段小型流密码解密剩余部分
  • 通过eval执行解密内容
  • 返回HTTP 201状态码并设置Content-Type: text/css; charset=utf-8,伪装成正常资源请求

载荷是模板化的,运行时才重写关键字符串——进一步增加静态特征匹配的难度。在这个样本里,请求标记BSOHAzPB和Web shell密钥wSLjN1beuR是运行时才补到PHP里的,不是直接以最终形式存的。

防守提示:Web shell检测机制必须纳入运行时行为和内存检查,不能只靠文件扫描。对这类威胁来说,磁盘上的PHP文件完全可以是良性的,内存映射里的代码才是恶意的。

3.7 通过apr_time_now延迟激活

植入器不急着在早期启动阶段开线程或跑重型例程,而是把对apr_time_now的钩子当作延迟触发器。

Apache进程开始例行做时间调用时,植入器才创建并分离出负责本地UNIX套接字后门的worker线程。这种时序把破坏服务的风险降到最低,也帮助植入器融入正常运行行为。它还为难倒那些只观察进程启动后短暂窗口的动态分析环境。

除了HTTP驱动的Web shell,植入器还在/run/bigtlog.pipe建一个本地UNIX域套接字(AF_UNIX,一种只用本地文件路径通信的套接字),而不是暴露传统TCP监听端口。

在32位Linux上,套接字操作通常通过历史上的socketcall系统调用(早期Linux把多个套接字操作打包在一个系统调用里的接口)多路复用,处理socketbindlistenaccept等操作。植入器使用这套接口,符合x86-32环境。

经过基于令牌Kzwd6jM5(用作连接密码)的简短认证检查后,植入器把标准输入、输出、错误流重定向到套接字,执行/bin/bash——这就拿到了无需TCP监听端口的交互式shell访问,单纯的网络监控更难发现它。监听器在认证成功后保持活动,用fork()(创建子进程的系统调用)创建独立shell进程,同时继续接受新连接。

有意思的是,我们找不到任何内建机制能让威胁行为人主动连接这个套接字。没有客户端套接字代码,也没有对认证令牌的额外引用,提示这两种方式(Web shell和套接字)是独立能力。也许威胁行为人能通过Web shell与套接字交互,但目前没有证据证实或否定这一点。

图6:套接字通往shell

图6:植入器把本地套接字连接直接交给/bin/bash,重定向标准流并执行shell二进制。

防守提示:服务端失陷场景下,/run下的本地进程间通信端点值得仔细检查。


四、防护与防御

Sophos把这款威胁检测为Linux/Agnt-IC

4.1 威胁狩猎

下面这些行为应当作为调查线索处理,与文件完整性、进程、内存和BIG-IP相关证据做关联分析,而不是单独用作确认依据。

Web层信号

排查项:

  • 对上面列出的.php3端点的请求,特别是你的环境里这类请求很少的话
  • PHP端点返回HTTP 201却声称自己是CSS(Content-Type: text/css; charset=utf-8
  • 对类CSS的PHP路径发起的POST请求,结构一致或请求体大小异常

主机层信号

排查项:

  • Apache worker进程读取/proc/self/maps
  • PHP模块映射的内存保护临时变更(RWX接RX),特别在libphp附近
  • /run/bigtlog.pipe下创建UNIX域套接字
  • Apache衍生进程重定向标准输入输出并执行/bin/bash

4.2 防御者指南

应急响应指南

如果怀疑这台植入器已经在场:

  • 先抓易失性证据(易失性证据指内存、网络连接、运行中进程状态等,关机就消失的东西)
  • 假设双通道访问同时存在
  • 单靠服务重启无法保证清干净

加固与检测

虽然每个环境都不一样,但有几个大方向可以减少暴露面或提升可见性:

攻击面收敛

  • 如果你的环境不需要执行.php3,经过变更控制和业务影响评估后考虑禁用。不要对BIG-IP APM系统一刀切使用这条,要先遵循F5官方指引
  • 优先采用能最大限度减少长生命周期PHP执行路径的配置
  • 限制ptrace(这能减少一些跨进程检查和注入机会,但不一定是进程内加载器和修补行为的对策):
echo 1 > /proc/sys/kernel/yama/ptrace_scope
  • 在Apache配置里加上:
<FilesMatch ".php3$">
 Require all denied
</FilesMatch>
  • 减少不必要的PHP执行功能,评估限制危险函数对业务的影响

行为监控

  • 对Apache worker的异常行为组合报警:
    • 读取/proc/self/maps
    • 改变libphp的内存保护
    • /run下绑定UNIX套接字
    • 派生/bin/bash
  • 把HTTP 201 + text/css响应(PHP端点发出且不属于正常应用行为)视为可疑
  • 规划内存感知响应:把进程内存采集、磁盘模块内容与内存模块内容对比纳入关键Web服务器的应急响应预案

五、结论

这款植入器展示了现代Linux恶意软件如何通过精密的投递机制实现那些原本熟悉的攻击者能力。虽然嵌入的PHP最终行为还是传统Web shell那一套,但外围的基础设施已经高出一个层级——自定义ELF加载、早期启动拦截、APR感知模块监控、重定位修补、纯内存载荷投递。

基于我们对相关umount样本和被感染httpd样本的关联分析,我们评估这场行动采用的是分阶段架构:安装器组件负责部署、持久化和传播;被感染的httpd组件聚焦运行时能力、进程操纵、Web shell投递和交互式访问。

最重要的发现也许是:Web shell最终形态根本不需要存在于磁盘。植入器改变目标PHP文件向运行进程的呈现方式,Apache和PHP看到的内容可以和传统文件检查工具看到的不一样。只盯着文件系统的应急响应人员可能漏掉关键证据。

这个样本属于更广泛的一类Linux威胁——它们更少依赖明显的磁盘痕迹,更多依赖运行时操纵。对这类威胁,防守方应当聚焦跨层关联。单靠文件系统检查可能暴露不出沦陷,但网络遥测、协议检查、进程监控、内存分析和完整性校验各自能照亮攻击链的不同部分。


原文出处:本文翻译自Sophos官方博客文章 Dissecting a PHP web server rootkit,作者:Andy Curtis(AndyC),原文链接:https://www.sophos.com/en-us/blog/dissecting-a-php-web-server-rootkit

样本哈希26bd5b0722d1dbab5db749a063c49bc8638653ac2addfead7a9cb3d6d57bccc9

检测名称:Sophos检测为Linux/Agnt-IC;ESET命名为PoisonedRefresh

关联CVECVE-2025-53521(BIG-IP APM无认证远程代码执行漏洞)

技术参考

  • F5官方公告:https://my.f5.com/manage/s/article/K000156741
  • ESET分析的Mastodon帖子:https://infosec.exchange/@ESETresearch/116460555146536345
  • NVD漏洞条目:https://nvd.nist.gov/vuln/detail/CVE-2025-53521
  • MITRE关于中国菜刀:https://attack.mitre.org/software/S0020/

© 版权声明
THE END
喜欢就支持一下吧
点赞7 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容