基于Java与Nmap的漏洞扫描系统:从端口探测到安全报告
简介一份基于 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.classJava 字节码程序入口负责初始化扫描框架封装 Nmap 命令调用与结果回传DisplayForm.classJava 字节码图形化展示层通常承载扫描进度、结果列表和报告预览nmap_service.cC 源码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 的 NSENmap 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 2nul 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.encodingUTF-8 rem 启动主类 Main %JAVA_HOME%\bin\java %JAVA_OPTS% -cp .;lib\* Main endlocal这段脚本里三板斧先验证环境变量再验证 Nmap 可执行文件最后带参启动。-Xmx1024m这个参数在扫描大型 C 段网段时很关键如果内存给太小结果解析阶段会出现OutOfMemoryError而且是在扫描快要结束的时候才崩非常憋屈。-Dfile.encodingUTF-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 扫描阶段要做的第一件事是判定目标端口是否开放。这里有个策略选择问题-sSSYN 半开扫描和-sTTCP 全连接扫描效果完全不同。半开扫描不建立完整 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后写覆盖先写只保留最后一次探测结果。MapString, 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.batCMD 窗口弹出后立刻关闭什么错误信息都没留下。原因脚本中pause只会在执行到错误分支时触发如果错误发生在java命令本身的解析阶段例如-Dfile.encoding写错会直接抛出 JVM 启动异常并结束进程根本不会走到pause。另外%JAVA_HOME%路径里如果包含空格或括号批处理解析时会报「不是内部或外部命令」。解决在 bat 文件第一行后面加cmd /k让窗口执行完不关闭把真实错误信息显示出来。然后检查JAVA_HOME路径是否加了引号。标准写法推荐这样set JAVA_HOMEC:\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 审计脚本分为多个 profileMySQL 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 version1.0 encodingUTF-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线程数减到 2Nmap 侧也降为--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 1755 是 Java 1152 是 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 一导就收工。毕竟漏洞扫描的产品价值在变化的发现不在静态的报告——这一点希望帮到你少走点弯路。本文还有配套的精品资源点击获取