适用场景
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 部署。