
简介这是一份面向网络安全初学者与毕设学生的WebShell检测工具源码包基于Python实现可用于快速识别服务器中潜藏的恶意脚本帮助理解特征码匹配、文件遍历与插件化检测的完整思路。压缩包共21个文件以19个py脚本为主辅以1个md说明文档和1个html报告页面整体仅12KB轻量易读。核心模块涵盖目录扫描、敏感词过滤、WebShell特征匹配及报告生成plugins目录下按php_dynamic_function、php_array_map、php_eval_assert等类型拆分检测插件结构清晰便于按需扩展与二次开发。目前已有55人学习下载适合作为课程设计、毕业设计参考或安全工具入门练手项目读者可从中掌握特征码定义、递归搜索、低干扰分析等关键实现方式并借鉴插件化架构自行补充新规则。1. 从一次应急响应说起Python 写的 webshell 检测工具到底解决什么问题凌晨两点被叫起来查一台对外 Web 服务器客户说页面被篡改日志里一堆陌生 POST。登上去翻www目录upload.php、shell.jsp、1.php这种文件名一眼可疑但真正让人头疼的是那些伪装成正常业务文件的马——config.php里混着eval($_POST[x])cache.php里藏着 base64 解码后拼接执行。手工 grep 关键词能捞出一部分可一旦攻击者做了编码、拆分、注释混淆肉眼和正则就开始集体翻车。这就是 webshell 检测工具存在的意义把「翻文件找可疑代码」这件事从人肉经验变成可复现的自动化流程。用 Python 来做这件事是很多一线工程师的默认选择。原因很实在文件遍历、编码处理、正则匹配、哈希计算这些基础能力Python 标准库就够用要接 YARA、要调机器学习模型、要出 HTML 报告生态里都有现成轮子。这个标题下的工具本质是一个「扫描器」——输入一个目录或一批文件输出一份可疑文件清单和判定理由。它适合两类人做应急响应、需要快速定位 webshell 的安全运维以及想理解 webshell 特征、自己动手写检测逻辑的开发者。下面我按自己实际搭过的一套方案把选型、实现、参数和坑讲清楚。2. webshell 检测的核心逻辑特征、熵值与 AST 三条路线怎么选2.1 先搞清楚 webshell 长什么样检测才有靶子webshell 的本质是一段能被 Web 服务器解析执行的代码攻击者通过 HTTP 请求把指令传进去。按形态分常见的有三类。第一类是「一句话木马」代码极短比如 PHP 的?php eval($_POST[cmd]);?特征是危险函数加外部输入。第二类是「功能型大马」自带文件管理、命令执行、数据库操作界面代码量大但会密集出现system、exec、shell_exec、passthru、popen、proc_open这类函数。第三类是「混淆马」用 base64、gzinflate、str_rot13、字符串拼接、变量函数等方式把真实逻辑藏起来静态关键词经常抓不到。检测路线也就对应着这三类形态展开。最直接的是特征匹配维护一份危险函数和敏感模式的正则库逐文件匹配。它快、可解释但对混淆马漏报高。进阶一点的是熵值分析混淆后的代码往往是一大段无规律字符串信息熵明显高于正常业务代码用熵值做辅助打分能捞出不少特征匹配漏掉的。再往上是语法树分析把 PHP、JSP 代码解析成 AST追踪「外部输入是否流到了危险函数」这是从「像不像」升级到「是不是」的思路准确率高但实现成本也高。我一般会怎么选如果目标是应急响应里快速出结果特征匹配加熵值打分就够了一两个小时能跑起来如果要做成长期驻留的检测能力再考虑引入 AST 或 YARA 规则。别一上来就追求全 AST投入产出比不划算。2.2 三条路线的能力边界对比把三条路线的关键维度摆在一起看选型会清楚很多。维度特征匹配熵值分析AST 数据流实现难度低低高一句话木马高命中一般高命中混淆马低命中高命中中命中误报率中业务代码含 eval 会误报中压缩文件误报低扫描速度快快慢可解释性强弱强适合场景应急快扫辅助打分长期检测实际落地时我通常把特征匹配作为主判据熵值作为加分项两者加权出一个风险分。比如命中eval加 40 分命中$_POST加 20 分熵值超过阈值加 15 分总分过 60 就进可疑清单。这样既保留了可解释性又能覆盖一部分混淆样本。AST 那套留给有持续运营需求的团队普通应急不必上。提示加权分数不是越高越好阈值定太低会把正常框架代码全捞进来定太高又漏马。建议先用一批已知正常文件和已知 webshell 做校准找到误报和漏报的平衡点。3. 用 Python 把扫描器跑起来目录遍历、特征库与打分实现3.1 最小可运行版本遍历目录加正则匹配先搭一个能跑的最小版本核心就三件事遍历目标目录、读文件内容、用正则匹配危险模式。下面这段代码可以直接存成scanner.py运行。import os import re # 危险函数与敏感模式按语言分组 PATTERNS { php: [ (r\beval\s*\(, 40, eval 执行), (r\bassert\s*\(, 30, assert 执行), (r\b(system|exec|shell_exec|passthru|popen|proc_open)\s*\(, 35, 命令执行函数), (r\$_(POST|GET|REQUEST|COOKIE)\s*\[, 20, 外部输入), (r\bbase64_decode\s*\(, 15, base64 解码), (r\bgzinflate\s*\(, 15, gzinflate 解压), ], jsp: [ (rRuntime\.getRuntime\(\)\.exec, 40, Runtime 命令执行), (rProcessBuilder, 30, ProcessBuilder), (rrequest\.getParameter, 15, 外部输入), ], } # 按扩展名映射语言 EXT_LANG {.php: php, .jsp: jsp, .jspx: jsp, .asp: asp, .aspx: asp} def scan_file(path): ext os.path.splitext(path)[1].lower() lang EXT_LANG.get(ext) if not lang: return None try: with open(path, r, encodingutf-8, errorsignore) as f: content f.read() except Exception as e: return {path: path, error: str(e), score: 0, hits: []} score 0 hits [] for pattern, weight, desc in PATTERNS.get(lang, []): if re.search(pattern, content, re.IGNORECASE): score weight hits.append(desc) return {path: path, score: score, hits: hits} def scan_dir(root): results [] for dirpath, _, filenames in os.walk(root): for name in filenames: full os.path.join(dirpath, name) r scan_file(full) if r and r[score] 0: results.append(r) results.sort(keylambda x: x[score], reverseTrue) return results if __name__ __main__: import sys target sys.argv[1] if len(sys.argv) 1 else . for item in scan_dir(target): print(f[{item[score]:3}] {item[path]} - {, .join(item[hits])})逻辑说明PATTERNS是特征库每个模式带一个权重命中就累加。scan_file先按扩展名判断语言读文件时用errorsignore避免编码问题中断扫描。scan_dir用os.walk递归遍历最后按分数降序输出。参数上权重是我根据经验给的初始值eval和命令执行函数权重最高因为一旦出现基本可以定性外部输入权重中等单独出现不算马但和危险函数组合就危险。运行方式很简单python scanner.py /var/www/html输出会按风险分从高到低列出文件分数越高越可疑。这个版本已经能覆盖大部分一句话木马和功能型大马但对混淆马基本无能为力接下来补熵值。3.2 加熵值打分把混淆马捞出来混淆后的代码特征是「一大段高熵字符串」用香农熵可以量化。在scan_file里加一段计算逻辑。import math from collections import Counter def shannon_entropy(data): if not data: return 0.0 counter Counter(data) length len(data) entropy 0.0 for count in counter.values(): p count / length entropy - p * math.log2(p) return entropy def entropy_score(content, threshold4.5, min_len200): # 只对足够长的内容算熵避免短文件误判 if len(content) min_len: return 0 # 取最长的一行做熵计算混淆代码通常集中在单行 longest max(content.splitlines(), keylen, default) if len(longest) min_len: return 0 ent shannon_entropy(longest) return 15 if ent threshold else 0逻辑说明shannon_entropy是标准香农熵公式值越高说明字符分布越随机。entropy_score只对长度超过min_len的最长行计算因为混淆代码常被压成一行超长字符串。阈值4.5是经验值正常业务代码单行熵一般在 3.5 到 4.2 之间超过 4.5 大概率是编码后的 payload。这个分数加到总分里就能把特征匹配漏掉的混淆马拉进可疑清单。参数调整建议如果目标目录里有大量压缩后的 JS 或 CSS熵值会误报可以把min_len调大或者只对.php、.jsp这类可执行脚本算熵。别对图片、字体文件算没意义还拖慢速度。3.3 输出报告与批量扫描的工程化处理应急场景下光在终端打印不够得出一份能交给客户的报告。加一个 HTML 输出顺便处理大目录的扫描性能。import html import time def write_report(results, out_pathreport.html): rows [] for r in results: rows.append( ftrtd{r[score]}/td ftd{html.escape(r[path])}/td ftd{html.escape(, .join(r[hits]))}/td/tr ) tpl htmlheadmeta charsetutf-8titlewebshell 扫描报告/title styletable{border-collapse:collapse}td,th{border:1px solid #999;padding:4px 8px}/style /headbodyh2扫描结果/h2 tabletrth风险分/thth文件/thth命中特征/th/tr{rows}/table /body/html with open(out_path, w, encodingutf-8) as f: f.write(tpl.format(rows.join(rows))) def scan_dir_parallel(root, workers4): from concurrent.futures import ThreadPoolExecutor files [] for dirpath, _, filenames in os.walk(root): for name in filenames: files.append(os.path.join(dirpath, name)) results [] with ThreadPoolExecutor(max_workersworkers) as pool: for r in pool.map(scan_file, files): if r and r[score] 0: results.append(r) results.sort(keylambda x: x[score], reverseTrue) return results逻辑说明write_report把结果渲染成 HTML 表格html.escape防止文件路径里的特殊字符破坏页面。scan_dir_parallel用线程池并发扫描workers默认 4I/O 密集场景可以调到 8 到 16但别开太大磁盘随机读会成为瓶颈。实测一个几万文件的站点单线程要几分钟4 线程能压到一分钟出头。注意并发扫描时如果目标目录在机械硬盘上线程数开太高反而更慢因为磁头频繁寻道。SSD 上可以放心加线程。4. 避坑与排查webshell 检测工具最容易翻车的五个地方4.1 编码问题导致文件读不全现象扫描结果里某些文件明明有可疑代码却没被命中或者直接报错跳过。原因目标文件是 GBK、GB2312 编码用 UTF-8 读会抛异常errorsignore虽然不报错但会丢掉部分字符导致正则匹配失败。解决读文件时先探测编码或者用errorsreplace保留占位符再对多种编码各试一次。我一般会写一个read_file_safe依次尝试 utf-8、gbk、latin-1哪个成功用哪个。4.2 正则写太宽正常业务代码被误报现象报告里一堆正常文件被标红比如框架的eval用于模板编译或者system出现在注释里。原因正则没有区分上下文也没排除注释和字符串。解决匹配前先剥离注释和字符串字面量或者对命中位置做二次确认。更简单的办法是维护一个白名单把已知框架文件、第三方库目录排除掉。误报太多会让人对报告失去信任这比漏报更伤。4.3 大文件拖垮扫描速度现象扫描卡在某个几 MB 的日志文件或压缩包上半天不动。原因os.walk把所有文件都读进内存大文件直接吃满。解决扫描前按扩展名过滤只处理脚本文件对超过一定大小比如 2MB的文件跳过或只读前若干 KB。webshell 通常不会太大几 MB 的脚本文件本身就可疑但没必要全读。4.4 熵值阈值一刀切压缩文件全中招现象.min.js、.css这类压缩文件熵值很高全被标成可疑。原因压缩和混淆在熵值上表现相似单靠熵值分不开。解决对已知的压缩文件后缀直接跳过熵值计算或者结合文件内容判断——压缩 JS 通常有function、var等正常关键字而混淆马往往是纯编码字符串。也可以把熵值权重调低只作为辅助信号。4.5 只扫文件不扫内存漏掉无文件落地现象磁盘上干干净净但服务器确实被控了。原因攻击者用内存马代码不落盘或者 webshell 被删了但进程还在。解决文件扫描只是第一层配合进程排查、网络连接检查、Web 日志分析一起做。检测工具的输出要能和其他应急手段联动别指望一个脚本解决所有问题。5. 进阶技巧用 YARA 规则和文件哈希做二次确认特征库维护久了会发现纯正则的规则越来越臃肿改一处影响一片。这时候可以引入 YARA它天生就是为恶意代码特征匹配设计的规则可读性和可维护性都比裸正则好。Python 里用yara-python库就能调。import yara RULE_SOURCE rule PHP_Webshell_Eval { meta: description PHP 一句话木马特征 score 40 strings: $eval /eval\\s*\\(/ $input /\\$_(POST|GET|REQUEST|COOKIE)/ condition: $eval and $input } def yara_scan(path, rules): try: matches rules.match(path) except yara.Error: return [] return [m.rule for m in matches] rules yara.compile(sourceRULE_SOURCE)逻辑说明YARA 规则里strings定义特征condition定义组合逻辑$eval and $input表示两个特征同时出现才命中这比单条正则精准得多。meta里的score可以拿来和前面的打分体系对接。yara_scan对单个文件匹配返回命中的规则名。另一个实用技巧是文件哈希比对。把已知 webshell 样本的 MD5 或 SHA256 存成一个集合扫描时先算哈希查表命中直接定性不用走正则。这个对已知样本极快缺点是只能抓已知的。我一般把哈希比对放在最前面命中就跳过后续分析省时间。import hashlib def file_hash(path, algosha256): h hashlib.new(algo) with open(path, rb) as f: for chunk in iter(lambda: f.read(8192), b): h.update(chunk) return h.hexdigest() KNOWN_BAD { # 实际使用时从样本库加载 e3b0c44298fc1c149afbf4c8996fb924...: 已知 webshell 样本 A, }逻辑说明file_hash分块读取避免大文件吃内存。KNOWN_BAD是哈希到描述的映射实际用的时候从文件或数据库加载。扫描流程里先查哈希再走 YARA最后走正则加熵值层层递进既快又准。最后说个验证方法拿一批已知的正常文件和 webshell 样本做回归测试统计误报率和漏报率。每次改规则都跑一遍避免改好一个漏掉十个。我自己吃过这个亏规则改完没回归上线后误报翻倍被运维追着问。检测工具的可信度是靠一次次校准攒出来的不是规则写得越多越好。希望帮到你。本文还有配套的精品资源点击获取