⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 juejin.cn/post/7458189303063035958 「月生_」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

序言

今天在工作的时候,领导突然安排和我说一个需求,就是根据一个 ip 和 端口去获取对应服务上对应端口的信息,当时主要是为了确定数据库的版本和型号,比如 MySQL、Oracle 这些数据库,我后面尝试发现其他端口也可以获取信息。

这个在公司里之前是通过 python 来写的,python 里面刚好有这个模块,但是 Java 没有,所以写这篇文章记录一下,帮助大家以后避免踩坑。

代码已经提交到了我的GitHub:

https://github.com/maoshengyzx/SpringBoot

nmap4j

nmap4j 是一个用于 Java 语言的 Nmap 端口扫描器的包装库,它允许 Java 开发者在自己的项目中方便地调用 Nmap 的功能进行网络扫描和探测。

GitHub地址:

https://github.com/narkisr/nmap4j

在运行这个代码之前,我们是需要下载 nmap 的可执行文件,地址为:

https://nmap.org/download.html#windows

这里下载第一个,下载后安装就行了。

代码说明

接下来和大家说一下 nmap4j 中的测试代码,这里面有个坑,我当时找了好久,代码在 test/org/nmap4j/Nmap4jTest.java

public class Nmap4jTest {
@Test
public void basicNmap4jUsageTest() {

try {
// 这里的路径要改为刚才 nmap 软件的安装路径
String path = "/usr/local" ;

Nmap4j nmap4j = new Nmap4j( path ) ;
// 这地方使用了 -oX 后面要跟文件名称
nmap4j.addFlags( "-sV -T5 -O -oX -" ) ;
nmap4j.includeHosts( "localhost" ) ;
nmap4j.execute() ;
if( !nmap4j.hasError() ) {

NMapRun nmapRun = nmap4j.getResult() ; // 这一行一定要注释掉,不然会一直报错
String output = nmap4j.getOutput() ; // 这一样代码意义也不大,我直接删掉了
if( output == null ) {
fail() ;
}
String errors = nmap4j.getExecutionResults().getErrors() ;
if (errors == null ) {
fail() ;
}
}
} catch (NMapInitializationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
fail() ;
} catch (NMapExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
fail() ;
}

}

}

参数说明:

目标选择参数:

  • -iL:从文件中读取扫描目标列表。例如,nmap -iL targets.txt,会从targets.txt文件中读取每行一个的 IP 地址或域名作为扫描目标。
  • -iR: 随机选择指定数量的主机进行扫描。如nmap -iR 100,会随机选取 100 个主机进行扫描。
  • --exclude <host1[,host2,...]>: 排除指定的主机或网络不进行扫描。例如,nmap 192.168.1.0/24 --exclude 192.168.1.100,192.168.1.200,将扫描192.168.1.0/24网段,但排除192.168.1.100和192.168.1.200这两台主机。
  • --excludefile <exclude_file>: 从文件中读取要排除的主机或网络列表。

扫描类型参数:

  • -sS: TCP SYN 扫描,也称为半开放扫描。它发送 SYN 包到目标端口,如果收到 SYN/ACK 响应,就表示端口开放;如果收到 RST 响应,则表示端口关闭。这种扫描方式速度快,且不容易被目标系统记录,相对隐蔽,例如nmap -sS 192.168.1.100。
  • -sT: TCP 连接扫描,通过完整的 TCP 三次握手来确定端口是否开放。这种扫描方式最准确,但也最容易被检测到,如nmap -sT 192.168.1.100。
  • -sU: UDP 扫描,用于检测目标主机上的 UDP 端口是否开放。因为 UDP 是无连接协议,所以判断端口状态相对复杂,nmap -sU 192.168.1.100可对指定主机进行 UDP 扫描。
  • -sF、-sX、-sN: 分别是 FIN 扫描、XMAS 扫描和 NULL 扫描。这些扫描方式通过发送特殊标志位的 TCP 包来判断端口状态,常用于绕过一些简单的防火墙检测。

端口指定参数:

  • -p: 指定要扫描的端口范围。可以是单个端口,如-p 80;也可以是多个端口,如-p 80,443,8080;还可以是端口范围,如-p 1-1000表示扫描 1 到 1000 号端口。
  • --top-ports: 扫描最常用的指定数量的端口。例如,nmap --top-ports 100 192.168.1.100会扫描目标主机上最常用的 100 个端口。
  • -F: 快速扫描模式,只扫描一些常见的端口,相当于-p 1-1024加上一些其他常用端口。

服务探测参数:

  • -sV: 启用服务版本探测,尝试确定目标主机上运行的服务及其版本信息。例如,nmap -sV 192.168.1.100可以扫描出目标主机开放端口上运行的服务名称和版本号。
  • --version-intensity: 设置服务版本探测的强度,级别越高,探测越全面,但耗时也越长,取值范围是 0 到 9。

操作系统探测参数:

  • -O: 启用操作系统探测,尝试识别目标主机的操作系统类型和版本。如nmap -O 192.168.1.100。
  • --osscan-limit: 限制操作系统探测只对可能的目标进行,这样可以加快扫描速度,但可能会降低准确性。
  • --osscan-guess: 更积极地猜测操作系统类型,当 Nmap 不确定时会给出更宽泛的猜测结果。

