简介:这是一套面向计算机与网络安全专业毕业设计及课程实践的Java漏洞扫描系统源码包,适合需要完成安全工具类课题、学习扫描器架构或进行二次开发的学生与开发者。包内共927个文件,以604个NSE检测脚本和146个Lua脚本为主体,辅以class、jar、java源码及xml配置、txt说明等,整体约33.11MB,目录中保留了较完整的扫描规则与运行组件。资源已有205人学习,可作为漏洞扫描项目的参考样本。其价值在于可对照理解模块化扫描流程、目标爬取与页面解析、SQL注入和XSS等常见漏洞检测思路、扫描结果分析与报告生成,以及图形界面参数配置和多线程调度等实现;同时可借助Nmap脚本库扩展检测能力,适合用来搭建毕设原型、梳理系统结构或研究规则驱动的安全扫描方案。
1. 从一份Java漏洞扫描系统压缩包说起:它到底扫什么、不扫什么
很多人拿到“Java漏洞扫描系统”第一反应是把它当万能扫描器,实际跑一遍会发现它更像一套可拆的规则实验台:爬虫负责找入口,规则引擎负责发载荷,报告模块负责把响应差异翻译成可读结论。这份毕业设计资源把任务管理、规则配置、扫描执行和报告导出串成了一条最小闭环,适合Java Web方向的同学用来理解扫描器内部怎么协作,也适合安全入门者拿本地模拟项目X做回归验证。它不替代人工验证,也不承诺覆盖所有业务逻辑漏洞;真正有价值的是你能改规则、看参数、复现误报。拿到压缩包后先别急着点启动,先确认它扫的是HTTP层常见问题,而不是一个“一键出报告”的黑匣子。
2. 拆开扫描引擎:请求调度、指纹识别与规则匹配怎么串起来
2.1 从入口类看任务生命周期
这个资源里最值得先读的不是页面,而是任务入口类。常见做法是控制器只负责接收目标地址、爬虫深度、规则集和报告格式,真正的扫描动作由任务服务落库后交给调度器。调度器从待执行队列里取任务,先调用爬虫拿URL集合,再按启用的规则逐条发请求,最后把命中结果写入结果表。整条链路如果不拆开看,很容易把“扫描完成”误当成“检测覆盖完整”,实际上爬虫少抓一个参数,后面的规则再全也白搭。
下面这段是任务提交与执行的骨架代码,类名做了简化,重点看线程池和调用顺序:
public class ScanTaskRunner { private final ExecutorService pool = new ThreadPoolExecutor( 4, 8, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(200), new ThreadPoolExecutor.CallerRunsPolicy() ); public void submit(ScanTask task) { pool.submit(() -> { // 1. 按深度抓取目标入口,限制同域,避免跑飞 List<String> urls = crawler.crawl(task.getTarget(), task.getMaxDepth()); // 2. 规则只加载一次,避免每个URL重复读磁盘 List<Rule> rules = ruleRepository.loadEnabled(task.getRuleSet()); for (String url : urls) { // 3. 单URL扫描内部再按规则并发,结果即时落库 List<Finding> findings = scanner.scan(url, rules); resultWriter.save(task.getId(), findings); } }); } }线程池核心数4、最大8、队列200,拒绝策略用CallerRunsPolicy,目的是在队列满时让提交线程自己跑,避免任务被静默丢掉。maxDepth控制爬虫深度,一般本地模拟项目X设2到3足够;设0只扫种子URL,很多“扫不出东西”的现场就是这里被改过。规则加载放在循环外,是因为规则文件读取和正则编译开销不小,放进循环会让扫描速度断崖式下降。失败时先看任务表状态是否长期停在“运行中”,再看线程池队列是否堆满,最后确认爬虫返回的URL数量是不是0。
任务生命周期里还有一个容易忽略的点:结果写入和任务状态更新通常不在一个事务里。扫描线程写完一批结果后挂掉,任务可能仍显示“运行中”,报告却是空的。合格做法是给任务加心跳字段,调度器定期把超时未心跳的任务标记为失败,并保留已写入的部分结果。这个细节在压缩包的调度类里能看出来,读代码时别只盯着扫描器实现。
2.2 请求调度与并发参数怎么设
扫描器的速度和质量几乎是一对矛盾。并发调大,单位时间发的载荷多,但目标容易返回429、403,或者直接把你的出口地址限流;并发调小,一个中等站点跑半天,做课程演示都嫌慢。这个资源把HTTP参数集中放在配置里,常见做法是连接超时3秒、读取超时5秒、全局最大连接100、单路由最大20、重试1次、限速20 QPS。参数不是拍脑袋, reading timeout太小会把慢响应误判成“不存在”,太大又会让卡死任务拖住整个线程池。
scanner: http: connect-timeout-ms: 3000 read-timeout-ms: 5000 max-connections: 100 max-per-route: 20 retry: 1 rate-limit-qps: 20 user-agent: "JavaScan/1.0" follow-redirects: falseconnect-timeout-ms管TCP握手,目标不可达时快速失败;read-timeout-ms管等待响应体,遇到大文件下载要配合最大响应体限制。max-per-route限制同一域名下的并发,避免把单个目标打挂。retry: 1只对连接超时和5xx做一次重试,不要对403重试,否则会加重封禁。follow-redirects: false是为了让跳转本身成为检测证据,很多规则要靠3xx响应判断路径存在。限速20 QPS适合本地模拟项目X和测试环境,放到真实目标上要先确认授权和承受能力。
调参时我一般先跑一轮只爬虫、不发载荷的“干跑”,看URL数量和响应时间分布,再决定并发。干跑阶段把rate-limit-qps设为5,观察目标是否出现连续403;如果出现,先降并发而不是换UA。扫描日志里要能打出每个请求的耗时和状态码,否则调参就是玄学。这个资源默认日志只记任务级别,建议在HTTP客户端加一个拦截器,把超过2秒的请求单独记下来,后面判断超时误报会用到。
2.3 指纹识别与规则匹配的边界
指纹识别的作用不是炫技,而是减少无效请求。常见做法是先看响应头里的Server、Set-Cookie、X-Powered-By,再看页面特征路径,比如登录页、错误页、静态资源目录。强指纹可以直接决定后续加载哪一组规则,弱指纹只用来做结果排序。这个资源里指纹模块和规则模块是分开的,指纹结果写在任务上下文里,规则匹配时可以引用,但默认不强制依赖。这样设计的好处是规则可以单独回归,不会因为指纹没识别出来就整组跳过。
规则匹配不能只看关键词。一个页面出现“SQL syntax”不代表存在注入,可能是帮助文档;一个页面返回200也不代表载荷生效。比较稳的匹配顺序是:先看状态码是否在规则允许范围,再看响应长度是否偏离基线,再看正则或关键词是否命中,最后看响应相似度。下面这张表是读规则时常见的匹配信号强弱对比:
| 信号 | 强指纹用法 | 弱指纹用法 | 误报风险 |
|---|---|---|---|
| 状态码 | 500错误页配合错误关键词 | 单独看200/302 | 高 |
| 响应长度 | 与基线长度差异超过阈值 | 只看长度非0 | 中 |
| 关键词 | 数据库报错原文 | 通用“error”单词 | 高 |
| 响应相似度 | 布尔盲注真假页面差异 | 动态页面直接比 | 高 |
| 跳转链 | 目录遍历命中文件特征 | 只看302 | 中 |
指纹识别的边界在于它只能告诉你“像什么”,不能告诉你“有什么”。我见过把指纹结果直接当漏洞结论的写法,报告里写“识别到某中间件,存在某漏洞”,实际请求都没发。这个资源的报告字段里保留了evidence和request,就是提醒读报告的人回到原始请求。规则匹配的边界则是:它适合发现已知模式,不适合发现业务逻辑缺陷。用这套系统做课程设计,重点应该放在“规则怎么组织、误报怎么收敛”,而不是追求扫出多少条。
3. 把系统跑起来:环境、数据库与一次完整扫描的复现步骤
3.1 环境准备与依赖版本核对
先别急着改代码,先把运行环境对齐。这个资源是Java项目,常见做法是JDK 8或11、Maven 3.6以上、一个关系型数据库。JDK版本不一致最常见的表现是编译时报“不支持的类文件版本”,或者启动时报模块化相关错误。Maven依赖下载失败先看仓库配置,不要一上来就删本地缓存。数据库时区没配好,任务时间会差几个小时,报告排序会乱。
java -version mvn -v两条命令的输出要能对上项目说明里的要求。java -version看主版本,mvn -v看Maven和它使用的JDK是不是同一个。有人终端里java是11,Maven却指向8,编译能过,运行时报方法找不到。依赖下载阶段如果卡在某个包,先换仓库镜像,再看settings.xml里是否配了离线模式。数据库连接前先确认端口和账号权限,不要等启动报错再回头查。
环境里还有一个容易漏的点:报告导出可能依赖字体或模板文件。如果报告生成报模板找不到,先看资源目录是否被打进jar,再看启动工作目录。相对路径在IDE里能跑、在命令行里跑不起来,是这类项目最常见的翻车点之一。我一般会在启动脚本里显式cd到jar所在目录,或者把所有路径改成绝对路径,少给自己留悬念。
3.2 数据库初始化与配置项
数据库脚本一般包含任务表、结果表、规则表和报告表。任务表存目标、状态、爬虫深度、规则集;结果表存URL、规则ID、严重级别、请求响应证据;规则表可以存启用状态,也可以直接读文件。初始化时先建库再执行脚本,字符集用utf8mb4,否则报告里的中文和特殊载荷会乱码。
CREATE DATABASE scan_demo DEFAULT CHARACTER SET utf8mb4; USE scan_demo; CREATE TABLE scan_task ( id BIGINT PRIMARY KEY AUTO_INCREMENT, target VARCHAR(512) NOT NULL, max_depth INT DEFAULT 2, rule_set VARCHAR(128) DEFAULT 'default', status VARCHAR(32) DEFAULT 'PENDING', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE scan_finding ( id BIGINT PRIMARY KEY AUTO_INCREMENT, task_id BIGINT NOT NULL, url VARCHAR(1024), rule_id VARCHAR(128), severity VARCHAR(16), evidence TEXT, request TEXT, response TEXT );target限制512长度是为了防止超长URL把索引撑坏;max_depth默认2,和扫描器配置保持一致;status用字符串而不是枚举,方便后期加“部分完成”。evidence存命中片段,request和response存原始报文,报告里能回放。索引至少给task_id和rule_id各建一个,结果多的时候按任务查不会全表扫。如果脚本里没建索引,自己补上,这是看报告慢的常见原因。
配置项集中在application.yml或同类配置文件里。重点看数据库连接、规则目录、报告目录、上传限制。路径不要用./rules这种相对写法,除非你确定启动目录。数据库密码不要明文提交到公开仓库,课程演示也建议用环境变量覆盖。
app: rule-dir: /opt/scan-demo/rules report-dir: /opt/scan-demo/reports max-response-body-kb: 512 spring: datasource: url: jdbc:mysql://127.0.0.1:3306/scan_demo?useUnicode=true&characterEncoding=utf8 username: scan_user password: ${DB_PASSWORD:change_me}rule-dir和report-dir用绝对路径,省得工作目录变化后规则加载不到。max-response-body-kb限制单次响应体,防止大文件把内存吃满。数据库连接串里带字符集,避免中文证据乱码。密码用环境变量占位,启动时传DB_PASSWORD。如果启动报“表不存在”,先确认连接的是不是刚初始化的库,再确认脚本是否执行到了结果表。
3.3 发起第一次扫描并读报告
启动方式常见两种:IDE里跑主类,或者打包后java -jar。第一次扫描目标不要选复杂站点,用本地模拟项目X,只开一个爬虫深度和一组基础规则。创建任务的接口一般接收目标、深度、规则集和报告格式,返回任务ID。任务执行是异步的,创建成功后要轮询状态,不要创建完立刻去拿报告。
# 打包并启动,端口按需改 mvn clean package -DskipTests java -jar target/scan-demo.jar --server.port=8080 # 创建一次扫描任务 curl -X POST http://127.0.0.1:8080/api/scan \ -H "Content-Type: application/json" \ -d '{"target":"http://127.0.0.1:9090","maxDepth":2,"ruleSet":"default","reportFormat":"json"}' # 轮询任务状态 curl http://127.0.0.1:8080/api/scan/1/status # 下载报告 curl -o report.json http://127.0.0.1:8080/api/scan/1/reporttarget必须带协议,不带协议时爬虫解析会失败;maxDepth先设2,确认URL数量合理后再加;ruleSet对应规则目录下的子目录或文件名,写错不会报大错,只会加载不到规则。轮询状态看到DONE再下载报告,看到FAILED先看任务日志里的异常栈。报告里重点看url、ruleId、severity、evidence四个字段,evidence为空的结果优先当误报处理。第一次跑完不要急着评价系统强弱,先数一下爬虫抓了多少URL、规则发了多少请求、命中多少条,这三个数对不上,问题通常在配置而不是检测逻辑。
4. 规则库与检测逻辑:SQL注入、XSS、目录遍历的判定参数
4.1 规则文件结构:从YAML字段到匹配器
规则文件是这套系统的核心资产。常见结构是每条规则一个YAML条目,包含id、name、severity、match、request、payloads。id要全局唯一,后面做回归测试和误报标记都靠它;severity不要全写high,否则报告没有优先级;match里放状态码、正则、响应长度差、相似度阈值;request描述方法、路径、参数位置和载荷列表。规则加载器一般会先解析YAML,再把正则编译成Pattern,加载失败要能指出是哪一行。
id: sqli-error-001 name: 错误回显型SQL注入探测 severity: high match: status: [200, 500] body_regex: "(SQL syntax|ORA-[0-9]{5}|SQLSTATE|MySqlException)" min_body_length: 20 request: method: GET path: "/product?id=1" payloads: - "1'" - "1\"" - "1')"status允许200和500,是因为有些站点把数据库错误包装成200页面;body_regex只匹配数据库报错特征,不用宽泛的error;min_body_length过滤空响应和极短错误页。payloads按参数闭合方式给三种,不要堆几十个变形,先把基础闭合跑通。规则加载失败时看日志里的规则文件名和行号,YAML缩进错一位就会整条失效。正则里少用贪婪匹配,响应体大时性能会明显下降。
匹配器执行顺序建议固定:状态码过滤、长度过滤、正则匹配、相似度复核。前两步开销小,能挡掉大部分无效响应。正则匹配命中后不要直接写结果,先把请求和响应片段截出来,再交给复核器判断是否重复。这个资源里结果表有evidence字段,就是给复核用的。规则写得再漂亮,如果没有证据片段,报告阅读者还是得重新抓包。
4.2 布尔盲注与时间盲注的阈值设置
盲注检测最怕把网络抖动当漏洞。布尔盲注看真假页面差异,时间盲注看响应延迟。常见做法是每个载荷至少发两次,取响应时间中位数,再和基线比较。布尔盲注的相似度阈值不要设得太死,动态页面本身有随机内容,阈值太低会把正常差异当注入,太高会漏掉细微变化。这个资源里默认相似度阈值在0.85附近,时间延迟阈值3000毫秒,重试2次,实际用的时候要按目标网络调。
double similarity = Similarity.cosine(baseBody, trueBody); boolean booleanBlind = similarity < 0.85 && trueBody.length() > baseBody.length() * 0.5; long delta = timeFalseMedian - timeTrueMedian; boolean timeBlind = delta > 3000 && retryCount >= 2;similarity < 0.85表示真假页面差异足够大,但后面又加了长度比例,防止空页面误判。timeFalseMedian和timeTrueMedian用中位数而不是单次值,能压掉偶发慢请求。delta > 3000是延迟阈值,目标本身响应慢时要先测基线,不能直接套。retryCount >= 2表示时间盲注至少重复两次,单次超时不作数。调试时把每次请求的耗时打进日志,看到连续两次超时再人工确认。
时间盲注还有一个坑:数据库对sleep函数的支持不一样,载荷发过去可能直接报语法错误。规则里要按目标指纹选载荷,不要一套payload打天下。布尔盲注则要注意页面内容是否包含时间戳、随机token、广告位,这些动态内容会让相似度永远偏低。处理方法是在基线请求里先抓两次,把稳定不变的片段提取出来做比较,而不是整页比。这个逻辑在压缩包的相似度工具类里可以改,改完拿本地模拟项目X跑回归,别直接上目标。
4.3 反射型XSS与目录遍历的载荷设计
反射型XSS的判定关键是看载荷是否原样出现在响应里,以及出现在什么上下文。出现在HTML正文、属性值、脚本块里的危险程度不一样,匹配规则也要区分。目录遍历则看路径穿越后是否返回了文件内容特征,比如root:、bin/bash、Windows的[fonts]。载荷设计不要只追求变形数量,先把URL编码、HTML实体编码、双重编码三种情况覆盖到。
List<String> xssPayloads = Arrays.asList( "\"><script>alert(1)</script>", "'><img src=x onerror=alert(1)>", "javascript:alert(1)" ); List<String> traversalPayloads = Arrays.asList( "../../../../etc/passwd", "..%2f..%2f..%2fetc%2fpasswd", "....//....//etc/passwd" );XSS载荷里第一条闭合双引号和标签,第二条闭合单引号并进事件属性,第三条测javascript:伪协议。目录遍历第一条是原始路径,第二条是URL编码,第三条是绕过去除../的写法。发载荷前要确认参数位置,id=1这种数字型参数和q=abc这种字符串型参数闭合方式不同。匹配响应时不要只看alert(1),要看它是否在可执行上下文里;如果只是被转义后显示在页面上,不能算反射型XSS。
目录遍历的响应特征要按操作系统区分,Linux看root:x:0:0,Windows看盘符或初始化文件段落。响应状态码可能是200,也可能是500,不能只认200。文件内容被截断时看前几百字节就够,别把整个文件读进内存。这个资源默认限制响应体512KB,调大之前先想清楚会不会把扫描器自己拖死。XSS和目录遍历都属于“证据比结论重要”的检测,报告里没有原始请求和响应片段,基本没法复核。
5. 避坑与排查:扫不出、误报高、任务卡死的现场记录
5.1 扫不出结果:规则目录与种子URL的两个坑
现象一:任务状态显示完成,结果表0条,日志里也没有请求记录。原因通常是规则目录用了相对路径,IDE启动时工作目录是项目根目录,命令行启动时工作目录变了,规则加载器读不到文件又不抛异常。解决是把rule-dir改成绝对路径,或者在启动脚本里先cd到jar所在目录,再在启动日志里打印实际加载的规则数量。规则数量为0时直接让任务失败,别让它“成功”跑完。
现象二:只扫了首页,内部链接一个没抓。原因多半是爬虫深度设成0,或者链接提取正则只匹配href="http...",漏掉了相对路径和表单。排查时先把爬虫结果单独打出来,看URL集合大小;再把深度临时调到2,确认同域限制没有把子路径挡掉。表单入口要单独解析action和method,只抓a标签会漏掉登录、搜索这类关键入口。
# 启动时显式指定规则目录,避免相对路径歧义 java -jar target/scan-demo.jar \ --app.rule-dir=/opt/scan-demo/rules \ --logging.level.app.scan=DEBUG启动参数覆盖配置后,日志里应能看到“loaded rules: N”。N为0就停下查路径,不要继续创建任务。爬虫调试日志里看crawled urls数量,数量和页面实际链接数差太多,再去看链接提取器。这个坑的血泪经验是:扫描器说“完成”不等于“扫过”,中间任何一层空集合都会被当成正常结束。
5.2 误报与漏报:相似度、编码和去重
现象三:动态页面每条URL都报XSS,证据里只有反射的载荷片段。原因是基线响应本身包含时间戳、随机token,相似度阈值又设得低,任何响应都被判成“差异明显”。解决是先对同一URL发两次基线请求,提取稳定片段再比较;匹配到载荷后加一步上下文判断,只有落在标签或事件属性里才记high,纯文本回显降级或丢弃。去重时按url + ruleId + 参数名聚合,别按整段响应哈希,否则动态内容会让同一条漏洞反复出现。
现象四:编码后的注入点漏报,原始载荷能中,URL编码后不中。原因是扫描器只发原始载荷,没有做编码矩阵。解决是对每个参数生成原始、URL编码、双重编码三种变体,规则里分别给payloads。漏报排查不要只看结果表,把实际发出的请求打到调试日志,确认服务端收到的是编码前还是编码后的值。有些框架会自动解码一次,有些不会,这个差异只能靠请求日志确认。
5.3 任务卡死与内存溢出:超时、响应体和结果写入
现象五:扫描任务长时间停在运行中,线程池队列堆满,新任务提交不进去。原因通常是读取超时没设或设得过大,目标返回一个持续下载的大文件,工作线程一直占着。解决是设置read-timeout-ms和max-response-body-kb,响应体超过限制直接截断并记录;线程池加超时中断,任务心跳超过阈值标记失败。排查时用线程栈看卡在哪个读操作,再看该URL的响应头Content-Length。
scanner: http: read-timeout-ms: 5000 limit: max-response-body-kb: 512 task: heartbeat-timeout-seconds: 120read-timeout-ms管单次读,max-response-body-kb管总量,heartbeat-timeout-seconds管任务级卡死。三个参数要一起用,只设一个还会从别的口子漏。线程栈里如果大量线程停在socketRead,基本就是响应体没限住。任务心跳由扫描线程定期更新,调度器发现超时后把状态改成失败,并保留已写入结果,别让用户对着“运行中”干等。
现象六:跑大目标时内存溢出,日志里出现大量结果对象。原因是结果全量放内存,报告生成时又复制一份。解决是扫描结果分页写库,报告用流式写出,每写一条就释放;导出大报告时限制单文件条数,超过就拆文件。排查时看堆转储里占用最多的是不是Finding列表,如果是,先改写入方式,别急着加堆内存。加内存只能推迟溢出,不能解决全量持有。
6. 进阶用法:把扫描结果接进CI与自定义规则验证
这套系统跑通之后,最有价值的用法不是手动点页面,而是把命令行模式接进流水线。常见做法是给启动参数加--target、--rules、--report、--fail-on,扫描完成后根据严重级别设置退出码,流水线看到非0就阻断。报告用JSON,方便后续转表格或缺陷单。下面这段命令把high及以上作为失败条件,jq只做示例,实际可以用项目里的报告解析器。
java -jar target/scan-demo.jar \ --target http://127.0.0.1:9090 \ --rules /opt/scan-demo/rules \ --report /opt/scan-demo/out/report.json \ --fail-on high # 报告中存在 high 时退出码非0,流水线据此阻断 jq -e '.[] | select(.severity=="high")' /opt/scan-demo/out/report.json > /dev/null--fail-on high比在脚本里自己判断更稳,因为严重级别映射在系统内部统一。--report指定绝对路径,流水线工作目录变化不会丢报告。jq那行只是复核,真正阻断靠退出码。自定义规则验证我一般分三步:先拿本地模拟项目X的固定响应做样例,给每条规则准备一个应命中和一个不应命中的响应文件;再写一个很小的回归脚本,批量跑规则匹配器;最后才把规则放到测试目标上小流量验证。每次只动一类规则,新增、调阈值、删规则分开提交,出了问题能快速回滚。
从那以后我每次改规则都先跑一遍本地样例,再去看真实目标的结果差异;扫描器最怕的不是扫不到,而是你不知道为什么扫不到。希望帮到你。
本文还有配套的精品资源,点击获取