湛蓝
  • 首页
  • 博文 · 章
  • 漫游影视
  • 笔记 · 栈
首页 / 博文 · 章 / 一个 Typecho 站被入侵之后

一个 Typecho 站被入侵之后

博文 · 章 • 2026-11-01 • 约 13 分钟读完 • 4,823 字
本文目录
  • 一个 500,和两个目录
  • 时间戳比日志好用
  • 六种藏法
  • 最没技术含量的那种
  • 空格和 Tab
  • 把字符串拆成单字
  • 十六进制转义
  • 躲在不该有文件的地方
  • 会看你的 php.ini
  • 十万行代码,只为了藏一行
  • 重装解决不了这个问题
  • 密码是同一个
  • 清理时守住三条
  • 怎么确认真的干净了
  • 我没法保证的部分
  • 如果你也碰上
  • 附录

上周有人找我,说一个跑了四年的 Typecho 博客打不开了,怀疑被黑,顺手把主机控制台的截图发过来,上面带着 FTP 账号。

我先打开看了一眼。浏览器给了个 500。

查完之后,两件事都成立,但它们之间没有因果关系:站点确实被人进去了,而「打不开」是另一个毛病。把这两件事捆在一起查,是我见过最常见的第一个坑。很多人一上来就清后门,清完发现站还是打不开,于是怀疑自己没清干净,越查越乱。

一个 500,和两个目录

这台机器是 DirectAdmin 加 suPHP,前面挂 nginx。HTTP 请求得到的是这个:

Directory "/home/USER/domains/example.com/public_html" is writeable by group
suPHP 0.7.2

第一次看到这行,很容易以为是入侵的痕迹。其实正好相反,这是 suPHP 在保护你。

它用用户身份跑 PHP。站点目录如果是组可写或全局可写(775、777),同一个组里任何进程都能往里丢一个 PHP 文件,然后被当成正常代码执行出去。等于把远程代码执行的口子敞着。所以它宁可让站点不可用。

drwxrwxrwx   public_html      ← 500 的来历
drwxr-xr-x   public_html      ← 改成 755,站点立刻活过来

HTTPS 是另一回事。DirectAdmin 给 SSL 单独分了个 private_html 目录,我登进去看,里面只有一个主机商留下的占位页,内容是「欢迎来到 example.com,把你的网站传到 public_html」。

于是出现一个挺迷惑的现象:同一个域名,http 报 500,https 反而 200。原因很朴素,两个协议读的根本不是同一个目录。

那个 200 其实也不健康。证书是自签的,CN 写着 localhost,有效期到 2045 年,任何浏览器打开都会先警告一句。

时间戳比日志好用

权限五分钟就修好了,活儿在另一头:怎么证明它「脏」。

访问日志拿不到,能动的只有 FTP。我把可疑文件按修改时间排了个序,攻击者的作业节奏一下就出来了。

时间 动作 落地的东西
08-07 23:34 第一个后门 27 KB 的文件管理器
08-10 03:43 黑帽 SEO 主控 161 KB,注释里塞满垃圾中文
08-10 03:56 批量丢加密马 4 个文件,外加一个 Google 站点验证文件
08-10 06:02 污染 robots.txt 指向 5 个随机命名的 sitemap
08-10 06:08 丢进一个大家伙 1.53 MB,102,060 行
08-19 02:00 试图提权 Chankro 的执行残留
09-03 17:36 改首页 同一张篡改页,递归丢了 303 份

同一天凌晨三点到六点,七个文件集中落地。人睡觉的时候不会有人上传七个文件,正常运维也不长这样。这是我抓住的第一个锚点,后面所有判断都是从这条线长出来的。

六种藏法

这批样本基本把中文黑产圈常用的混淆手法凑齐了。挨个记一下,以后在别人服务器上看到能认出来。

最没技术含量的那种

<?php eval(gzinflate(base64_decode('...')));

eval 套 gzinflate 套 base64,一眼就是马。这类属于铺量用的,成本低,死得也快。

空格和 Tab

<?php
$dispatcher = "                                   ";

整段载荷由空格和制表符拼成,编辑器里看着是几行空白。要看见它,得先把「显示不可见字符」打开。

我当时不是靠看见的,是靠文件大小不对劲——5 KB 的文件,打开是空的,这不合理。

