WebShell 查杀与入侵检测实战:Linux 应急响应指南

适用场景

WebShell 是攻击者入侵 Web 服务器后最常部署的后门程序。它通常以 PHP/JSP/ASP/ASPX/Python 等脚本文件形式存在,攻击者通过浏览器或命令行工具远程执行系统命令。本文适用于Linux 服务器安全运维人员,在疑似入侵后的应急响应阶段进行快速排查与清除。

前置条件

  • Linux 服务器(CentOS 7+ / Ubuntu 20.04+)
  • Python 3.6+
  • 具备 root 或 sudo 权限
  • 基本的 Linux 命令操作能力

原理说明

WebShell 按其功能复杂度可分为三类:小马(单文件、单一功能,如文件上传、命令执行)、大马(多功能集成、带有登录认证和隐蔽功能)、一句话木马(极简 payload,配合客户端工具使用)。其共同特征是:文件内容中包含危险函数调用(如 eval()system()shell_exec())、编码混淆(Base64/Hex 编码)、文件时间异常、近期修改等。

操作步骤

步骤一:手动排查常用方法

应急响应开始时,先执行以下快速排查命令:

# 1. 查找最近 7 天内修改的 PHP 文件
find /var/www -name "*.php" -mtime -7 -type f | head -50

# 2. 查找包含 eval 或 system 等危险函数的 PHP 文件
grep -rn "eval(" /var/www --include="*.php" 2>/dev/null | head -20
grep -rn "system(" /var/www --include="*.php" 2>/dev/null | head -20
grep -rn "shell_exec(" /var/www --include="*.php" 2>/dev/null | head -20
grep -rn "exec(" /var/www --include="*.php" 2>/dev/null | head -20
grep -rn "popen(" /var/www --include="*.php" 2>/dev/null | head -20
grep -rn "assert(" /var/www --include="*.php" 2>/dev/null | head -20
grep -rn "base64_decode(" /var/www --include="*.php" 2>/dev/null | head -20

# 3. 查找非标准扩展名的隐藏文件
find /var/www -name "*.php5" -o -name "*.phtml" -o \
    -name "*.php7" -o -name "*.shtml" 2>/dev/null

# 4. 查找 SUID 异常文件
find / -perm -4000 -type f 2>/dev/null

# 5. 查看可疑进程
ps aux | grep -E "(bash|sh|perl|python)" | grep -v grep

步骤二:Python WebShell 查杀脚本

以下 Python 脚本实现自动化 WebShell 扫描,基于静态特征匹配 + 熵值检测 + 文件时间分析三重判定:

#!/usr/bin/env python3
"""
WebShell 查杀脚本 v1.0
用法: python3 webshell_scanner.py /var/www
"""
import os, sys, re, math, collections

# 高危函数特征
SUSPICIOUS_FUNCTIONS = {
    'php': [
        r'\beval\s*\(', r'\bsystem\s*\(', r'\bshell_exec\s*\(',
        r'\bexec\s*\(', r'\bpopen\s*\(', r'\bpassthru\s*\(',
        r'\bassert\s*\(', r'\bcreate_function\s*\(',
        r'\bbase64_decode\s*\(', r'\bgzinflate\s*\(',
        r'\beval\(\s*\$(GET|POST|REQUEST|SERVER)',
    ],
    'jsp': [
        r'Runtime\.getRuntime\(\)', r'ProcessBuilder',
    ],
    'asp': [
        r'Eval\s*\(', r'Execute\s*\(', r'WScript\.Shell',
    ],
    'py': [
        r'\bexec\s*\(', r'\beval\s*\(', r'\b__import__\s*\(',
        r'os\.system', r'subprocess\.',
    ],
}

def file_entropy(data):
    """计算文件信息熵,高熵值可能表示混淆/加密代码"""
    if not data:
        return 0
    counter = collections.Counter(data)
    total = len(data)
    entropy = -sum((count/total) * math.log2(count/total)
                   for count in counter.values())
    return entropy

def scan_file(filepath, ext):
    """扫描单个文件"""
    try:
        with open(filepath, 'rb') as f:
            raw = f.read()
    except (IOError, PermissionError):
        return None, [], 0

    if raw[:100].count(b'\x00') > 10:
        return None, [], 0
    if len(raw) > 5 * 1024 * 1024:
        return None, [], 0

    text = raw.decode('utf-8', errors='ignore')
    matches = []
    funcs = SUSPICIOUS_FUNCTIONS.get(ext, [])
    for pattern in funcs:
        if re.search(pattern, text, re.IGNORECASE):
            matches.append(pattern)

    if not matches:
        return None, [], 0

    entropy = file_entropy(raw)
    if len(matches) >= 3 or entropy > 6.0:
        level = "高危"
    elif len(matches) >= 2:
        level = "可疑"
    else:
        level = "低危"

    return level, matches, entropy

