简介:这份资源面向网络安全与机器学习方向的学习者、安全开发人员及课程设计者,提供一套完整的Web攻击检测系统实现方案,重点解决XSS与SQL注入两类常见威胁的识别问题。项目分为AiWaf-1与AiWaf-2两个模块,前者采用聚类思路,后者基于机器学习方法,共实现GRU、CNN、KNN、SVM、RF五种检测模型,检测流程覆盖数据加载、URL解码与小写化预处理、基于预训练word2Vec的向量化与padding补齐、模型训练、预测及评估等环节。压缩包共66个文件,以py源码与pyc编译文件为主,辅以txt说明、csv与pcap数据、pkl与pickle模型文件、png与jpg图示及md文档,整体约26.61MB,目录按模块划分清晰。已有301人学习,适合希望理解WAF检测原理、复现多模型对比实验或作为毕业设计参考的读者,可据此掌握从特征工程到模型评估的完整链路。
1. 基于机器学习的web攻击检测系统:从HTTP流量到告警的完整落地路径
很多团队第一次做web攻击检测,都会掉进同一个坑:把WAF日志直接丢给模型,训练出一个准确率99%的“黑匣子”,上线三天就被误报淹没。问题不在模型,而在整条链路——HTTP请求怎么变成特征、正常流量怎么定义、告警阈值怎么定,这些环节没打通,再好的算法也是空中楼阁。基于机器学习的web攻击检测系统,核心思路是用正常流量训练出“什么是正常”的边界,再用分类模型识别SQL注入、XSS、路径遍历等异常请求。它适合有基础日志采集能力、想从规则引擎升级到智能检测的团队,也适合安全方向的学生做一套能跑通、能讲清楚的完整项目。下面按数据采集、特征工程、模型训练、服务化部署的顺序,把每个环节的参数和坑讲透。
2. 数据从哪来:HTTP请求采集与标注的三种可行方案
2.1 用反向代理日志做冷启动
最省事的起点是Nginx或Apache的access日志。一条典型日志包含客户端IP、时间、方法、URL、状态码、响应大小、User-Agent、Referer。这些字段足够做第一版特征。采集时注意日志格式要包含请求体长度和查询字符串,否则SQL注入的关键特征会丢。常见做法是在Nginx配置里自定义log_format,把$request_body和$args单独记下来,但请求体默认不记录,需要开启client_body_buffer_size并注意性能开销。
# nginx.conf 中自定义日志格式,保留查询串和请求体长度 log_format attack_detect '$remote_addr|$time_iso8601|$request_method|$uri|$args|' '$status|$body_bytes_sent|$http_user_agent|$http_referer|' '$request_length|$request_time'; access_log /var/log/nginx/attack_detect.log attack_detect;这段配置把请求行拆成独立字段,用竖线分隔方便后续解析。$args是查询字符串,SQL注入和XSS经常藏在这里;$request_length包含请求头和体总长度,异常大的请求值得关注。注意$request_body没有直接记录,因为记录请求体会影响性能且可能泄露敏感数据,第一版可以先用URL和头部特征。
2.2 用公开数据集补充攻击样本
正常流量好采,攻击流量难采。自己搭靶场打流量成本高,常见做法是用公开的HTTP攻击数据集做补充。这类数据集通常包含原始HTTP请求文本和标签,格式类似:
GET /index.php?id=1%20UNION%20SELECT%20username,password%20FROM%20users HTTP/1.1 Host: example.com User-Agent: sqlmap/1.5解析时按行读取,遇到空行分隔请求头和体。标签一般有normal、sqli、xss、path_traversal等。注意公开数据集的攻击载荷偏老,直接训练会导致模型对新型编码绕过不敏感,建议只把它当负样本基底,再混入自己业务日志里的正常流量做平衡。
2.3 标注策略:弱监督比人工标注更现实
让安全工程师逐条标几万条日志不现实。我一般用弱监督:规则引擎命中的请求标为攻击,但只取高置信规则(比如明显的union select、<script>),其余全部当正常。这样标注快,代价是正常样本里混入少量漏报攻击,训练时用IsolationForest先剔除异常点。另一个技巧是按时间切分,用前70%时间的数据训练,后30%验证,避免同一次攻击的变体同时出现在训练和测试集里造成虚高。
3. 特征工程:把HTTP请求变成模型能吃的向量
3.1 字符级与词级特征的分工
web攻击检测的特征分两层。字符级特征看的是原始字符串的统计规律:长度、特殊字符比例、大小写切换次数、数字占比、熵值。SQL注入的查询串通常特殊字符比例高、熵值大;XSS的<、>、script出现频率异常。词级特征则把URL按/、?、&、=切分,统计关键词命中,比如select、union、script、../。两层特征拼在一起,树模型能学到组合模式。
import re import math from collections import Counter def char_features(s): """字符级统计特征""" if not s: return [0]*6 length = len(s) special = sum(1 for c in s if not c.isalnum()) upper = sum(1 for c in s if c.isupper()) digit = sum(1 for c in s if c.isdigit()) # 香农熵,衡量字符分布混乱程度 freq = Counter(s) entropy = -sum((v/length)*math.log2(v/length) for v in freq.values()) return [length, special/length, upper/length, digit/length, entropy, len(freq)/length] def keyword_features(s): """关键词命中特征""" s_lower = s.lower() keywords = ['select','union','insert','update','delete','drop', 'script','alert','onerror','../','..\\','etc/passwd', 'cmd=','exec','eval','base64'] return [1 if k in s_lower else 0 for k in keywords]char_features返回6个浮点数,keyword_features返回18个0/1值。实际使用时把URL、查询串、User-Agent分别提取再拼接,总维度控制在100以内,避免维度爆炸。注意熵值对短字符串不稳定,长度小于5时直接置0。
3.2 URL解码与归一化的三个坑
第一个坑:只解码一次。攻击者常用双重编码,%2527解码一次是%27,再解码才是'。我一般循环解码直到不再变化,最多3次。第二个坑:大小写混用。SeLeCt和select要统一转小写再匹配关键词。第三个坑:多余空格和注释。SQL注释/**/和--要替换成单空格,否则关键词被拆散。
from urllib.parse import unquote def normalize(s, max_round=3): """循环解码并归一化""" prev = None for _ in range(max_round): if prev == s: break prev = s s = unquote(s) s = s.lower() s = re.sub(r'/\*.*?\*/', ' ', s) # 去掉块注释 s = re.sub(r'--.*$', ' ', s) # 去掉行注释 s = re.sub(r'\s+', ' ', s).strip() return smax_round=3是经验值,再深层的编码绕过很少见且解码开销大。归一化后的字符串再送进特征提取,能显著降低漏报。
3.3 用TF-IDF做基线,别一上来就上深度学习
很多教程直接上LSTM或BERT,但web攻击检测的样本量通常不大,深度学习容易过拟合。我一般先用TF-IDF加逻辑回归做基线,看AUC能不能到0.95以上。如果能,说明特征工程到位,再换XGBoost或LightGBM提升。TF-IDF的ngram_range设(1,3),max_features设5000,min_df设3过滤低频词。
from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.pipeline import Pipeline pipeline = Pipeline([ ('tfidf', TfidfVectorizer(ngram_range=(1,3), max_features=5000, min_df=3)), ('clf', LogisticRegression(max_iter=1000, class_weight='balanced')) ])class_weight='balanced'很重要,攻击样本通常远少于正常样本,不加这个参数模型会偏向预测正常。max_iter=1000防止收敛警告。
4. 模型训练与评估:准确率是陷阱,召回率和误报率才是关键
4.1 按攻击类型分模型还是统一模型
统一模型简单,但SQL注入和XSS的特征分布差异大,统一模型容易顾此失彼。我的做法是先用一个二分类模型判断“是否攻击”,再用多分类模型判断攻击类型。二分类模型用LightGBM,多分类用同一个特征集但换softmax输出。如果某类攻击样本太少(少于500条),单独训一个二分类器,不要硬塞进多分类。
import lightgbm as lgb from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, stratify=y, random_state=42) model = lgb.LGBMClassifier( n_estimators=300, learning_rate=0.05, num_leaves=31, scale_pos_weight=10, # 正负样本比例约1:10时设10 random_state=42 ) model.fit(X_train, y_train) y_pred = model.predict(X_test) print(classification_report(y_test, y_pred))scale_pos_weight设成负样本数除以正样本数,让模型更关注攻击样本。num_leaves=31是默认值,样本少时降到15防止过拟合。训练完重点看召回率,误报率控制在1%以下再上线。
4.2 评估指标怎么选
准确率在类别不平衡时没有意义。我一般看三个数:攻击召回率(Recall)、正常流量的误报率(FPR)、以及F1。召回率低于0.9说明漏报多,FPR高于0.01说明误报多。上线前用最近一周的真实流量做一次盲测,不要用训练集切出来的测试集,因为同一时段的流量分布太相似。
4.3 模型持久化与版本管理
训练好的模型用joblib保存,同时保存特征提取的TfidfVectorizer,否则线上推理时特征对不上。版本号按日期加序号,比如model_20250101_01.pkl。每次更新模型前,用旧模型和新模型在同一批流量上跑对比,新模型召回率提升但FPR上升超过0.5%就暂缓上线。
import joblib joblib.dump(model, 'model_20250101_01.pkl') joblib.dump(pipeline.named_steps['tfidf'], 'tfidf_20250101_01.pkl')5. 服务化部署:从离线模型到实时告警的最后一公里
5.1 用Flask搭推理接口的最小实现
线上推理要快,单条请求特征提取加预测控制在50毫秒内。用Flask起一个HTTP服务,接收原始URL和User-Agent,返回是否攻击及类型。
from flask import Flask, request, jsonify import joblib app = Flask(__name__) model = joblib.load('model_20250101_01.pkl') tfidf = joblib.load('tfidf_20250101_01.pkl') @app.route('/predict', methods=['POST']) def predict(): data = request.json raw = data.get('url', '') + ' ' + data.get('ua', '') norm = normalize(raw) vec = tfidf.transform([norm]) pred = model.predict(vec)[0] prob = model.predict_proba(vec)[0].max() return jsonify({'attack': int(pred), 'confidence': float(prob)}) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000)normalize函数复用第3章的定义。confidence低于0.7的告警标记为“待确认”,不直接触发阻断,只记录。这样能减少误报对业务的干扰。
5.2 阈值调优与告警分级
模型输出概率后,不要直接卡0.5。我一般设两档:概率大于0.9直接告警,0.7到0.9记录但不告警,低于0.7忽略。阈值用验证集上的FPR反推,比如要求FPR不超过0.5%,就找对应概率分位点。告警分级后,安全团队每天处理的告警量能降一个数量级。
5.3 性能压测与降级策略
用ab或wrk压测接口,单核QPS至少要到200。如果模型推理慢,把LightGBM换成逻辑回归,或者用onnxruntime加速。降级策略:当接口超时率超过5%,自动切回规则引擎,保证业务不中断。
6. 避坑与排查:五条血泪经验
6.1 现象:上线后误报率飙升到10%以上
原因:训练集里的正常流量来自办公网,线上流量来自公网,User-Agent分布完全不同,模型把大量正常爬虫判成攻击。解决:训练集必须混入线上真实流量的采样,至少覆盖一周。上线前用线上流量做盲测,FPR超过1%就不发。
6.2 现象:SQL注入漏报严重,尤其是POST请求
原因:特征只提取了URL,POST请求体里的注入载荷没进模型。解决:在Nginx层记录请求体前512字节,或者用ModSecurity的审计日志。注意请求体可能含敏感数据,记录前做脱敏。
6.3 现象:模型训练时AUC很高,上线后效果差
原因:训练集和测试集按随机切分,同一次攻击的变体同时出现在两边,造成数据泄露。解决:按时间切分,用前70%时间训练,后30%测试。或者按攻击源IP切分,同一IP的请求只出现在一边。
6.4 现象:推理接口偶尔返回500错误
原因:输入URL含非法字符导致unquote抛异常。解决:在normalize外层加try-except,异常时返回原始字符串并记录日志,不要让整个请求失败。
6.5 现象:模型文件加载慢,服务启动要十几秒
原因:joblib加载大模型时反序列化耗时。解决:用onnxruntime转换模型,加载时间能降到1秒内。或者把模型加载放在服务启动时,不要每次请求都加载。
7. 进阶技巧:用在线学习让模型跟上新型攻击
离线模型最大的问题是滞后。新型攻击载荷出现后,模型要等下一次训练才能识别。一个实用技巧是加一层在线学习:把置信度在0.5到0.7之间的样本自动缓存,每天人工确认一批,确认后的样本增量训练模型。LightGBM支持init_model参数,可以在旧模型基础上继续训练,不用从头来。
# 增量训练:在旧模型基础上用新样本继续训练 new_model = lgb.LGBMClassifier( n_estimators=50, learning_rate=0.02, num_leaves=15) new_model.fit(X_new, y_new, init_model=old_model)n_estimators设小一点,只让新样本微调模型,避免覆盖旧知识。learning_rate降到0.02,步子小更稳。每轮增量训练后用固定验证集测一次,F1下降超过2%就回滚。
另一个技巧是特征重要性监控。每次训练后打印Top 20特征,如果某个关键词特征的权重突然暴涨,可能是训练集里混入了标注错误的样本。我习惯把特征重要性变化超过30%的特征单独拎出来看,往往能发现数据问题。
最后说一个我踩过的坑:不要追求模型在测试集上的完美指标。真实环境里,攻击者会不断变形,模型能稳定保持90%召回和1%误报,比测试集上99%准确率但上线就崩要值钱得多。每次更新模型前,用最近一周的真实流量跑一遍,看告警量有没有异常波动,这个习惯帮我省了很多后悔药。希望帮到你。
本文还有配套的精品资源,点击获取