漏洞无差别地存在于每一个没打补丁的站点上。这篇记录一次完整的应急过程:看到通告、确认暴露、动手修补、验证关闭、排查痕迹——给同样在用 Typecho 的朋友一个可抄的作业。
一、起因:一份通告
凌晨在社区看到一篇漏洞通告:Typecho ≥ 1.2.0 全版本无条件 RCE。影响范围覆盖 1.2.0 / 1.2.1 / 1.3.0 及其间所有 rc/beta——从 2022 年 4 月至今官方未发修复版,最新版照样中招。
本站跑的正是 1.3.0,在打击范围内。没有任何可观望的余地,直接进入应急。
二、漏洞到底坏在哪
漏洞涉及 Typecho 的异步服务组件(/action/service 接口),由两个缺陷叠加而成。
成因一:令牌校验被"类型"绕过
if (sha1($secret . '&' . $i) == $token) {
return true;
}sha1() 永远返回 40 位非空字符串,转成布尔就是 true。PHP 的双等号是松散比较,拿非空字符串和布尔 true 比,结果恒为真。于是攻击者在 JSON 请求体里把 token 字段传成布尔 true,密钥是啥根本不用知道——校验形同虚设。
成因二:方法名不设防,回调表可被覆写
过了校验之后,异步处理逻辑会遍历请求里的 requests 数组,拿数组里的方法名直接调插件对象:
foreach ($requests as $request) {
[$method, $params] = $request;
$plugin->call($method, ... $params);
}方法名完全可控。而 Plugin 类有一个 public static 的 init 方法,传个数组进去就能把整个插件回调表(self::$plugin)换成攻击者的内容;真正干活的魔术方法 __call 会按回调表逐个 call_user_func_array——表里写什么函数名就执行什么。两步一叠:覆写回调表 + 任意方法调用 = 任意 PHP 代码执行。
入口门槛低到什么程度:/action/service 的 do=async / do=ping 两个路由不挂任何鉴权,匿名 POST 即达,唯一的要求是带上目标站点的同源 Referer 头。单个 HTTP 请求,没有加密、没有验证码。
三、确认暴露:用无害探针,别拿真刀真枪
看到通告后第一件事不是直接修补,而是确认站点真的受影响。这里有个原则:验证漏洞用无害探针,不用完整利用链。
构造一个 JSON 请求——token 传布尔 true,requests 传空数组(空数组意味着即使鉴权被绕过也不会执行任何回调,零副作用):
curl -X POST "https://你的域名/action/service?do=async" \
-H "Content-Type: application/json" \
-H "Referer: https://你的域名/" \
--data '{"token": true, "requests": []}'结果:do=async 和 do=ping 均返回 HTTP 200——鉴权确实被绕过了,站点暴露实锤。
四、修补:官方通告的最小补丁
按通告给的最小补丁方案,一共动两个文件三处。原则是改动最小化,只堵绕过路径,不动其它逻辑,避免影响正常的异步服务调用。
补丁一:令牌校验改恒时强比较(var/Typecho/Common.php)
// 修改前
if (sha1($secret . '&' . $i) == $token) {
// 修改后
if (hash_equals(sha1($secret . '&' . $i), (string) $token)) {hash_equals 既是强类型比较(true 转字符串后长度不匹配,直接 false),又自带恒时特性,顺手防掉时序侧信道。
补丁二 / 三:异步入口加类型约束(var/Widget/Service.php 的两个令牌校验入口)
// 修改前
if (!Common::timeTokenValidate($token, $this->options->secret, 3)) {
// 修改后
if (!is_string($token) || !Common::timeTokenValidate($token, $this->options->secret, 3)) {sendPingHandle(do=ping)和 asyncHandle(do=async)两处同样处理。即便第一道补丁已足够堵住绕过,第二道 is_string 约束也把数组、布尔等奇怪类型挡在门外——纵深防御,便宜且有效。
五、验证关闭:同样的探针,预期 403
打完补丁,用和确认暴露时一模一样的探针回放,再加两个变体(布尔 true、数字 1):
| 探针 | 补丁前 | 补丁后 |
|---|---|---|
token: true + 空 requests(do=async) | 200 | 403 |
token: true + 正常字段(do=ping) | 200 | 403 |
token: 1(数字变体) | — | 403 |
首页、文章页、友链页全部正常 200——补丁生效,业务无感。
六、痕迹排查:补完洞,回头看看有没有人已经进来过
这种级别的洞暴露在外,最坏的假设是:已经被打过了。补完洞必须做一轮入侵痕迹排查,我的清单:
- 上传目录里有没有 PHP——
uploads下只应有图片/附件,出现.php/.phtml/.phar一律可疑; - 站点根目录的 PHP 文件清单——对照正常应有列表(
index.php/config.inc.php等),多出来的文件逐个审; - 近期被修改过的核心 PHP——
find . -name "*.php" -mtime -30,改动应与自己的维护记录一一对应; - 特征文件名扫描——
*shell*/*wso*/*cmd*/*.php.bak/phpinfo*之类。
本次排查结果:干净。上传目录零 PHP、无特征文件、近期核心改动只有补丁的两个文件。算是松了口气。
七、给同样在用 Typecho 的你
- 别等官方。通告发布时最新版仍受影响,自己动手打补丁是唯一选项。两个文件三处改动,十分钟的事;
- 升级会覆盖补丁。这是核心文件补丁,将来升 Typecho 版本会被冲掉——升级后记得检查这两处是否已内置修复,没有就重打;
- 验证用无害探针。确认暴露时
requests传空数组,不要对任何站点跑真实利用链——那是别人的服务器; - 补完洞做痕迹排查。堵住的是以后,排查的是以前,两件事都要做;
- 顺手的加固:删掉建站后不再需要的
install.php;后台密码换强的;不再用的访问令牌及时撤销。
漏洞每天都有新的,区别只在于你是那个"看了通告当晚就修完"的人,还是那个"被打穿了才知道"的人。
—— 完 ——