SQL 融入 Nmap:强大与便捷
最近,我与一位经常需要对其自身网络进行端口扫描以了解漏洞趋势的人交换了电子邮件。执行此任务的首选端口扫描工具是 Nmap,但管理来自 Nmap 的数据完全是另一回事。几周后,允许 Nmap 将结果直接记录到 MySQL 的补丁已准备就绪。虽然 Nmap 支持机器可解析格式以及 XML 输出,但直接记录到 SQL 数据库的能力远远超过 XML 甚至机器可解析输出。首先,nmapsql 不需要额外的 shell 步骤来将输出馈送到后端。
nmapsql 是直接应用于 Fyodor 备受尊敬的 Nmap v3.48 端口扫描工具的补丁(在撰写本文时,Nmap v3.50 刚刚发布;nmapsql v3.50 的更新版本可从网站获得)。它添加了 MySQL 支持,但它不仅仅是添加结果;它还执行目标标记、扫描器标记和简单的趋势分析。一旦数据被捕获到 SQL 数据库中,就可以执行一系列全新的任务。 nmapsql 可以从 sourceforge.net/projects/nmapsql 下载。目前,它依赖于 MySQL 的客户端接口进行数据操作。
由于安全管理员不一定是数据库专家,因此 nmapsql 的设计宗旨是易于使用。它非常简单,以至于可以从单个表中获得网络扫描中可能需要的大部分信息。简单也是 IP 地址以纯文本形式存储而不是使用 inet_aton() 表示法的原因。我意识到文本操作的性能损失,但重点是展示小数据集的便利性。目标标签、运行时和扫描器 ID 用于在性能至关重要的大型数据集中进行数字搜索。
在本文中,我们首先专注于运行启用 SQL 的扫描,以建立网络上开放端口和活动目标的基线。稍后,我们将查看 SQL 中捕获的数据,并找到比较结果的方法。
nmapsql 首先读取有效用户主目录中的 ~/nmpsql.rc 文件。因此,如果您使用su在运行 nmapsql 之前获取 root 权限,则会读取 ~root/nmapsql.rc。此时,仅从 nmapsql.rc 读取四个项目,每个项目单独一行,并且采用 item=value 格式,这在许多其他实用程序中很常见。这些项目是 server=localhost、db=nmaplog、user=nmap 和 passwd=scanamanga。
server 是运行 MySQL 的主机的 DNS 名称,db 是该服务器上数据库的名称。 user 和 password 项目用于连接到数据库,并且列出的用户必须至少具有对数据库的 SELECT、INSERT 和 UPDATE 权限。
在命令行中,nmapsql 为 Nmap 已提供的选项引入了四个新选项:--mysql、--runid、--targetid 和 --scannerid。当在不使用任何这些选项的情况下执行 nmapsql 二进制文件时,它的行为与正常的 Nmap 完全相同。这些选项都不会干扰 Nmap 现有的输出能力,因此完全可以同时记录到 SQL 并从同一次扫描生成机器可解析的输出。
--mysql 选项,在命令行上不带任何其他 nmapsql 选项的情况下,会触发 MySQL 日志记录,所有标签和 ID 都是自动分配的。所有其他 nmapsql 选项都会自动假定 --mysql。自动分配始终选择相应表中的最大可用值并递增 1。
扫描器 ID 功能,由 --scanner-id xxx 选项启动,其中 xxx 是 ID 值,旨在用于部署了多个扫描器的情况,可能在多子网环境中。扫描器 ID 与运行时 ID 一起存储在 portstat 表中,以允许按扫描主机分隔结果集。例如,可以使用如下查询轻松分隔扫描器十的结果
mysql> select * from portstat -> where scannerid = 10 and runid = 100;
--run-id xxx 选项用于为当前的 nmapsql 运行指定特定的 ID。如果未指定此选项,则使用系统生成的 ID。如果指定的 runid 已经存在于数据库中,则会重复使用它。此功能允许将多次扫描的结果方便地分组到单个 runid 下。
运行时 ID 及其相关信息存储在 runlist 表中。有关所用表的摘要,请参阅“nmaplog 使用的表”侧边栏。一些运行时信息会在扫描结束时更新,包括命令行上指定的可能目标总数和找到的活动目标总数。同样,扫描器 ID 和相关信息会转到 scanners 表。
nmaplog 使用的表
此时,可以在 nmaplog 数据库中找到八个表。相关表包括
TARGETS—包含有关目标主机的信息,包括目标 ID、IP 地址、已解析的主机名和 Nmap 猜测的操作系统。 hostid 字段链接到 portstat 表。
SCANNERS—包含有关执行 nmapsql 的主机的信息。 scan_id 字段是我们链接到 portstat 表的链接。
RUNLIST—包含有关每次 Nmap 调用 (包括从中运行 Nmap 的主机) 的用户 ID、日期和时间信息。用户名和用户 ID 来自 /etc/passwd。 scanner_id 字段与 scanners.scan_id 相关联。
PORTSTAT—包含端口扫描结果。 nmaplog 报告的每个端口都会被记录下来,以及状态(打开/关闭/已过滤)。几乎所有其他表都通过 ID 字段链接到此表。
HOSTSSTAT—包含有关每次 nmapsql 运行的目标主机的基本统计信息,例如扫描的端口总数和找到的开放端口数。
每个扫描的目标也被分配一个标签,信息存储在 targets 表中。与 runlist 一样,targets 表中的行分两个阶段填充。第一阶段捕获 IP 地址和主机名(如果可解析),第二阶段填充 os_guessed 列。目前,未存储无法识别的 OS 的指纹信息,但将来可能会存储。 targets 表中永远不会创建重复项。根据我的经验,您可能拥有重复 IP 子网的唯一情况是当您从一个客户转移到另一个客户时。在这种情况下,应该为每个客户使用不同的数据库。
目标 ID 目前未使用,但您可以在命令行上为任何目标指定自己的目标 ID。如果指定的 ID 存在,则会忽略它,并使用系统生成的 ID 来确保唯一性。如果命令行上 --target-id 之后的目标 ID 值在 targets 表中不存在,则会将其分配给当前目标的 IP 地址。如果目标规范是针对多个系统,则第一个目标具有指定的目标 ID,随后的目标被分配递增的 ID。
nmapsql 记录执行的日期和时间、执行 Nmap 的用户、运行 Nmap 的主机以及执行的标识号。最后两项允许在大型环境中使用 nmapsql,并构成扫描之间比较的基础。 runid 或运行时 ID 在该数据集中始终是唯一的。如果目标规范保持不变,则仅 runid 就可以区分两次扫描的结果。但是也可以使用 --run-id 命令行选项将多次扫描的结果分组到单个 runid 下。例如,考虑以下 nmapsql 调用
$ nmap -A --mysql --runid 100 192.168.10.1/24
此命令使用 --mysql 选项启用日志记录功能启动 Nmap,为当前 runid 分配 100,并扫描 192.168.10.1/24 网络。如果这是 nmapsql 的首次调用,这将为网络建立基线,所有后续运行都可以与之进行比较。 nmapsql 还会自动为运行它的主机(在本例中为 192.168.10.44)创建一个条目,并在 scanners 表中为其分配一个 scanner_id。此运行的 Nmap 的部分控制台输出如列表 1 所示。
列表 1. 来自 Nmap 的部分输出
Starting nmap 3.48 ( http://www.insecure.org/nmap/ ) at 2003-12-14 10:00 SGT Insufficient responses for TCP sequencing (1), OS detection may be less accurate Interesting ports on 192.168.10.0: (The 1656 ports scanned but not shown below are in state: closed) PORT STATE SERVICE VERSION 80/tcp open http? Device type: print server Running: Linksys embedded OS details: Linksys EtherFast print server Interesting ports on wap.hasnains.com (192.168.10.1): (The 1654 ports scanned but not shown below are in state: closed) PORT STATE SERVICE VERSION 21/tcp open ftp? 23/tcp open telnet? 80/tcp open http Device type: broadband router Running: Zyxel ZyNOS OS details: ZyXEL Prestige 700/Netgear MA314 broadband router
本示例中的目标规范是整个 C 类子网。 nmapsql 自动为网络中的每个活动主机分配唯一的目标 ID,并将其他信息存储在 hoststats 表中。仅此表就可以成为简易的端口扫描结果比较工具。
让我们快速看一下记录的内容。为此,我们登录到 MySQL 客户端并连接到 nmapsql.rc 文件中列出的数据库。然后我们发出查询
$ mysql nmaplog -p mysql> select target_ip, d, t, port, protocol, -> state, runid from portstat -> order by target_ip, d, t ;
此查询将生成列表 2 中显示的表。它提供了一个按目标 IP、日期和时间排序的漂亮列表。请注意,runid 列对于所有行都为 100,如命令行中所述。
列表 2. 单次扫描的结果
+---------------+------------+----------+--------+----------+--------+-------+ | target_ip | d | t | port | protocol | state | runid | +---------------+------------+----------+--------+----------+--------+-------+ | 192.168.10.0 | 2003-12-14 | 10:00:37 | 80 | tcp | open | 100 | | 192.168.10.1 | 2003-12-14 | 10:00:37 | 21 | tcp | open | 100 | | 192.168.10.1 | 2003-12-14 | 10:00:37 | 23 | tcp | open | 100 | | 192.168.10.1 | 2003-12-14 | 10:00:37 | 80 | tcp | open | 100 | | 192.168.10.44 | 2003-12-14 | 10:00:37 | 22 | tcp | open | 100 | | 192.168.10.44 | 2003-12-14 | 10:00:37 | 111 | tcp | open | 100 | | 192.168.10.44 | 2003-12-14 | 10:00:37 | 3306 | tcp | open | 100 | | 192.168.10.44 | 2003-12-14 | 10:00:37 | 6000 | tcp | open | 100 | | 192.168.10.64 | 2003-12-14 | 10:00:37 | 135 | tcp | open | 100 | | 192.168.10.64 | 2003-12-14 | 10:00:37 | 139 | tcp | open | 100 | | 192.168.10.64 | 2003-12-14 | 10:00:37 | 445 | tcp | open | 100 | | 192.168.10.64 | 2003-12-14 | 10:00:37 | 1024 | tcp | open | 100 | | 192.168.10.64 | 2003-12-14 | 10:00:37 | 1025 | tcp | open | 100 | | 192.168.10.64 | 2003-12-14 | 10:00:37 | 1031 | tcp | open | 100 | | 192.168.10.64 | 2003-12-14 | 10:00:37 | 5000 | tcp | open | 100 | | 192.168.10.64 | 2003-12-14 | 10:00:37 | 5101 | tcp | open | 100 | | 192.168.10.64 | 2003-12-14 | 10:00:37 | 6000 | tcp | open | 100 | | 192.168.10.65 | 2003-12-14 | 10:00:37 | 135 | tcp | open | 100 | | 192.168.10.65 | 2003-12-14 | 10:00:37 | 139 | tcp | open | 100 | | 192.168.10.65 | 2003-12-14 | 10:00:37 | 445 | tcp | open | 100 | | 192.168.10.65 | 2003-12-14 | 10:00:37 | 1025 | tcp | open | 100 | | 192.168.10.65 | 2003-12-14 | 10:00:37 | 5000 | tcp | open | 100 | +---------------+------------+----------+--------+----------+--------+-------+
当我们使用以下查询时,我们得到列表 3,显示目标 192.168.10.44 的开放端口信息
mysql> select target_ip, d, t, port, protocol, -> state, runid from portstat -> where target_ip = '192.168.10.44' -> order by d, t;
列表 3. 单个主机的扫描结果
+---------------+------------+----------+--------+----------+--------+-------+ | target_ip | d | t | port | protocol | state | runid | +---------------+------------+----------+--------+----------+--------+-------+ | 192.168.10.44 | 2003-12-14 | 10:00:37 | 22 | tcp | open | 100 | | 192.168.10.44 | 2003-12-14 | 10:00:37 | 111 | tcp | open | 100 | | 192.168.10.44 | 2003-12-14 | 10:00:37 | 3306 | tcp | open | 100 | | 192.168.10.44 | 2003-12-14 | 10:00:37 | 6000 | tcp | open | 100 | +---------------+------------+----------+--------+----------+--------+-------+
如果您将四行输出与列表 1 中 192.168.10.44 的部分进行匹配,您可以立即看到它们之间的关系。如此处所示,仅 portstat 表就可以提供来自 Nmap 的所有端口扫描信息。当然,如果您已完成多次扫描,则上述查询会显示在所有扫描中找到的 192.168.10.44 的所有结果。
假设在两天后、一周后或一个月后,您再次扫描您的网络,并希望直观地比较两个结果。快速浏览 runlist 表显示 runid 102 对应于首次扫描后两天。掌握了该信息后,您输入查询
mysql> select target_ip, d,t,port, protocol, -> state from portstat -> where target_ip = '192.168.10.44' -> and runid = 102 order by d,t,port;
您可以轻松地比较列表 4 和列表 5 的结果,以找出差异。显然,程序可以比较这两个结果集并为您总结差异。
列表 4. 两天后扫描的 192.168.10.44
+---------------+------------+----------+--------+----------+--------+ | target_ip | d | t | port | protocol | state | +---------------+------------+----------+--------+----------+--------+ | 192.168.10.44 | 2003-12-16 | 00:47:16 | 22 | tcp | open | | 192.168.10.44 | 2003-12-16 | 00:47:16 | 111 | tcp | open | | 192.168.10.44 | 2003-12-16 | 00:47:16 | 3306 | tcp | open | | 192.168.10.44 | 2003-12-16 | 00:47:16 | 6000 | tcp | open | +---------------+------------+----------+--------+----------+--------+
列表 5. 主机的组合结果
+------------+----------+---------------+----------------------+-----------------------------+--------+----------+---------+--------+-------------------------------+ | d | t | ip | host | os_guessed | port | protocol | service | state | fullversion | +------------+----------+---------------+----------------------+-----------------------------+--------+----------+---------+--------+-------------------------------+ | 2003-12-14 | 10:00:37 | 192.168.10.44 | ophelia.hasnains.com | Linux Kernel 2.4.0 - 2.5.20 | 22 | tcp | ssh | open | OpenSSH 3.5p1(protocol 1.99) | | 2003-12-14 | 10:00:37 | 192.168.10.44 | ophelia.hasnains.com | Linux Kernel 2.4.0 - 2.5.20 | 111 | tcp | rpcbind | open | 2 (rpc #100000) | | 2003-12-14 | 10:00:37 | 192.168.10.44 | ophelia.hasnains.com | Linux Kernel 2.4.0 - 2.5.20 | 3306 | tcp | mysql | open | MySQL4.0.16-standard-log | | 2003-12-14 | 10:00:37 | 192.168.10.44 | ophelia.hasnains.com | Linux Kernel 2.4.0 - 2.5.20 | 6000 | tcp | X11 | open | (access denied) | +------------+----------+---------------+----------------------+-----------------------------+--------+----------+---------+--------+-------------------------------+
回到简易的端口扫描结果,hoststat 表包含每个活动主机的信息。它在 open_ports 列中保留开放端口的简单计数。要查找目标主机的开放端口信息,我们查询
mysql> select ip,d,t,open_ports, ports_scanned, -> runid from hoststats where order by ip, d,t;
以接收单行输出。(我们添加了 order by 子句以供将来使用。)open_ports 列与日期/时间和 runid 列一起查看时,可以粗略地描绘出一段时间内开放端口的趋势。
targets 表捕获它遇到的每个目标的信息,每个唯一的 IP 地址一行。这是唯一捕获主机名(如果可解析)和 Nmap 猜测的操作系统的地方。让我们找出它对我们的目标的了解
mysql> select * from targets -> where ip = '192.168.10.44';
请注意,OS_guessed 字段现在包含 Linux Kernel 2.4.0-2.5.20,并且 hostname 列设置为 ophelia.hasnains.com(我喜欢莎士比亚的悲剧女主角)。
现在我们基本上拥有了所有的位和片段,让我们构建一个单独的查询,将我们目标主机的所有信息放在一个位置
mysql> select r.runid, r.d, r.t, t.ip, t.host, -> t.os_guessed, p.port, p.protocol, p.service, -> p.state, p.fullversion from runlist r, -> targets t, portstat p -> where r.runid = 100 and p.target_ip = t.ip -> and p.runid = r.runid -> order by r.runid, r.d, r.t, t.ip;
由于篇幅所限,我们不显示输出,但您可以自己尝试一下。我们可以使用报表编写器按目标对结果进行分组。对于更精美的输出,我们需要使用更强大的工具,例如 PHP 或 Perl。最有用的报告之一是识别每个目标的开放端口的变化。例如,假设我们的目标关闭了 111/TCP 但打开了 23/TCP。在这种情况下,即使详细信息已更改,hoststats 中的 open_ports 列仍将显示四个端口。但是,自定义程序可以轻松地找出差异以进行报告。
最常见的查询将是找出给定目标的哪些端口是开放的,这可以通过以下方式完成
mysql> select d, t, port, protocol, state, -> fullversion from portstat -> where target_ip = '192.168.10.44' -> order by d,t,port;
另一个常见查询是给定端口在过去是否在目标上打开——“两周前 192.168.10.44 上是否打开了 SSH?” 只要安装了 nmapsql,假设 nmapsql 从 crontab 例行运行,答案将在以下查询中
mysql> select d, t,target_ip, port,protocol, -> service, state, fullversion from portstat -> where port = 22 and protocol = "tcp" -> and state = "open" -> d = date_sub( curdate(), interval 14 day) -> order by d, runid, target_ip ;
显然,您可能在给定日期运行多个 nmapsql 实例,因此使用了 order by 子句。如果您使用第三方工具(例如 PHP 或 Perl)来生成结果集,您可以查阅 runlist 表以找到您需要的确切时间范围的 runid,并查询该 runid 的目标结果。
另一个有用的查询是识别给定网络中具有给定开放端口的目标总数——“192.168.10/24 子网中有多少主机打开了 80/TCP?” 此查询将产生以下结果
mysql> select runid, d, t, target_ip, port, -> protocol, state from portstats -> where port = 80 and protocol = 'tcp' -> and state = 'open' -> and target_ip like '192.168.10.%';
文本匹配不太适合子网匹配,但您应该理解大致思路。
在许多情况下,例如当顾问从一个网络转到另一个网络时,希望能够更改数据库的名称,可能更改为客户的名称,以便来自多个位置的数据不会混在一起。在撰写本文时,执行此操作的方法是更新 nmaplog 使用的 ~/nmapsql.rc 文件中的 db=nmaplog 项,以获取数据库访问信息。
为了更改正在使用的数据库,请将 ~/nmapsql.rc 中的 nmaplog 替换为适当的名称,然后确保 ~/nmapsql.rc 中指定的用户具有对该数据库的权限。然后,将数据库架构加载到新数据库中。假设新数据库名为 newnmap,则以下行将加载架构
$ mysql newnmap < nmaplog.sql
但是,我不建议使用不同的数据库。将数据卸载到磁盘文件,然后将空白架构加载到 nmaplog 数据库中要容易得多。以下行将完成此操作
$ mysqldump nmaplog > newnmap.sql $ mysql nmaplog < nmaplog.sql
根据您的数据库权限设置方式,您可能需要为上述命令指定 MySQL 用户名和密码才能使其工作。
当在少量目标上不频繁运行时,nmapsql 的实用性很难体现。在具有多个子网和数十个目标的大型环境中,nmapsql 真正大放异彩。当然,最简单的部署是 nmapsql 和 MySQL 服务器驻留在同一主机上,例如顾问从一个网络携带到另一个网络的笔记本电脑。由于大多数网络都受到防火墙保护并使用 RFC 1918 寻址,因此在漫游环境中的单台笔记本电脑上,targets 表中极有可能出现重复的 IP 地址。在这些情况下,您应该卸载数据并为每个新环境使用新的数据库。
nmapsql 适用于其他部署方案:中型环境,其中来自不同子网的多个扫描器记录回单个 MySQL 服务器;以及大型环境,其中多个独立的系统(同一框上的 MySQL 和 nmapsql)执行本地扫描和日志记录。在这两种环境中,重复的 RFC 1918 地址不太可能出现。但是,由于本地扫描/日志记录与收集到中央服务器之间存在延迟,因此数据不是实时的。在这两种情况下,扫描器 ID 都可用于分离数据。
Hasnain Atique (hatique@hasnains.com) 与他的妻子和三岁的女儿住在阳光明媚的新加坡。当他不和女儿一起看 哈利·波特 时,他试图成为 ping 的领主,偶尔会成功。