简介:一份基于 Java 的漏洞扫描系统完整工程包,面向安全测试、网络运维与 Java 后端开发人群,帮助读者搭建集端口扫描、服务识别、漏洞指纹匹配、CVE 漏洞库查询、策略配置、报告告警于一体的安全检测工具。工程将 Java 网络编程、多线程并发、NIO、加密与 SSL/TLS 等技术落地到实际扫描场景中,并整合了 Nmap 生态的大量脚本资源。压缩包共 927 个文件、约 33.07MB,核心内容以 nse/lua 扫描脚本、class/jar 编译产物与依赖库、java 源码和 xml/txt 配置/说明文档为主,另有 ndiff、bat 启动脚本等辅助工具。nse 脚本可用于服务识别与漏洞探测,Java 代码则展示了扫描调度、并发控制与报告生成思路,整体结构清晰,方便按模块阅读和二次开发。已有 182 人学习下载,适合具备一定 Java 基础、希望结合 Nmap 脚本理解真实漏洞扫描系统的读者。
1. 这个「基于Java的漏洞扫描系统」压缩包,解压之后比预想中实在得多
「基于Java的漏洞扫描系统」这个 zip 包解压开,第一眼看到的不是满屏 .java 源文件,而是Main.class、DisplayForm.class两个已编译类,外加nmap_service.c、mysql-cis.audit、ndiff.bat、start.bat这几个明显属于 Nmap 生态和 Windows 批处理体系的东西。这套组合的定位很明确:用 Java 做调度中枢、并发控制和报告出口,用 Nmap 系工具做底层端口探测与服务识别,再用 CIS 审计脚本补上数据库配置基线检查,三层拼成一条能跑的扫描流水线。它能解决的实际问题包括内网资产端口暴露面摸底、MySQL 配置项安全合规检查,以及把扫描结果整理成带修复建议的报告。适合正在做安全方向课程设计或毕业设计的 Java 学生,也适合运维同学在采购商业扫描器之前先用手头这套顶一阵。
2. 拆包看构成:Main.class、nmap_service.c 与 mysql-cis.audit 到底怎么分工
这套系统的文件构成乍看有点杂,但它其实遵循一个很清晰的分层思路。Java 侧负责 UI、任务调度、结果汇总与报告输出,Nmap 侧负责真正触达网络的探测行为,CIS 审计脚本则专门处理 MySQL 这类数据库的配置基线。三层各自的产物通过文件、命令行输出和约定好的中间格式对接,下面逐个拆开说。
2.1 文件清单与职责映射:每个文件都不是白放的
把压缩包里的核心文件按职责归类,大概是这么个对应关系:
| 文件 | 类型 | 职责 |
|---|---|---|
Main.class | Java 字节码 | 程序入口,负责初始化扫描框架,封装 Nmap 命令调用与结果回传 |
DisplayForm.class | Java 字节码 | 图形化展示层,通常承载扫描进度、结果列表和报告预览 |
nmap_service.c | C 源码 | Nmap 的服务探测脚本源码,用于增强服务指纹识别能力,通常配合nmap-service-probes使用 |
mysql-cis.audit | 审计脚本 | 针对 MySQL 的安全配置基线检查规则,符合 CIS Benchmark 要求 |
ndiff.bat | 批处理 | 调用 Ndiff 工具对比两次扫描结果的差异 |
start.bat | 批处理 | 一键启动脚本,设置 Java 运行时参数并拉起主类 |
CHANGELOG | 文本 | 记录了迭代过程中的版本变更与已知问题 |
这里最容易被忽略的是nmap_service.c。很多人以为它是 Nmap 项目本身的源码,实际上它往往是一个自定义的服务探测规则文件或补丁,用来自定义识别非标准端口的服务类型。比如内网里常见的办公系统跑在 8088、9090 这类非常规端口上,默认的 Nmap 指纹库很可能识别不出来,而这个文件就是用来补指纹的。
mysql-cis.audit的价值在于它把 MySQL 的安全基线检查脚本化了。传统做法是 DBA 手工执行SHOW VARIABLES逐条核对配置项,这个文件则可以让 Nmap 的 NSE(Nmap Scripting Engine)或 Nessus 直接读取规则,自动比对目标数据库的配置状态。
2.2 start.bat 的内容还原:Java 程序是怎么把 Nmap 拉起来的
start.bat是整个系统跑起来的第一道闸门。虽然原包里的批处理用了相对路径,但从常见做法和类文件依赖来看,它的核心逻辑应该是先检查 Java 环境、定位 Nmap 安装路径,再调用java -cp把主类跑起来。一个典型还原如下:
@echo off setlocal rem 检查 JAVA_HOME 是否配置 if "%JAVA_HOME%"=="" ( echo [ERROR] JAVA_HOME is not set. Please install JDK 8+ first. pause exit /b 1 ) rem 检查 nmap 是否在 PATH 中 where nmap >nul 2>nul if errorlevel 1 ( echo [ERROR] nmap not found in PATH. Please install nmap and add to PATH. pause exit /b 1 ) rem 设置 Java 运行时内存参数,避免大网段扫描时堆溢出 set JAVA_OPTS=-Xms256m -Xmx1024m -Dfile.encoding=UTF-8 rem 启动主类 Main "%JAVA_HOME%\bin\java" %JAVA_OPTS% -cp .;lib\* Main endlocal这段脚本里三板斧:先验证环境变量,再验证 Nmap 可执行文件,最后带参启动。-Xmx1024m这个参数在扫描大型 C 段网段时很关键,如果内存给太小,结果解析阶段会出现OutOfMemoryError,而且是在扫描快要结束的时候才崩,非常憋屈。-Dfile.encoding=UTF-8同样重要,因为 Nmap 的输出在中文 Windows 环境下默认是 GBK 编码,如果 Java 侧读取时不做编码对齐,解析结果就是乱码。
2.3 Java 调用 Nmap 的命令行封装方式
Main.class内部对 Nmap 的调用,常见做法是用ProcessBuilder把扫描命令组织成字符串数组直接执行。这里有讲究:不能把整条命令拼成一个字符串丢给cmd /c,因为-p 1-65535、--script这类参数里如果含特殊字符,Windows 下会被解释错位。
ProcessBuilder pb = new ProcessBuilder( "nmap", "-sS", "-sV", "-O", "-p", targetPortRange, "--open", "-oX", "-", // 输出 XML 格式到标准输出 "-Pn", // 跳过主机发现,视为在线 targetIp ); pb.redirectErrorStream(true); Process process = pb.start(); // 注意:必须用独立线程消费 stdout,否则管道缓冲区写满会阻塞 BufferedReader reader = new BufferedReader( new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8) );关键点是-oX -,它让 Nmap 把结果以 XML 格式直接输出到标准输出,Java 侧解析 XML 比解析文本表格要稳定得多。-Pn在这里是刚需,因为很多目标主机会过滤 ICMP 探测包,不加这个参数会导致在线主机被漏掉。redirectErrorStream(true)把 stderr 并进 stdout,省去双管道读写的死锁风险。
3. 核心扫描链路:端口探测、服务识别与漏洞指纹这一步是怎么走通的
扫描系统最核心的链路是「端口探测 → 服务识别 → 指纹比对 → 漏洞匹配」。这套系统把这条链路拆成了三段:Nmap 负责前两段,Java 侧用做第三段的规则匹配,最后再把结果汇总成报告。理解这段链路,才算真正看懂了这套代码的骨架。
3.1 端口扫描策略选择:为什么默认用 SYN 半开而不是 TCP 全连接
Nmap 扫描阶段要做的第一件事是判定目标端口是否开放。这里有个策略选择问题:-sS(SYN 半开扫描)和-sT(TCP 全连接扫描)效果完全不同。半开扫描不建立完整 TCP 连接,速度快、日志少,但需要管理员权限(Windows 下必须用管理员身份运行批处理);全连接扫描会走完三次握手,无需特权但速度慢、目标日志里会留下大量连接记录。
这套系统在start.bat层面没有强制指定扫描类型,说明具体类型是在 Main 类的扫描配置界面上选的。常见的默认配置是-sS,但在虚拟机、容器环境里跑的时候,如果当前用户权限不够,-sS会静默降级为-sT,这会导致扫描时长成倍增加。遇到这种情况,先去系统日志确认有没有「Operation not permitted」一类提示,比直接怀疑 IP 填错更有效。
3.2 服务识别与版本探测的组合参数
服务识别是漏洞匹配的前置条件,识别错了,后面全白搭。这套系统里的 Nmap 参数通常长这样:
nmap -sV --version-intensity 7 -p <ports> <target>--version-intensity取值范围 0 到 9,默认是 7。数字越大,探测时发的探针越多,识别越准,但耗时越长。我在实际用它扫内网的时候,一般会降到 5,因为内网资产版本相对固定,强度 7 和 5 的结果基本一致,但耗时能省下百分之三四十。如果目标是公网资产且需要准确识别中间件版本,才把它拉回 7。
这里最容易踩坑的是nmap_service.c没有生效。很多人知道往nmap-service-probes里加规则,但改了之后完全不生效,原因通常是 Nmap 实际加载的是系统安装目录下那份服务探测文件,而不是源码目录里那份。调试方法很简单:执行nmap --version看到安装路径,用nmap --service-probes或nmap -d -sV看调试输出里到底加载了哪个文件。
3.3 漏洞指纹匹配逻辑:CVE 库的管理与更新机制
这套系统的 Java 侧负责把 Nmap 识别出的「服务名 + 版本号」与漏洞库做比对。漏洞库的数据结构通常是这样的:
public class VulnEntry { private String cveId; // CVE-2024-1234 private String serviceName; // apache / nginx / mysql private String versionRange; // 例如: 2.4.0 - 2.4.49 private int severity; // 0-10, CVSS 分数 private String description; private String suggestion; // 修复建议 }匹配逻辑的核心是一个区间判断:目标服务版本落在哪个漏洞影响区间内,就判定为命中。这个逻辑有两个坑。第一个坑是版本字符串的规范化,如Apache 2.4.49和Apache/2.4.49这两个字符串如果不做统一处理,正则匹配会直接漏掉。我处理时会先提取2.4.49再转成[2, 4, 49]这样的整数数组做逐位比较,而不是直接字符串包含。
第二个坑是漏洞库的时效性。这套系统的 CHANGELOG 里明确写了它是把漏洞库内嵌在项目中的数据文件里,意味着它不会自动更新。CVE 数据是日更的,半年不更新,扫描结果的参考价值就大打折扣。常见做法是在 Java 侧预留一个loadVulnDb()接口,把漏洞库从本地文件换成从 NVD API 或者绿盟、奇安信这类厂商的开放接口拉取,定期刷新到本地 SQLite。
4. 报告生成与并发调度:多目标扫描时怎么保证不卡死、不误报
单台主机扫描没什么压力,问题出在扫描一个 C 段、甚至多个网段的时候。线程怎么分配、结果怎么汇总、报告怎么生成,这三件事在这套系统里有自己的实现方式,但都值得按实际场景再做调整。
4.1 多线程调度:ExecutorService 的线程数怎么定
Java 侧的多线程模型通常是这样:Main类里维护一个线程池,每个目标主机或每段端口范围是一个任务,丢给ExecutorService去跑。常见错误是把线程池设得很大,觉得快。实际上 Nmap 子进程本身就自带并发,你再在外面套一层高并发,机器 IO 和 CPU 全被吃满,反而互相拖慢。
一般我会采用两层并发控制方案:外面 Java 侧用Executors.newFixedThreadPool(4),里面的每个 Nmap 命令用--min-hostgroup 16 --max-hostgroup 32控制 Nmap 自身的主机并发。这样整体扫描 concurrency 是可配置线程数 × Nmap 并发组的乘积,而且外层线程不会因为 Nmap 输出量大而把内存打爆。
ExecutorService executor = Executors.newFixedThreadPool( Integer.parseInt(scanConfig.get("scan.threads")) ); for (String ip : targetList) { executor.submit(() -> { ScanResult result = nmapScanner.scan(ip); resultSink.save(result); }); } executor.shutdown(); executor.awaitTermination(30, TimeUnit.MINUTES);awaitTermination这里有个细节:如果等不到超时时间就返回,说明有的主机 Nmap 还在跑。一般我会在 awaitTermination 返回 false 之后强制执行shutdownNow(),然后把没跑完的 IP 记录下来,下次补扫,而不是干等。
4.2 结果去重与误报抑制:怎么避免 Nmap 输出里的重复项污染报告
Nmap 的 XML 输出里常常有重复条目,比如同一个端口被多次探测、同一服务被识别出多个版本,或者 open 和 filtered 两个状态同时出现。直接把 XML 解析结果丢进报告,会出现一个端口报三个漏洞的情况。一个简单有效的去重策略是:以IP + 端口 + 协议作为唯一键构建 Map,后写覆盖先写,只保留最后一次探测结果。
Map<String, VulnResult> dedupMap = new HashMap<>(); String key = result.getIp() + ":" + result.getPort() + "/" + result.getProtocol(); dedupMap.put(key, result);这个去重逻辑还有一层作用:如果漏洞库里有 CVE 条目同时覆盖 TCP 和 UDP 的相同端口,先去重再匹配,报告就干净很多。
4.3 报告生成:从 XML 到 HTML 的转换路径
报告生成这块,Java 侧常见做法是用 JAXP 解析 Nmap 输出,再配合简单的 XSLT 或模板引擎生成 HTML。这套系统用的是本地模板拼接,逻辑不复杂,但有个实际痛点:生成的 HTML 报告里中文字符在浏览器打开是乱码。
问题出在写文件时用了默认平台的编码,Windows 下就是 GBK,而 HTML 头部声明的是 UTF-8。你得在写文件流时明确指定编码:
try (Writer writer = new OutputStreamWriter( new FileOutputStream("report.html"), StandardCharsets.UTF_8)) { writer.write(renderReport(results)); }如果把每个漏洞的修复建议也生成到报告里,这份报告才能直接丢给开发去改。务必在生成前检查漏洞库中每一条 CVE 是否都带suggestion字段,没有的补一句通用建议,否则报告导出来,漏洞列表下方一片空白,负责人看完也不知道该干什么。
5. 避坑与常见问题排查:环境、编码、权限是老三样,重启解决不了
这套系统跑不起来的报错绝大多数不是代码问题,而是环境与调用姿势问题。下面这几条是我在实际使用、以及帮别人排查时遇到的高频故障,按照「现象 → 原因 → 解决」的顺序整理出来。
5.1 start.bat 双击后窗口一闪而过
现象:双击start.bat,CMD 窗口弹出后立刻关闭,什么错误信息都没留下。
原因:脚本中pause只会在执行到错误分支时触发,如果错误发生在java命令本身的解析阶段,例如-Dfile.encoding写错,会直接抛出 JVM 启动异常并结束进程,根本不会走到pause。另外,%JAVA_HOME%路径里如果包含空格或括号,批处理解析时会报「不是内部或外部命令」。
解决:在 bat 文件第一行后面加cmd /k,让窗口执行完不关闭,把真实错误信息显示出来。然后检查JAVA_HOME路径是否加了引号。标准写法推荐这样:
set "JAVA_HOME=C:\Program Files\Java\jdk1.8.0_202" "%JAVA_HOME%\bin\java" -version5.2 mysql-cis.audit 审计结果大量误报
现象:内网 MySQL 版本是 5.7.44,跑完 CIS 审计,报告里报了一堆在 8.0 才适用的检查项,比如validate_password组件检查,被标记为「不合规」。
原因:CIS 审计脚本分为多个 profile(MySQL 5.6、5.7、8.0 各有独立基线),包内的mysql-cis.audit如果写的是通用规则,脚本执行时没有自动探测目标数据库版本,就会把所有规则全量执行,误报在所难免。
解决:跑审计之前先确认目标 MySQL 的版本,然后手动裁剪审计文件。常见做法是把mysql-cis.audit里compliance条目中的条件判断改成带版本判断的形式,比如在 ACL 里检查到version < '8.0'就跳过对应的 rule。改完试跑,对比误报数,能降五成左右。
5.3 ndiff.bat 输出结果为空或全是乱码
现象:连续两次扫描后运行ndiff.bat,期望看到端口变化列表,结果要么是空的,要么输出的中文注释全是锟斤拷。
原因:Ndiff 的输入是 Nmap 的 XML 文件,如果两次扫描用的 XML 文件路径没写对,ndiff 自然读不出内容。乱码则是 Ndiff 读取 XML 时默认按系统编码读,而 XML 本身是 UTF-8,两个文件编码不一致导致解析失败。更隐蔽的问题是 Nmap 的 XML 输出里主机状态是down的主机,ndiff 默认会忽略,所以拿带-Pn的扫描结果做前后对比,会发现每次都是全量新增。
解决:两次扫描都要用-oX明确指定输出文件,且文件名用日期命名。ndiff.bat里把比较命令显式指定为:
ndiff --text scan_before.xml scan_after.xml > diff_result.txt如果还乱码,用findstr /r "state" diff_result.txt先看一眼原始 XML 是否正常,把 XML 头部改为<?xml version="1.0" encoding="UTF-8"?>再跑一次。
5.4 并发扫描时 CPU 占用很高但整体吞吐不涨
现象:把 Java 线程池从 4 调到 16,扫描一个小网段,总耗时没变化,CPU 倒是从 40% 飙到 95%。
原因:Nmap 自身的--min-hostgroup和--max-hostgroup参数控制了主机分组并发。Java 线程数翻倍后,同时并行执行的 Nmap 进程数也翻倍,每个进程内部又有自己的并发组,等于在单机上叠了两层超出容量的并发。IO 和 CPU 成了瓶颈,但扫描持续时间没减少,因为网络往返耗时没变。
解决:不要只调线程池,要按目标网络质量调 Nmap 侧参数。内网千兆环境,外层线程保持 4 到 6 个,Nmap 侧--max-hostgroup 32 --min-hostgroup 16。如果扫描对象是跨地域公网 IP,线程数减到 2,Nmap 侧也降为--max-hostgroup 8,不然重传率高,反而慢。
5.5 Java 版本不匹配导致类加载失败
现象:运行start.bat后 JVM 报UnsupportedClassVersionError: Main has been compiled by a more recent version of the Java Runtime。
原因:Main.class是别人编译好的产物,用的 JDK 版本高于本机 JDK 版本。常见组合是源码用 JDK 11 或 17 编译,而目标机器只装了 JRE 8。
解决:分两步。第一步先确认系统里有哪些 JDK,命令是java -version;第二步确认 class 的编译版本,命令是javap -verbose Main.class | findstr "major",如果是 61 表示 Java 17,55 是 Java 11,52 是 Java 8。对不上就去装对应版本的 JDK,然后把JAVA_HOME切到新装的路径。这一条是环境问题里最不费脑子但最耽误时间的,因为你还得确认装完 JDK 之后,那些依赖lib\*下的 jar 也要能被同一版本加载,版本不一致会在运行期才报NoSuchMethodError。
6. 进阶:把两次扫描结果作差分比对,秒级定位新增端口与新增漏洞
这套系统已经带了ndiff.bat,但它要求你手动先跑两次扫描、再运行ndiff.bat对比。我在实际维护一组测试环境时发现,手动执行这套流程在资产量小的时候还凑合,资产量一大就乱了:今天忘了存基线,明天忘了跑对比,漏一次就等于没扫。后来我把它接进了自己的定时任务里,做法很简单,在start.bat所在目录写一个增量扫描脚本,每周一凌晨跑一次全量扫描和一次基线对比:
#!/bin/bash BASE_DIR="/data/scan" mkdir -p "$BASE_DIR/baseline" "$BASE_DIR/current" nmap -sS -sV -O -p 1-65535 --open -Pn -oX "$BASE_DIR/current/weekly_$(date +%m%d).xml" 192.168.1.0/24 # 基线不存在则拉当前结果做基线 if [ ! -f "$BASE_DIR/baseline/weekly_base.xml" ]; then cp "$BASE_DIR/current/weekly_$(date +%m%d).xml" "$BASE_DIR/baseline/weekly_base.xml" echo "已建立基线:$(date)" else ndiff --text "$BASE_DIR/baseline/weekly_base.xml" "$BASE_DIR/current/weekly_$(date +%m%d).xml" > "$BASE_DIR/current/weekly_diff_$(date +%m%d).txt" echo "本周新增开放端口:" grep '^+ ' "$BASE_DIR/current/weekly_diff_$(date +%m%d).txt" | head -20 fi这段脚本解决了三个问题:一是基线自动建立,不用手动留存第一次扫描结果;二是每周自动生成差分报告,新增端口一眼就能看到;三是把 Nmap 的-oX路径统一成带日期的文件名,避免覆盖历史记录。
更大的价值在于联动思路:差分结果可以再喂给 Java 侧的告警模块,当weekly_diff里出现新增高危端口时,用 JavaMail 发一封邮件。实务里这个做法比扫一次发一次报告有效得多,因为每周变更点通常就是三五个,人眼扫一眼就知道是不是正常变更,不用在几百条漏洞列表里大海捞针。
我那套环境的测试内网里,MySQL 的端口从一开始就没变,但有一次差分报告明确提示3306/tcp新增开放,登录上去一看果然是个同事自己装了套 MySQL 实例做实验,权限没配好就暴露给了全内网。从那以后,我每次给新环境部署这套扫描系统,都强制把「首次扫描建基线 + 定期差分」这步先配好,而不是把扫描结果往 HTML 一导就收工。毕竟漏洞扫描的产品价值在变化的发现,不在静态的报告——这一点,希望帮到你少走点弯路。
本文还有配套的精品资源,点击获取