⚖️ 本篇的边界(重要)
本篇只从防御与取证角度讨论 WebShell 管理工具的通信协议特征与检测方法。目的是让取证人员能够看懂日志、识别工具、还原行为、关联攻击者。
本篇不包含:WebShell 的构造方法、免杀与变形技术、加密绕过手法、上传与落地技巧、载荷构造细节。这些内容不属于取证知识,且会直接降低防御方的识别能力。
文中出现的协议描述、字段特征、检测规则,仅用于解释"如何在日志与流量中认出这些工具"。凡涉及具体实现的部分,均以"如何检测"而非"如何实现"的角度表述。
一、概述
磁盘上找不到后门,不等于入侵没发生过。
上一讲从文件系统找 WebShell,但这套方法有个硬边界:很多运营时间不长的后门已经被删了,内存马更是根本不落地为普通文件。
Behinder 这类工具的 Behinder.jar 被加载进 JVM 内存,磁盘扫描可能一无所获。这时候唯一能还原行为的线索只剩流量与日志。
WebShell 管理工具的共性是:通信比正常业务"更像机器"。
正常用户浏览网站,路径五花八门、Referer 层层传递、UA 变化丰富。而工具客户端与后门之间是程序对程序的固定对话——路径长期固定、频率机械等间隔、Referer 恒为 -、请求体是 Base64 或二进制密文。
这种"过于规整"就是本篇的抓手。
在取证流程中的位置:本篇属于分析环的行为重建层,与文件层互为补充而非替代。
它的独特价值在两个场景。
一是唯一证据场景(文件已删、内存马不落地)。
二是交叉验证场景(文件层发现可疑文件后,用日志确认"谁访问过、访问了几次、返回什么状态")。
上游依赖 Nginx 与 Apache 配置取证 提供的 log_format 实际定义与轮转清单;下游支撑攻击链还原(第 5 步)与报告里的归因表述。
涉及的证据形态(具体到字段,不是"流量"这种大类):
| 形态 |
位置 |
可观测特征 |
| 访问日志 |
/www/wwwlogs/<site>.log*、含 .gz 轮转历史 |
combined 的 9 个字段,须先核对实际 log_format |
| URL 参数 |
查询串里的长 Base64 串(q=、key=、pass=) |
最强信号——Base64 出现在正常查询串里极少见 |
| 路径构造 |
/static/img/../upload/xxx.php |
静态目录 + .. 跳出 + 文件名本身是 Base64 |
| 请求体长度 |
需从流量侧或应用层日志取 |
16 的倍数(AES 分组对齐)、少数几个固定值 |
| 响应体长度 |
combined 的 $body_bytes_sent |
固定在几百字节到 2 KB,不随内容变化 |
| Referer / UA |
日志末两段 |
恒为 -;Java/1.8.0_281、Go-http-client 等非浏览器串 |
| 技术栈矛盾 |
纯 PHP 站点出现 .jsp |
命中数与状态码分布是判据 |
| 蜜罐与诱捕 |
主动部署的诱捕路径 |
访问即说明有人在扫,访问者 IP 高价值 |
🔴 一个必须先建立的观念:单个特征几乎必然有误报。 固定路径可能是健康检查,机械频率可能是定时任务,Base64 可能是正常 API。所以本篇的脚本输出是线索排序,不是判决——见 4.2 统计方法 与 4.4 归因表述。
哪个 (IP, 路径) 组合的行为最不像正常业务;某组请求的次数、间隔标准差、请求体长度聚集度、Referer 缺失率分别是多少;可疑 IP 是否在别处也留下过痕迹;某时段的行为属于"写入后验证"还是"持续使用";报告里应该把结论写到哪一档强度。
请求体和响应体的内容——combined 日志里没有这两项。这是本篇最常被误解的一点,见 4.1 字段语义。
对方具体执行了什么命令?只能说"符合某工具的协议特征",不能说"执行了某命令"。
明文密钥、加密参数推导、载荷构造细节——这些属于攻击实现,不在取证知识范围内。
被 access_log off 关闭的 location 下的行为?同样查不到。
内容边界(本篇的红线,已在篇首声明):本文只从防御与取证角度讨论通信协议的可观测特征与检测方法,目的是让取证人员看懂日志、识别工具、还原行为、关联攻击者。不包含 WebShell 构造方法、免杀与变形技术、加密绕过手法、上传与落地技巧、载荷构造细节。凡涉及具体实现的部分,均以"如何检测"而非"如何实现"的角度表述。
读者前提:需要 HTTP 请求/响应结构与 combined 日志的完整字段顺序(先读 第 1 篇 的 2.2 节)、Base64 与 AES 的分组对齐概念、以及基本的统计量含义(标准差、变异系数)。
日志量到百万行级别时手工翻查不现实——这正是 3.4 第 4 步 脚本存在的理由。
建议先做 3.3 的人工粗筛,再跑脚本。
二、核心原理
2.1 主要工具与通信模型概览
国内常用的 WebShell 管理工具,通信模型可归为四类:
| 工具 |
通信模型 |
载荷加密 |
取证要点 |
| 中国菜刀(Caidao) |
单文件后门,客户端直连 |
早期明文;部分版本对请求体做简单混淆 |
请求体短、路径固定、POST 为主;特征在"一个文件承载全部功能" |
| 蚁剑(AntSword) |
单文件后门,客户端直连 |
支持 AES、RC4 等 |
载荷加密参数常通过 URL 传密钥;同 URL 大量 POST |
| Behinder(冰蝎) |
双文件架构:server.php(正常业务文件)+ Behinder.jar(内存马) |
AES-CBC 载荷,密钥在 URL 路径中协商 |
★ 见 2.2 |
| Godzilla(哥斯拉) |
单文件或双文件,支持 JSP/PHP/ASPX |
AES,长密钥经 URL 传参 |
★ 见 2.3 |
| NhShell |
单文件后门 |
自定义编码 |
请求体非明文、常无 Referer |
| rederb / Cobalt Strike 等 |
Stager 型内存马 |
加密信道 |
出现在 .jsp/.asp 但服务器是 PHP,或反之 |
双文件架构为何重要(取证意义):Behinder 这类工具的 Behinder.jar 不落地为普通文件——它被加载进 JVM 内存。所以磁盘扫描可能一无所获,只有流量和内存镜像能证明它存在。这正是本篇存在的意义。
2.2 Behinder 的通信特征(重点)
Behinder 采用 AES-CBC 加密的请求体 + 路径内携带密钥 的模型。
以下从"如何在日志中识别"的角度说明。
结构一:URL 路径携带加密参数
POST /static/img/../upload/aGFsbG8gd29ybGQ.php?q=LEQI1r8CAg1pVQ1WVA1WC1JVA1WC1JVA1WQ1WS1JQ1JVZQ%3D%3D HTTP/1.1
| 观察点 |
说明 |
路径中出现很长的 Base64 串(常见于 q、key、pass 等参数名) |
密钥/校验串;参数名可被工具配置改变 |
路径里用 .. 从静态目录跳到可写目录 |
如 /static/img/../upload/webshell.php——混在静态目录里 |
出现 .. 但最终未跳出站点根 |
典型的"藏在看似静态目录下" |
结构二:请求体是 Base64 包装的 AES-CBC 密文
U3VjbiB2ZXJzaW9uOiAxLjcuMzA= ← Base64 解码后是明文
日志中(combined 格式)的体现:
198.51.100.24 - - [18/Mar/2024:02:14:33 +0800] "POST /static/img/../upload/webshell.php?q=LEQI...%3D%3D HTTP/1.1" 200 1548 "-" "Java/1.8.0_281"
识别要点:
| 特征 |
说明 |
| URL 里出现长 Base64 参数 |
最强信号。Base64 出现在正常 URL 查询串里极少见 |
| 请求体长度为 16 的倍数(Base64 后通常是 4 的倍数) |
AES 分组对齐的结果 |
| 同一 URL 的请求体长度呈少数几个固定值 |
密文长度由明文长度决定,命令种类有限 ⇒ 长度高度聚集 |
| 响应体长度固定在几百字节到 2 KB |
客户端回显结果,不像正常业务那样随内容变化 |
| 无 Referer |
客户端直接构造请求,不可能有站内跳转链 |
| UA 为空或为 Java/Go/Python 客户端串 |
不是浏览器 |
这六条里,长 Base64 参数权重最高。其他几条单独看都能找到正常解释,凑齐了才成立。
取证边界:本篇只说明"密文长度呈 16 倍数、URL 含长 Base64"这类可观测特征。密钥协商的具体字段、加密算法参数的完整推导,属于攻击实现细节,不在取证知识范围内,也不需要在报告里还原——报告只需写明"该通信符合 Behinder 类工具的协议特征"。
2.3 Godzilla 的通信特征
Godzilla 支持多种后门形态与多种加密方式。
取证上有用的共性:
| 特征 |
说明 |
| 密钥在 URL 查询参数中传递 |
参数名常见为 pass、key、q、p(可配置,不可依赖固定名) |
| URL 参数值很长且是合法 Base64/十六进制 |
★ 核心信号 |
| 同 URL、高频 POST、请求体固定长度 |
交互式会话特征 |
| 支持 JSP/ASPX 后门 |
★ 若服务器是纯 PHP 站点,日志里却出现 .jsp / .asp / .aspx 请求且返回 200 —— 高度异常 |
"技术栈与请求路径矛盾"是一条非常好用的规则:
站点是纯 PHP(Nginx + PHP-FPM),但 access_log 里出现:
198.51.100.24 POST /upload/cmd.jsp HTTP/1.1 → 200
这不是扫描(扫描通常返回 404/403),而是 200。
200 意味着服务端真的执行了它——要么站点混装了 Java 容器,要么服务端做了路径伪装映射,要么是另一个后门在响应。
这个矛盾点必须逐个核实。
2.4 流量层的通用检测维度
以下是不依赖具体工具的通用规则,适用于所有 WebShell 运营行为:
| 维度 |
检测方法 |
权重 |
| 路径集中度 |
单一 URL 占比(按 IP 分组统计) |
★★★★★ |
| 请求体长度方差 |
同一 IP+URL 的 POST 长度标准差 |
★★★★★ |
| 无 Referer 比例 |
# Referer 为 - 的占比 |
★★★★ |
| 访问时间规整度 |
相邻请求间隔的变异系数 |
★★★★ |
| UA 异常 |
为空、非浏览器串、与其他流量截然不同 |
★★★ |
| 状态码组合异常 |
同一 URL 只有 200 很少有 404("写后必验"特征) |
★★★★ |
| 技术栈矛盾 |
PHP 站点出现 .jsp/.asp 且返回 200 |
★★★★★ |
"写后必验"是一个高价值模式:
正常用户访问一个不存在的 URL 会收到 404,访问存在的会收到 200,两种都常见。
而 WebShell 场景常见的是:同一路径在极短时间内先 404(探测)后 200(写入后验证),或者长期只出现 200 而从无 404(说明攻击者只用自己的固定路径,不做探索)。
2.5 日志层:必须知道的三条限制
在写检测规则前,必须先认清 Nginx/Apache 日志的天花板:
| 限制 |
后果 |
应对 |
| 默认不记录请求体 |
看不到 AES 密文、指令内容、上传的文件 |
只能从长度、频率、路径推断;结论必须写明这一限制 |
| 默认不记录响应体 |
看不到回显的命令结果 |
同上 |
$body_bytes_sent 是响应体大小,不是请求体大小 |
⚠️ 最常见的误读:有人拿它当"上传大小"分析 |
请求体大小只能从 $request_length(若自定义了 log_format)或上游日志拿 |
可能没有 $request_time / $request_length |
无法做耗时与请求体大小分析 |
先 grep log_format 确认字段 |
可能 access_log off |
该 location 完全无记录 |
见第 05 篇 |
🔴 报告中必须写明:access_log 不含请求体,因此"该请求下发了什么指令"无法从本证据确定。这不是分析不到位,是证据本身的边界。夸大结论是取证报告最严重的缺陷。
2.6 蜜罐与诱捕后门的取证价值
在被入侵的服务器上部署"诱捕后门",是被动获取情报的经典手法。
其取证价值在于:
| 价值点 |
说明 |
| 获取密钥材料 |
攻击者会把自己的加密密钥、口令、C2 地址写进蜜罐——这是主动上送的证据 |
| 还原完整行为 |
蜜罐可以记录攻击者执行的每一条命令和返回结果,弥补 access_log 无请求体的缺陷 |
| 证明攻击意图 |
攻击者在蜜罐上的操作,主观故意证据非常充分 |
| 关联身份 |
蜜罐可以"回话"给攻击者,取证人员在授权范围内可与其交互以获取更多线索 |
⚠️ 合规红线:蜜罐/诱捕后门的部署必须事先获得委托方书面授权,且要明确数据留存与上报流程。未经授权自行部署可能构成违法,且会改变现场状态、影响证据完整性。 若案件已进入司法程序,任何"主动交互"都应在委托方与办案机关共同认可下进行。
三、操作步骤
3.1 第 1 步:确认日志格式与可用字段
Z=/mnt/df/www/wwwlogs/digiforensics-site.log
1) 看真实格式(可能不是 combined!)
grep -n 'log_format' /mnt/df/www/server/nginx/conf/nginx.conf 2>/dev/null
grep -rn 'log_format' /mnt/df/etc/nginx/ 2>/dev/null
2) 确认有哪些轮转文件
ls -l /mnt/df/www/wwwlogs/ 2>/dev/null
digiforensics-site.log digiforensics-site.log-20240317.gz ...
3) 取样看真实行长什么样
head -3 "$Z"
198.51.100.24 - - [18/Mar/2024:02:14:33 +0800] "POST /a.php HTTP/1.1" 200 1548 "-" "Java/1.8.0_281"
3.2 第 2 步:合并所有轮转日志
Z=/mnt/df/www/wwwlogs/digiforensics-site.log
ALL=/evidence/all-access.log
合并并按时间排序(注意:合并前先记录每个文件的哈希)
sha256sum /mnt/df/www/wwwlogs/digiforensics-site.log* | tee /evidence/log-hashes.txt
zcat -f "$Z" /mnt/df/www/wwwlogs/digiforensics-site.log-*.gz 2>/dev/null
| sort -t'[' -k2 | tee "$ALL" | wc -l
3.3 第 3 步:快速人工筛选
在跑脚本之前,先用几条命令把最可疑的挑出来:
Z=/mnt/df/www/wwwlogs/digiforensics-site.log
① 单一 IP 打单一路径的次数最多者(WebShell 运营的典型形态)
awk -F'"' '{split($1,a," "); print a[1], $2}' "$Z"
| awk '{print $1, $2, $3}' | sort | uniq -c | sort -rn | head -20
② URL 里含长 Base64 参数的(Behinder/Godzilla 类)
grep -oE '"[A-Z]+ [^"]*?[a-zA-Z_]+=[A-Za-z0-9%+/=]{24,}' "$Z" | head -20
③ 路径里含 .. 但未跳出站点根
grep -E '"[A-Z]+ [^"]*../' "$Z" | head -20
④ 技术栈矛盾:PHP 站点里的 .jsp/.asp/.aspx 请求
grep -E '.(jsp|asp|aspx)([ ?"]|$)' "$Z" | head -20
⑤ 无 Referer 的 POST 请求(Referer 字段在 "..." 之后)
grep -E '"POST [^"]*" [0-9]{3} [0-9]+ "-" ' "$Z" | head -20
⑥ 非常规 UA 为空的行
awk -F'"' 'NF<6 || $6=="-" || $6==" "' "$Z" | head -20
3.4 第 4 步:可运行的日志分析脚本
下面脚本只读取日志、只做统计、不发起任何连接、不发送任何请求。
#!/usr/bin/env python3
"""
log_triage.py —— WebShell 工具流量特征分析(防御侧)
只读日志,输出可疑度排序。不发送任何网络请求,不执行任何内容。
用法: python3 log_triage.py /evidence/all-access.log
"""
import re
import sys
import gzip
from collections import defaultdict
from datetime import datetime
from statistics import pstdev
combined 格式:$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$referer" "$ua"
LINE_RE = re.compile(
r'^(?P\S+)\s+\S+\s+(?P\S+)\s+'
r'[(?P[^]]+)]\s+'
r'"(?P[^"])"\s+'
r'(?P\d{3})\s+'
r'(?P\S+)\s+'
r'"(?P[^"])"\s+'
r'"(?P[^"]*)"'
)
兜底:请求行含空格的路径也要能解析
REQ_RE = re.compile(r'^(?P[A-Z]+)\s+(?P\S+)(?:\s+(?P\S+))?$')
长 Base64 串(URL 参数里)
B64_RE = re.compile(r'([?&][a-zA-Z_][a-zA-Z0-9_]*=)([A-Za-z0-9%+/=]{24,})')
已知后门