输出参数:

  • -oN: 将扫描结果以正常格式保存到指定文件。例如,nmap -oN scan_results.txt 192.168.1.100会把扫描结果保存到scan_results.txt文件中。
  • -oX: 将扫描结果以 XML 格式保存到指定文件,方便后续使用脚本或其他工具进行解析和处理。
  • -oG: 将扫描结果以 Grep 格式保存,这种格式便于使用文本处理工具进行分析。
  • -oA: 以多种格式(包括正常、XML 和 Grep 格式)保存扫描结果,文件名为指定的基本名称加上相应的扩展名。
  • -v: 详细输出模式,显示更多的扫描过程信息,如发送的数据包、收到的响应等。使用多个v可以增加详细程度,如-vv、-vvv。

这里给大家看一下改造后的代码:

/**
* 使用 nmap4j 工具进行扫描
*
* @param ip 目标 ip
* @param ports 目标端口
* @return 端口信息列表
*/
@RequestMapping("/querydb")
public List<NmapPortInfo> querydb(@RequestParam(value = "ip") String ip, @RequestParam("ports") List<String> ports) {
ArrayList<NmapPortInfo> portInfos = new ArrayList<>();
// 1.拼接端口
String portStr = StrUtil.join(",", ports);
//2. 指定 nmap 路径
String path = "D:/StudyApps/nmap";
String fileName = "temp_result.xml";

Nmap4j nmap4j = new Nmap4j(path);

//3.读取端口耗时较长,可以使用异步
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
nmap4j.addFlags("-sV -p " + portStr + " -T5 -O -oX " + fileName);
nmap4j.includeHosts(ip);
try {
nmap4j.execute();
} catch (Exception e) {
throw new RuntimeException(e);
}
}, threadPoolExecutor);

future.join();

//4. 获取端口信息
return getPortInfo(portInfos, fileName);
}
/**
* 获取 ip + 端口信息,封装为集合返回前端
*
* @param portInfos 返回前端集合
* @param fileName 临时的 xml 文件
* @return 信息列表
*/
@SneakyThrows
private List<NmapPortInfo> getPortInfo(List<NmapPortInfo> portInfos, String fileName) {
// 获取项目所在路径
String projectPath = System.getProperty("user.dir");
// 拼接文件路径
String filePath = projectPath + FileUtil.FILE_SEPARATOR + fileName;
log.info("文件路径:{}", filePath);

// nmap 返回 xml 格式固定,使用 dom4j 解析
SAXReader reader = new SAXReader();
org.dom4j.Document document = reader.read(FileUtil.file(filePath));
org.dom4j.Element rootElement = document.getRootElement();
org.dom4j.Element element = rootElement.element("host");

org.dom4j.Element xmlPorts = element.element("ports");

List<org.dom4j.Element> port = xmlPorts.elements("port");
for (org.dom4j.Element port1 : port) {
Element service = port1.element("service");
String product = service.attributeValue("product");
String version = service.attributeValue("version");
NmapPortInfo nmapPortInfo = new NmapPortInfo(product, version);
portInfos.add(nmapPortInfo);
}

// 删除临时文件
FileUtil.del(filePath);
return portInfos;
}
}

我这里就是没有去按照官网上的写法,我的思路是文件已经下载了我直接去读取 xml 文件解决会更快,这里是使用 dom4j 来读取的 xml 文件。代码就这么多,最后请求是可以获取到数据的:

补充

由于这段代码是在windows上运行的,而在实际的环境中项目都会部署到 Linux 环境汇总,所以我不得不在 Linux 上去运行调试这段代码。代码如下:

/**
* 使用 nmap4j 工具进行扫描, linux系统
*
* @param ip 目标 ip
* @param ports 目标端口
* @return 端口信息列表
*/
@GetMapping("/linux/querydb")
@SneakyThrows
public List<NmapPortInfo> linuxQuerydb(@RequestParam(value = "ip") String ip, @RequestParam("ports") List<String> ports) {
ArrayList<NmapPortInfo> portInfos = new ArrayList<>();
// 1.拼接端口
String portStr = StrUtil.join(",", ports);
String fileName = "temp_result.xml";
//2. linux namp 命令
String nmapCommand = "nmap -sV -p " + portStr + " -T5 -O -oX " + fileName + " " + ip;

//3. 读取端口耗时较长,可以使用异步
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
Process nampProcess = null;
try {
// 3. 运行命令
nampProcess = Runtime.getRuntime().exec(nmapCommand);
// 4. 等待命令执行完成
nampProcess.waitFor();
} catch (Exception e) {
throw new RuntimeException(e);
}
}, threadPoolExecutor);

future.join();

// 5. 获取端口信息
return getPortInfo(portInfos, fileName);
}

这里的初步思路是直接调用 Linux 的命令执行 nmap (Linux系统中必须下载 nmap)。至于下载这里就不多说了,大家只需要注意下载的版本最好也为 7.95,其他版本下载的 xml 文件有问题,无法解析。

总结

忘记和大家说了,nmap4j 在maven仓库是搜不到的,所以只通过 jar 包的方式来引入,地址为:

https://master.dl.sourceforge.net/project/nmap4j/1.1.0/org.nmap4j-1.1.0-RELEASE.zip?viasf=1

文章目录
  1. 1. 序言
  2. 2. nmap4j
    1. 2.1. 代码说明
      1. 2.1.1. 参数说明:
  3. 3. 补充
  4. 4. 总结