把字符串拆成单字

$conf = array(
    'site' => 'h'.'tt'.'p://h'.'h.g'.'otz'.'dua'.'n.c'.'om',
    'ci'   => 'cur'.'l_init',
);

域名和函数名都被切成单个字符再拼回去,专门躲基于明文的规则匹配。这种写法出现在业务代码里没有任何理由,看到就可以直接判死。

十六进制转义

@ini_set("\155\141\x78\x5f\x65\x78\x65\x63\x75\x74\x69\157\x6e\137\x74\151\155\145", 0);

这行其实是 max_execution_time = 0,也就是不限执行时间。同一份文件里还有 \x77\142,那是 “wb”,用二进制写模式打开文件。

躲在不该有文件的地方

install 目录下多了一个 next.php。同目录里其他文件的时间戳都是重装那天,只有它停在入侵那天。这一处不一致,就是整条线索的入口。

会看你的 php.ini

function getWorkingFunction($type) {
    $disabled = explode(',', @ini_get('disable_functions'));
    foreach ($func_alternatives[$type] as $func) {
        if (function_exists($func) && !in_array($func, $disabled)) return $func;
    }
    return false;
}

它先读 disable_functions,再从备选函数里挑一个没被禁掉的。你在 php.ini 里禁了 system 基本没用,它会把 exec、shell_exec、passthru 挨个试一遍。想靠 disable_functions 挡这类东西,不太够。

十万行代码,只为了藏一行

这批文件里最花心思的是这个:1.53 MB,102,060 行。

开头是一段人畜无害的 HTML 演示页,讲什么十六进制解析。往下拉是大片空白。真正的代码在第 102059 行:

eval("?>".$decoded);

上面十万行只干一件事,把 Base64 载荷每四个字符切一段,用 .= 一段段拼接。

$z .= "UEQ5";
$z .= "d2FI";
$z .= "QWda";
...
$a = "base"; $b = "64_decode"; $c = $a.$b;
$decoded = $c($z);
$decoded = $c($decoded);

为什么是四个字符?因为所有基于「连续长 Base64 串」的检测规则,这时候都失效了。十万行代码的唯一目的,是让一行代码不被看见。

我琢磨了一会儿这个成本收益的算法,觉得挺聪明的,也挺讨厌的。

解到第四层,底下的东西是个功能完整的文件管理器。有三个细节值得说。

写文件写了三条路:先 file_put_contents,失败换 fopen 加 fwrite,再失败就 tempnam 加 rename。三级降级,基本上没有写不进去的情况。

取路径的时候读 $_REQUEST[‘p’],没有就读 cookie 里的 last_path,能一路走到站点根目录外面去。

全文插了 36 处 goto,控制流拆得七零八落。静态分析工具读到这种代码,只能放弃。

重装解决不了这个问题

这条是我这次最想写下来的。

服务器上有个 install 目录,里面大部分文件的时间戳是 10 月 20 日。也就是说十天前有人重装过一次 Typecho,大概是想靠重装把站救回来。

但 next.php 的时间戳停在 8 月 10 日。

原因很朴素。重装是把压缩包解压覆盖,它只替换同名文件。next.php 不在官方包里,所以一根汗毛都没掉。根目录那六个后门同理,官方包里压根没有这些文件名。

覆盖式重装能修好被改坏的文件,清不掉被新增的文件。

所以判断一个站干不干净,看首页正不正常没有意义,得看有没有官方清单之外的东西。要做这件事只有两条路:把程序目录整个删掉再解压,或者拿一份可信的文件清单去比对。

密码是同一个

查到根上,入侵路径朴素得有点让人无语。WordPress 的 wp-config.php 里,数据库密码是这么写的:

define( 'DB_PASSWORD', '********' );

这个字符串同时是数据库密码、FTP 密码和控制台密码。

攻击者不需要什么高级手段。找一个还停在 2024 年 3 月版本、装着几个过期主题的 WordPress,攻破它,就等于拿到了整台主机,横向移动的成本是零。

另外还有三个东西把后果放大了。21 端口是明文 FTP,密码在公网上能被直接嗅探;站点目录是 777;生产环境的 __TYPECHO_DEBUG__ 还是 true,报错会把路径和 SQL 直接吐给访问者。

清理时守住三条

动手之前我给自己定了三条规矩,事后觉得值得写下来。

