WordPress 安全加固:木马清理、防护插件与 wp-json
WordPress 被挂马很少是因为核心有漏洞,八成是从插件、主题、弱口令这三个口子进来的。发现的时候通常已经晚了:首页被人加了隐藏链接,搜索结果点进去跳到别的站,服务器 CPU 长期 100%,或者后台多了一个不认识的管理员账号。
先确认是不是真被挂马
别急着删文件,先取证。整站打包一份放到本地,包括数据库导出,后面比对和追查都要用。然后在服务器上跑一次关键词扫描,这一步能捞出大部分一句话木马。
grep -rn --include=*.php -E "eval(|base64_decode(|gzinflate(|str_rot13(|assert("
/www/wwwroot/example.com/wp-content/ | head -50
正常的 WordPress 代码里,这几个函数极少出现在 wp-content 目录。命中之后不要直接删,先看文件路径和修改时间,ls -l --time-style=full-iso 能看出来它是哪天被写进去的,那个时间点附近的访问日志就是入口线索。
另一种情况是代码干净但页面被注入。检查主题的 header.php 和 footer.php 有没有陌生的 <script>,以及数据库 wp_options 表里 wp_head 这类钩子有没有被塞进外链。用 SELECT option_name, LEFT(option_value, 200) FROM wp_options WHERE option_value LIKE '%<script%'; 能快速筛一遍。
木马一般藏在这四个地方
| 位置 | 典型形态 | 特征 | 处理 |
|---|---|---|---|
wp-content/uploads/ |
伪装成图片的 .php,如 2024/05/logo.php |
目录里出现 php 文件 | 整目录排查,禁止 uploads 执行 PHP |
主题 functions.php |
文件末尾一段被混淆的代码 | 打开是一片 x64x65... |
用官方原版覆盖,别手工删 |
wp-includes/ 核心文件 |
多出 wp-tmp.php、class-wp-cache.php 之类的假文件 |
文件名像核心但不认识 | 下载同版本官方包,整目录覆盖 |
.htaccess / wp-config.php |
插入 auto_prepend_file 或一行 require |
首行被改写 | 手工对比,恢复干净版本 |
uploads 目录最容易被忽略,因为大家默认那里只放图片。攻击者上传一个 shell.php 再改成 shell.jpg.php,只要 Nginx 把 .php 交给 PHP 处理,就能直接执行。这条要在 Nginx 层面堵死,具体配置后面讲。
清理顺序:断入口、换文件、再观察
顺序错了会反复中招。有人删完木马第二天又被挂,原因就是入口没断,密码没换。
- 先备份现状,包括日志。清理动作会破坏证据。
- 断入口:后台把所有管理员密码重置一遍,把不认识的账号降级或删除;数据库密码、FTP 密码、面板密码全部改掉;如果装了
xmlrpc.php相关的爆破入口,直接关掉。 - 覆盖核心:下载同版本官方安装包,把
wp-admin/和wp-includes/整个目录替换掉。这两个目录里的文件不应该有任何自定义修改,覆盖是安全的。 - 重装主题和插件:不要试图从被感染的文件里”清掉那一段”,直接删目录重新安装。主题目录里如果有自己改过的代码,先比对再决定。
- 清 uploads 里的可执行文件:
find /www/wwwroot/example.com/wp-content/uploads -name "*.php" -type f,正常情况下应该一条都没有。 - 检查
.htaccess、wp-config.php、index.php的首尾几行,以及wp_options里的可疑 option。 - 把核心、主题、插件全部更新到当前版本。停更两年以上的插件直接卸载,这类代码是扫描器最爱找的目标。
- 换密钥(下面单独讲)。
- 全站再扫一遍,确认 grep 无命中,然后观察三到七天访问日志,看有没有回连或者再次写入。
换密钥:让所有登录状态立即失效
wp-config.php 里有八个密钥常量,从 AUTH_KEY 到 NONCE_SALT。它们参与登录 cookie 的签名,换掉之后所有已登录会话立刻失效——包括攻击者留在后台的那个会话。这是清理流程里最容易被跳过、也最有效的一步。
define('AUTH_KEY', 'put your unique phrase here');
define('SECURE_AUTH_KEY', 'put your unique phrase here');
define('LOGGED_IN_KEY', 'put your unique phrase here');
define('NONCE_KEY', 'put your unique phrase here');
define('AUTH_SALT', 'put your unique phrase here');
define('SECURE_AUTH_SALT', 'put your unique phrase here');
define('LOGGED_IN_SALT', 'put your unique phrase here');
define('NONCE_SALT', 'put your unique phrase here');
八个值不要自己随手敲,用 WordPress 官方的密钥生成接口,或者本地生成:openssl rand -base64 48 跑八次。不要把同一串值复制到八个常量里,盐值重复会削弱签名强度。换完所有人(包括你自己)都要重新登录,这是正常现象。
顺便把数据库用户也换掉。很多人一直用 root 连数据库,这在被入侵后等于把整个实例交出去。新建一个只对当前库有增删改查权限的用户,写进 wp-config.php 的 DB_USER 和 DB_PASSWORD。
wp-json 暴露的用户名,是撞库的起点
WordPress 默认开放 REST API,访问 /wp-json/wp/v2/users 会返回所有发过文章的用户,包含 id、name、slug。攻击者拿这个列表去撞密码,比盲猜效率高得多。这不是漏洞,是设计如此,但对外网开放的站点没必要留着。
处理方式有两种,按站点情况选。装了 WooCommerce、用了区块编辑器、有 APP 对接的站,不能整体禁用 REST API,只关掉 users 这一个端点:
add_filter('rest_endpoints', function ($endpoints) {
if (isset($endpoints['/wp/v2/users'])) {
unset($endpoints['/wp/v2/users']);
}
if (isset($endpoints['/wp/v2/users/(?P<id>[d]+)'])) {
unset($endpoints['/wp/v2/users/(?P<id>[d]+)']);
}
return $endpoints;
});
纯展示站、没有前后端分离的,可以在 Nginx 层直接挡:
location ~* ^/wp-json/wp/v2/users {
return 403;
}
两种方式都要注意一点:返回 403 而不是 404,攻击者仍然能判断端点存在,但至少拿不到用户列表。想更彻底就返回 404。改完用浏览器直接访问验证一次,别只看代码。相关思路在安全插件里也有对应开关,插件和 Nginx 二选一,别叠加。
后台地址和文件权限
后台是爆破的主要目标。/wp-login.php 和 /wp-admin/ 这两个地址全网都知道,改路径比加验证码有效。改路径用插件把登录地址换成自定义 slug,同时保留一个 404 兜底;或者更简单,在 Nginx 上给登录页加一层 Basic Auth,攻击者连表单都看不到。
location = /wp-login.php {
auth_basic "Restricted";
auth_basic_user_file /www/server/pass/wp-login.pass;
include fastcgi_params;
fastcgi_pass unix:/tmp/php-cgi-74.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
文件权限按下面的标准设,属主统一是运行 PHP 的用户(宝塔下是 www):
防护插件用不用,取决于你有没有人盯着日志。Wordfence、Sucuri 这类插件能拦暴力破解、比对核心文件完整性、扫描已知恶意代码,缺点是占资源、误报不少,装上之后要花时间调规则。小站装一个当作提醒,大站更应该把规则写在 Nginx 和 WAF 上,性能开销小,拦截也更靠前。别同时装两个安全插件,它们会争抢同一批钩子,扫描结果还会互相打架。
| 对象 | 权限 | 说明 |
|---|---|---|
| 目录 | 755 | 不要给 777,任何进程都能写入 |
| 普通文件 | 644 | PHP 文件不需要执行位 |
wp-config.php |
600 或 640 | 只有属主可读,里面是数据库口令 |
wp-content/uploads |
755 | 需要可写,但禁止执行 PHP |
禁止 uploads 执行 PHP 的规则写在 Nginx 的 server 块里,这条比任何插件都硬:
location ~* ^/wp-content/uploads/.*.(php|phtml|php5|php7)$ {
deny all;
}
备份是最后一道防线
前面所有手段都可能失效,只有一份能用的备份是确定的。做法是数据库每天一次、文件每周一次,保留至少三十天,并且存到站外——被入侵的服务器上的备份,攻击者一样能删。备份文件不要放在 /wp-content/backup/ 这种可猜到的路径,用对象存储或者另一台机器。
更重要的是演练。备份有没有用,只有还原过一次才知道。很多人第一次还原是在真出事的时候,结果发现备份是空的、或者少了一张表。建议每季度在测试环境还原一次,跑一遍首页、后台、搜索,确认可用。备份策略和还原步骤见备份恢复,被黑之后的完整处置流程在被黑修复,边界防护的配置思路在防火墙,整体加固清单在安全加固。