def scan_directory(root_dir):
    """递归扫描目录"""
    results = []
    web_exts = ['php', 'jsp', 'asp', 'aspx', 'py', 'phtml']

    for dirpath, _, filenames in os.walk(root_dir):
        for fn in filenames:
            ext = fn.rsplit('.', 1)[-1].lower() if '.' in fn else ''
            if ext not in web_exts:
                continue
            fullpath = os.path.join(dirpath, fn)
            try:
                st = os.stat(fullpath)
            except OSError:
                continue
            level, matches, entropy = scan_file(fullpath, ext)
            if level:
                results.append({
                    'path': fullpath, 'level': level,
                    'matches': [m[:40] for m in matches],
                    'entropy': round(entropy, 2),
                    'size': st.st_size, 'mtime': st.st_mtime,
                })
    return results

def print_report(results):
    if not results:
        print("未发现 WebShell 文件")
        return
    print("\n" + "=" * 70)
    print("  WebShell 扫描报告 -- 发现 {} 个可疑文件".format(len(results)))
    print("=" * 70)
    for r in sorted(results, key=lambda x: {'高危':0,'可疑':1,'低危':2}[x['level']]):
        from datetime import datetime
        mtime_str = datetime.fromtimestamp(r['mtime']).strftime('%Y-%m-%d %H:%M')
        print("\n{}".format(r['path']))
        print("  风险: {} 大小: {} 修改: {} 熵: {}".format(
            r['level'], r['size'], mtime_str, r['entropy']))
        for m in r['matches']:
            print("    - " + m)

if __name__ == '__main__':
    if len(sys.argv) < 2:
        print("用法: python3 webshell_scanner.py <扫描目录>")
        sys.exit(1)
    target = sys.argv[1]
    if not os.path.isdir(target):
        print("错误: 目录不存在: " + target)
        sys.exit(1)
    print("正在扫描: " + target + "...")
    results = scan_directory(target)
    print_report(results)

步骤三:文件完整性校验

#!/bin/bash
# 对 WordPress 核心文件进行完整性校验
WP_DIR="/var/www/html"

# 计算本地文件 Hash
find "$WP_DIR" -type f -name "*.php" -not -path "*/wp-content/*" | \
    while read f; do
    md5sum "$f"
done > /tmp/local_hashes.txt

# 与官方 Hash 比对(以 WordPress 6.x 为例)
# 需预先下载官方 Hash 文件
# sha1sum -c wordpress-6.x.sha1 2>/dev/null | grep -v "OK$"

步骤四:疑点文件深入分析

# 1. 查看文件头尾
head -5 suspicious.php
tail -5 suspicious.php

# 2. 去除注释看有效代码
grep -v "^\s*$" suspicious.php | grep -v "^\s*//" | \
    grep -v "^\s*#" | grep -v "^\s*/\*"

# 3. Base64 解码可疑字符串
grep -oP "base64_decode\(['\"]\K[^'\"]+" suspicious.php | \
    base64 -d 2>/dev/null

步骤五:清除与加固

# 1. 备份可疑文件(用于取证)
cp suspicious.php /root/forensics/suspicious_$(date +%Y%m%d).php

# 2. 删除 WebShell
rm -f suspicious.php

# 3. 查找并修复入侵入口
tail -500 /var/log/auth.log | grep "Accepted"

# 4. 修改所有密码(数据库、SSH、FTP、WP 后台)
# 5. 更新所有软件到最新版本
apt update && apt upgrade -y    # Ubuntu/Debian
yum update -y                    # CentOS/RHEL

配置验证

# 1. 运行查杀脚本
python3 webshell_scanner.py /var/www

# 2. 测试脚本能检测已知 WebShell
echo '' > /tmp/test_webshell.php
python3 webshell_scanner.py /tmp
# 应输出高危警告

# 3. 清理测试文件
rm -f /tmp/test_webshell.php

常见问题

Q1: 特征扫描可能漏掉哪些类型 WebShell?

高级 WebShell 使用多层编码混淆(如多次 Base64 + GZip + 异或)、动态函数调用($func($_POST['cmd']))或利用 PHP 7/8 新特性绕过静态检测。对抗这类混淆需要动态执行沙箱(如 Cuckoo Sandbox)或基于行为分析的 EDR 方案。

Q2: 清除 WebShell 后如何防止再次被入侵?

清除后必须修复入侵入口,否则 24 小时内可能再次被入侵。按优先级处理:(1) 更新所有 Web 应用和插件到最新版本;(2) 移除所有未使用的主题和插件;(3) 禁用文件编辑器(WordPress 中设置 define('DISALLOW_FILE_EDIT', true));(4) 设置 Web 目录所有者与运行用户分离(chown -R root:www-data /var/www 并设置 644/755 权限);(5) 部署 WAF 规则(如 ModSecurity)拦截文件上传漏洞和远程文件包含。

总结

WebShell 查杀是入侵应急响应的核心环节,单靠手工查找效率低下,必须借助自动化扫描脚本。三重检测策略(特征匹配 + 熵值分析 + 时间线分析)可覆盖大多数常见 WebShell。但最有效的防御仍然是事前预防:最小化权限原则、定期安全更新、文件完整性监控和 WAF 部署。