记录一次木马入侵及修复

结论

2026-11-10 11:21(北京时间),hxgeomaterial.com 再次被同一条 PbootCMS 缓存注入链路打中:攻击者在查询参数 s 里带上变形后的 PHP 短标签,页面中的 {pboot:pageurl} 把完整 URL 原样写进将被 include 的 HTML 缓存,View::cache() 里用来“剥离 PHP 标签”的正则反而把 <<??= 拼回 <?=,后续请求执行 payload,在 runtime/ 写下投放器 tempp3.php,再生成冰蝎式后门 bd459bcae6.php。

这不是后台密码泄露。当天同一波扫描也打了 www.tdxadapter.com,但该站 .htaccess 里已经有查询串拦截,请求全部返回 403。hxgeomaterial.com 在 11 月 9 日升级到 PbootCMS 3.2.32 后,根目录 .htaccess 只剩下伪静态,没有这组拦截规则,所以这次被写进去了。

升级后的 3.2.32 源码里,View::cache() 仍然使用 ! query_string('p,s'):这个函数会先丢掉 p 和 s 再判断查询串是否为空,因此“只带 p、s 的恶意请求”照样写缓存;{pboot:pageurl} 也仍未做 HTML 转义。上一篇《记一次pbootcms系统修复》里的代码层 P0,并不在这次升级后的文件里。

这次先清掉网站目录里的后门,再给两个 PbootCMS 站点统一补上 Apache 拦截,并禁止通过 HTTP 访问 runtime/、config/、data/。runtime/archive/session_ticket.php 是系统自己写的正常文件,不是木马。

告警与时间线

主机安全(腾讯云,服务器标识 usa_web / lhins-ghu278rh)连续告警:

  1. 11:21:31 检测到恶意文件 /www/wwwroot/hxgeomaterial.com/runtime/tempp3.php
  2. 11:21:41 检测到恶意文件 /www/wwwroot/hxgeomaterial.com/runtime/bd459bcae6.php
  3. 11:24:39 检测到 /root/incident-20261110-hx/bd459bcae6.php——这是隔离样本被扫描器再次命中,不是网站又被写入。取证不要把可执行 .php 样本留在磁盘上,只保留哈希和说明即可。

访问日志 /www/wwwlogs/hxgeomaterial.com-access_log 的对应关系:

  • 11:21:19 起,UA 为 pbootcms-multiroute-poc 的 GET 请求经 Cloudflare 回源,URL 形如 /?p=/aboutus/&s=<<??=file_put_contents('runtime/tempp3.php', base64_decode('...'))??>>,多数返回 200(页面正常渲染,同时缓存被种下)。
  • 11:21:40 GET /runtime/tempp3.php 返回 200、34 字节——投放器已执行:写出随机文件名后门并自删。
  • 11:21:41 再请求同一地址已是 404(文件已被主机安全或投放器自己删除)。
  • 11:21:51 GET /runtime/bd459bcae6.php 返回 200。

www.tdxadapter.com 当天同类带 file_put_contents 的查询请求全部 403。

入口与原因

和《记一次pbootcms系统修复》是同一条链,只是这次落点改成了 runtime/tempp3.php:

  1. 模板输出 {pboot:pageurl} 时调用 get_current_url(),不转义,完整查询串进入即将缓存的 HTML(常见位置是 og:url)。
  2. s 参数并不被业务代码读取,query_string('p,s') 先剔除 p、s 再判断,结果为空,于是 View::cache() 仍然写缓存。
  3. 写缓存前的剥离正则会删除 <?、<?=、?> 等,<<??= 删掉中间的 <? 后变成 <?=,??>> 删掉 ?> 后变成 ?>。这段内容被写进 runtime/cache/<md5>.html,内核随后 include 该文件,payload 执行。

站点当时跑在 Apache(80/443 由 httpd 监听,.htaccess 生效)。hxgeomaterial.com 的 .htaccess 升级后只剩伪静态,以及拒绝访问 data|static/backup 的规则,没有拦截 {pboot:if}、PHP 短标签和 file_put_contents 等危险函数。已经落到磁盘上的 runtime/*.php 会由 Apache 直接执行,不会走到伪静态。

清理

  • runtime/tempp3.php 检查时已被主机安全删除。
  • runtime/bd459bcae6.php(680 字节,属主 www,mtime 11:21:40)是冰蝎风格的加密通信后门,已从网站目录移除。
  • 全站扫描未发现其它同类随机名 PHP、历史上的 ea7987b3 目录,或上传目录里的 PHP。
  • 不要把后门原文拷到 /root 下留作“取证”,主机安全会再次告警。

这次具体改了什么

两个 PbootCMS 站点都改了,路径分别是:

  • /www/wwwroot/hxgeomaterial.com
  • /www/wwwroot/www.tdxadapter.com

原 .htaccess 备份为 .htaccess.bak-sec-20261110112658。

1. 站点根目录 .htaccess

在伪静态 catch-all 之前增加两类规则:敏感目录直接拒绝;查询串里出现模板注入或危险函数时返回 403。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<IfModule mod_rewrite.c>
Options +FollowSymlinks
RewriteEngine On

RewriteRule ^(data|static/backup|runtime|config)/ - [F,L]

RewriteCond %{QUERY_STRING} (?i)pboot\s*:\s*if [OR]
RewriteCond %{QUERY_STRING} (?i)(<\?=|<\?php|%3c\?|<<\?=) [OR]
RewriteCond %{QUERY_STRING} (?i)(file_put_contents|file_get_contents|fwrite|fputs|eval\(|assert\(|system\(|passthru|shell_exec|proc_open|popen|base64_decode|phpinfo) [OR]
RewriteCond %{QUERY_STRING} (?i)mksports66
RewriteRule .* - [F,L]

RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php?p=$1 [QSA,PT,L]
</IfModule>

<FilesMatch "(?i)^(reodme|Akl|tempp3)\.php$">
Require all denied
</FilesMatch>

2. runtime/.htaccess

即使有人再次在 runtime/ 写下 PHP,也不能通过网页执行。应用内部 include 编译模板不受影响。

1
Require all denied

验证

在服务器本机用 HTTPS 检查(用无害特征即可,不必重放攻击):

  • 两个站点首页返回 200
  • /?s=file_put_contents 返回 403
  • /runtime/、/runtime/archive/session_ticket.php 返回 403

还没做完的代码层修复

Web 层拦截只能挡住已知特征。升级 CMS 后要重新核对上一篇里的 P0 是否还在,目前这两处仍然是升级前的逻辑,需要再补一次:

  1. core/view/View.php 的 cache():把条件改成存在任意查询串就不写 HTML 缓存,例如 empty($_SERVER['QUERY_STRING']);剥离标签之后如果还能匹配到 <? / <?= / <%,放弃写缓存。
  2. apps/home/controller/ParserController.php:{pboot:pageurl} 改为 htmlspecialchars(get_current_url(), ENT_QUOTES, 'UTF-8') 后再替换。

topchinafactory.com、www.hneco.com 不是 PbootCMS,日志里没有这波 UA。主机安全控制台里这几条可以按已处理关闭。