删之前先备份。每个待删文件先抓到本地,算 SHA256 建台账。误删、复盘、提交给主机商,都靠这个。这次一共十三个文件,十三个哈希。

分批做,每批不超过十个,删完立刻重新列目录确认。别信「命令没报错就等于成功」这句话,这一步救过我。

改配置类的东西先试点。改权限之前先在单个文件上试 SITE CHMOD,确认返回码和权限位都变了,再批量推。

还原文件的时候有个细节:优先用服务器上已经有的官方安装包做对照。我这次就是拿站里那个 wordpress-6.4.3-zh_CN.zip 还原十七个核心文件,版本对得上,省了很多事。从网上下一个不确定版本的包,很容易把一个能跑的站搞成跑不动的。

怎么确认真的干净了

首页 200 说明不了什么。我最后过了一遍这张表。

检查项 通过标准
首页 200,标题正确,正文里没有 Fatal error 或 Warning
后台登录页 200,能登进去
主题静态资源 CSS、JS 都是 200
每一个后门路径 全部 404
黑帽 sitemap 404
页面正文 不含后门域名,不含篡改关键词
目录权限 没有组可写或其他可写的目录

第四行最容易被跳过。每个后门路径都得单独请求一次,拿到 404 才算数。文件删掉还不够,要确认 Web 服务器那边也确实访问不到了。

我没法保证的部分

这次做的是清创,不是根除,这点得说清楚。

现场留下了 Chankro 的执行痕迹。那是个用 putenv 加 LD_PRELOAD 绕过 disable_functions 的工具。从输出看攻击者试了一次,没成功(报的是 socket 文件找不到、preload 被忽略)。但虚拟主机拿不到系统层,我没法验证它有没有换成别的手段留下东西。

所以能确认的只有两件事:已知的后门清掉了,已知被改的文件还原了。至于有没有我不知道的,不知道。

站点如果重要,清创之后还是应该重建。留配置文件和上传目录,其余删干净重装,数据库单独扫一遍恶意内容。麻烦,但这是唯一能给出确定答案的做法。

如果你也碰上

顺序大概是这样。

先分开看。打不开多半是配置,被黑要单独查,两件事同时存在很正常。

然后翻时间戳,找异常聚集的时间窗。人有作息,机器没有。

接着把实际文件和官方文件清单比一遍,多出来的就是嫌疑。

清理之前备份,清理之后逐条验证。

最后改密码。所有系统、所有账号,互不相同,数据库密码尤其不能等于登录密码。

顺手能做的还有几件:关掉明文 FTP;目录给 755、文件给 644,看到 777 就当故障处理;生产环境关 Debug;install.php 用完就删;公网上的 Web 文件管理器能不用就不用,那东西等于把后门开在明处。

附录

被硬编码在劫持后门里的四个域名,原文是拆成单字符拼出来的:

hxxp://hh.gotzduan.com          # 内容源
hxxps://dd.zgqcyn.com/jj.js     # 注入脚本
hxxp://aa.gossopp.com/u.php     # 上报回连
hxxp://kgr909.com/a.php         # 备用回连

文件名特征:next.php、hiroshi.php、133.php、indosec.php、.wp-WebShellAnehin.php、chankro_out.txt。

字符串特征:xNightR00t v2.0.0、IndoSec、Blackhat Error Sistem Team。


前后大概花了四十分钟。时间不在删文件上,在判断哪些文件本来就不该在那里。

这件事没有捷径,靠得住的东西只有两样:一份可信的官方文件清单,和对时间戳的敏感。反过来说,如果连自己服务器上有哪些文件都不清楚,那被入侵之后,也就没法知道自己被改了什么。

赞 (0)
上一篇 奥本海默
您可能感兴趣的
世界,您好!
欢迎使用 WordPress。这是您的第一篇文章。编辑或删除它,然后开始写作吧! 下载
2025-02-21
© 2026 湛蓝⋅苏ICP备2025162006号-1
  • 首页
  • 博文 · 章
  • 漫游影视
  • 笔记 · 栈
搜索 (0)
    加载更多...
    最新推荐
    • 一个 Typecho 站被入侵之后
    • 奥本海默
    • 直肠癌诊疗进展与循证医学实践
    • 千与千寻
    • 世界,您好!
    推荐标签