识别网络中的所有公共服务

Nmap(“Network Mapper”)是一个免费的开源实用程序,用于网络发现和安全审计。

许多系统和网络管理员还将它用于网络清单、管理服务升级计划以及监控主机或服务正常运行时间等任务。还可以使用它来绕过弱保护、查找隐藏或配置错误的服务,或者只是为了让您更好地了解网络的工作原理。

IDS 通常会查找异常的网络模式,如果它看到机器在许多主机上快速连续地打开和关闭端口,则被视为端口扫描攻击。基于此,你不应该随意在网络上启动端口扫描,因为 Nmap 不是 100% 隐身的。

运行 Nmap 和 OS 漏洞识别需要什么?

Nmap 需要提升的权限才能使用原始套接字进行操作系统漏洞识别和扫描。您需要以 root 或 su “do” (SUDO) 身份运行命令以提升您的权限。

执行此操作的 SUDO 规则类似于以下内容(文件 /etc/sudoers):

%wheel    ALL=(ALL)    NOPASSWD: ALL

这意味着 'wheel' 组中的任何人都可以以 root 身份运行命令:

(2600) [test@vp5 2600]$ grep wheel /etc/group
wheel:x:10:josevnz,services

# 为了确认我们可以以root身份运行命令
(2600) [test@vp5 2600]$ sudo -l
Matching Defaults entries for josevnz on dmaf5:
    !visiblepw, always_set_home, match_group_by_gid, always_query_group_plugin, env_reset, env_keep="COLORS DISPLAY HOSTNAME HISTSIZE KDEDIR LS_COLORS",
    env_keep+="MAIL QTDIR USERNAME LANG LC_ADDRESS LC_CTYPE", env_keep+="LC_COLLATE LC_IDENTIFICATION LC_MEASUREMENT LC_MESSAGES", env_keep+="LC_MONETARY
    LC_NAME LC_NUMERIC LC_PAPER LC_TELEPHONE", env_keep+="LC_TIME LC_ALL LANGUAGE LINGUAS _XKB_CHARSET XAUTHORITY",
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/var/lib/snapd/snap/bin

User josevnz may run the following commands on dmaf5:
    (ALL) NOPASSWD: ALL

接下来,我们将对本地网络进行快速扫描(在本例中为 192.168.1.0/24)。

使用 -v(详细)标志来获取一些进度反馈,同时扫描所有端口,并进行操作系统漏洞识别 (-O)。

将 Nmap 运行的过程保存到一个 XML 文件 (-oX) 中,如果它被中断 (--resume),Nmap 可以使用它来恢复执行:

# 如果扫描中断: nmap --resume $HOME/home_scan.xml
[test@vp5 docs]$ sudo nmap -v -n -p- -sT -sV -O --osscan-limit --max-os-tries 1 -oX $HOME/home_scan.xml 192.168.1.0/24
Starting Nmap 7.80 ( https://nmap.org ) at 2021-12-30 16:35 EST
NSE: Loaded 45 scripts for scanning.
Initiating ARP Ping Scan at 16:35
Scanning 254 hosts [1 port/host]
...
# 一段时间后...
Network Distance: 1 hop
TCP Sequence Prediction: Difficulty=265 (Good luck!)
IP ID Sequence Generation: All zeros

Nmap scan report for 192.168.1.20
Host is up (0.0097s latency).
Not shown: 65530 closed ports
PORT      STATE    SERVICE      VERSION
36184/tcp filtered unknown
37309/tcp filtered unknown
49323/tcp open     unknown
49376/tcp filtered unknown
62078/tcp open     iphone-sync?
MAC Address: 9E:90:75:3A:D7:XX (Unknown)
...

生成的 XML 格式文件非常详细:

<host starttime="1640901327" endtime="1640902555"><status state="up" reason="arp-response" reason_ttl="0"/>
<address addr="192.168.1.1" addrtype="ipv4"/>
<address addr="38:5B:5E:1D:52:99" addrtype="mac"/>
<hostnames>
</hostnames>
<ports><extraports state="closed" count="65523">
<extrareasons reason="conn-refused" count="65523"/>
</extraports>
<port protocol="tcp" portid="139"><state state="open" reason="syn-ack" reason_ttl="0"/><service name="netbios-ssn" product="Samba smbd" version="3.X - 4.X" extrainfo="workgroup: ZZZ" method="probed" conf="10"><cpe>cpe:/a:samba:samba</cpe></service></port>
    ...

解析多种格式的数据是 Python 的优势之一。使用 lxml 为所有未“关闭”的端口提取数据并对其进行规范化:

class OutputParser:
    """
    Parse Nmap raw XML output
    """

    @staticmethod
    def parse_nmap_xml(xml: str) -> (str, Any):
        """
        解析XML并返回扫描端口的详细信息
        @param xml:
        @return: tuple nmaps arguments, port details
        """
        parsed_data = []
        root = ElementTree.fromstring(xml)
        nmap_args = root.attrib['args']
        for host in root.findall('host'):
            for address in host.findall('address'):
                curr_address = address.attrib['addr']
                data = {
                    'address': curr_address,
                    'ports': []
                }
                states = host.findall('ports/port/state')
                ports = host.findall('ports/port')
                for i in range(len(ports)):
                    if states[i].attrib['state'] == 'closed':
                        continue  # Skip closed ports
                    port_id = ports[i].attrib['portid']
                    protocol = ports[i].attrib['protocol']
                    services = ports[i].findall('service')
                    cpe_list = []
                    service_name = ""
                    service_product = ""
                    service_version = ""
                    for service in services:
                        for key in ['name', 'product', 'version']:
                            if key in service.attrib:
                                if key == 'name':
                                    service_name = service.attrib['name']
                                elif key == 'product':
                                    service_product = service.attrib['product']
                                elif key == 'version':
                                    service_version = service.attrib['version']
                        cpes = service.findall('cpe')
                        for cpe in cpes:
                            cpe_list.append(cpe.text)
                        data['ports'].append({
                            'port_id': port_id,
                            'protocol': protocol,
                            'service_name': service_name,
                            'service_product': service_product,
                            'service_version': service_version,
                            'cpes': cpe_list
                        })
                        parsed_data.append(data)
        return nmap_args, parsed_data

一旦收集到数据,我们就可以在 Rich 的帮助下在终端中创建一个漂亮的表。

该表包含以下列:

  • Internet 协议 (IP) 地址
  • 协议:在此脚本中,它将始终是传输控制协议 (TCP)
  • 端口 ID:服务运行的端口号
  • 服务:像安全外壳 (SSH) 这样的网络服务
  • 通用平台枚举 (CPE):是信息技术系统、软件和软件包的结构化命名方案。
  • 公告:Nmap 识别的与 CPE 相关的任何漏洞。需要我们自己把这些关联起来。
def create_scan_table(*, cli: str) -> Table:
    """
    为CLI UI创建表
    :param cli:运行时使用的完整Nmap参数
    :return: Skeleton table, no data
    """
    nmap_table = Table(title=f"NMAP run info: {cli}")
    nmap_table.add_column("IP", justify="right", style="cyan", no_wrap=True)
    nmap_table.add_column("Protocol", justify="right", style="cyan", no_wrap=True)
    nmap_table.add_column("Port ID", justify="right", style="magenta", no_wrap=True)
    nmap_table.add_column("Service", justify="right", style="green")
    nmap_table.add_column("CPE", justify="right", style="blue")
    nmap_table.add_column("Advisories", justify="right", style="blue")
    return nmap_table
...
def fill_simple_table(*, exec_data: str, parsed_xml: Dict[Any, Any]) -> Table:
    """
    使用nmap输出创建简单 xml ui表的便捷方法
    :param exec_data: Arguments and options used to run Nmap
    :param parsed_xml: Nmap data as a dictionary
    :return: Populated tabled
    """
    nmap_table = create_scan_table(cli=exec_data)
    for row_data in parsed_xml:
        address = row_data['address']
        ports = row_data['ports']
        for port_data in ports:
            nmap_table.add_row(
                address,
                port_data['protocol'],
                port_data['port_id'],
                f"{port_data['service_name']} {port_data['service_product']} {port_data['service_version']}",
                "\n".join(port_data['cpes']),
                ""
            )
    return nmap_table

生成的脚本使用上面的代码为用户提供有关本地网络扫描的全貌:

#!/usr/bin/env python
import sys
from rich.console import Console
from home_nmap.query import OutputParser
from home_nmap.ui import fill_simple_table

if __name__ == "__main__":
    console = Console()
    for nmap_xml in sys.argv[1:]:
        with open(nmap_xml, 'r') as xml:
            xml_data = xml.read()
            rundata, parsed = OutputParser.parse_nmap_xml(xml_data)
            nmap_table = fill_simple_table(exec_data=rundata, parsed_xml=parsed)
            console.print(nmap_table)

如何使用 Python 增强 Nmap_pyNmap

如果您注意到,“Advisories(公告)”列完全为空。我们将使用 NIST 网络安全引擎来填充缺失的公告,绕过包含版本信息的 CPE,以避免误报。

我们使用请求来帮助我们进行 HTTP 通信:

from dataclasses import dataclass
import requests
IGNORED_CPES = {"cpe:/o:linux:linux_kernel"}
from cpe import CPE
from lxml import html

@dataclass
class NIDS:
    summary: str
    link: str
    score: str

class NDISHtml:

    def __init__(self):
        """
        一些CPE返回了太多的误报,所以一开始忽略掉
        """
        self.raw_html = None
        self.parsed_results = []
        self.url = "https://nvd.nist.gov/vuln/search/results"
        self.ignored_cpes = IGNORED_CPES

    def get(self, cpe: str) -> str:
        """
        在NDIS网站上运行CPE搜索。如果CPE没有版本,则跳过搜索,因为它将返回太多的误报
        @param cpe: CPE identifier coming from Nmap, like cpe:/a:openbsd:openssh:8.0
        @return:
        """
        params = {
            'form_type': 'Basic',
            'results_type': 'overview',
            'search_type': 'all',
            'isCpeNameSearch': 'false',
            'query': cpe
        }
        if cpe in self.ignored_cpes:
            return ""
        valid_cpe = CPE(cpe)
        if not valid_cpe.get_version()[0]:
            return ""
        response = requests.get(
            url=self.url,
            params=params
        )
        response.raise_for_status()
        return response.text

    def parse(self, html_data: str) -> list[NIDS]:
        """
        解析NDIS网络搜索。
        假设最终用户不直接调用此方法,不会对HTML文件内容检查。
        @param html_data: RAW HTML used for scrapping
        @return: List of NDIS, if any
        """
        self.parsed_results = []
        if html_data:
            ndis_html = html.fromstring(html_data)
            # 1:1 match between 3 elements, use parallel array
            summary = ndis_html.xpath("//*[contains(@data-testid, 'vuln-summary')]")
            cve = ndis_html.xpath("//*[contains(@data-testid, 'vuln-detail-link')]")
            score = ndis_html.xpath("//*[contains(@data-testid, 'vuln-cvss2-link')]")
            for i in range(len(summary)):
                ndis = NIDS(
                    summary=summary[i].text,
                    link="https://nvd.nist.gov/vuln/detail/" + cve[i].text,
                    score=score[i].text
                )
                self.parsed_results.append(ndis)
        return self.parsed_results

我们将结果中的 Nmap CPES 与每个公告(如果有)相关联:

from typing import Any
from dataclasses import dataclass
@dataclass
class NIDS:
    summary: str
    link: str
    score: str
class NDISHtml:
    def correlate_nmap_with_nids(self, parsed_xml: Any) -> dict[str, list[NIDS]]:
        correlated_cpe = {}
        for row_data in parsed_xml:
            ports = row_data['ports']
            for port_data in ports:
                for cpe in port_data['cpes']:
                    raw_ndis = self.get(cpe)
                    cpes = self.parse(raw_ndis)
                    correlated_cpe[cpe] = cpes
        return correlated_cpe

如何使用 Python 增强 Nmap_Nmap_02

Nmap 扫描结果显示在一个漂亮的表格上

如果能够直接从 Python 运行 Nmap,而不是解析运行结果,那就太好了。

如何使用 Python 包装 Nmap (subprocess.run)

Nmap 不提供与外部程序交互的正式 API。因此,我们将从 Python 运行它并将结果保存到 XML 文件中。然后我们可以按照任何我们想要的方式使用数据(参见我们的类 NmapRunner 中方法 'scan' 中的 'subprocess.run' 调用):

class NMapRunner:

    def __init__(self):
        """
        创建Nmap可执行文件
        """
        self.nmap_report_file = None
        found_sudo = shutil.which('sudo', mode=os.F_OK | os.X_OK)
        if not found_sudo:
            raise ValueError(f"SUDO is missing")
        self.sudo = found_sudo
        found_nmap = shutil.which('nmap', mode=os.F_OK | os.X_OK)
        if not found_nmap:
            raise ValueError(f"NMAP is missing")
        self.nmap = found_nmap

    def scan(
            self,
            *,
            hosts: str,
            sudo: bool = True
    ):
        command = []
        if sudo:
            command.append(self.sudo)
        command.append(self.nmap)
        command.extend(__NMAP__FLAGS__)
        command.append(hosts)
        completed = subprocess.run(
            command,
            capture_output=True,
            shell=False,
            check=True
        )
        completed.check_returncode()
        args, data = OutputParser.parse_nmap_xml(completed.stdout.decode('utf-8'))
        return args, data, completed.stderr

如何加速 Nmap

您的本地网络比 Internet 的延迟要短。扫描开放端口和操作系统漏洞也很可能会更容易,因为您和主机之间没有防火墙。

此外,我们不关心触发 IDS 检测,因此您可以使用以下方法来减少完成端口扫描所需的时间(软件包系统中的变量NMAP__FLAGS):

import shlex
# 转换args以在CLI上正确使用
NMAP_HOME_NETWORK_DEFAULT_FLAGS = {
    '-n': 'Never do DNS resolution',
    '-sS': 'TCP SYN scan, recommended',
    '-p-': 'All ports',
    '-sV': 'Probe open ports to determine service/version info',
    '-O': 'OS Probe. Requires sudo/ root',
    '-T4': 'Aggressive timing template',
    '-PE': 'Enable this echo request behavior. Good for internal networks',
    '--version-intensity 5': 'Set version scan intensity. Default is 7',
    '--disable-arp-ping': 'No ARP or ND Ping',
    '--max-hostgroup 20': 'Hostgroup (batch of hosts scanned concurrently) size',
    '--min-parallelism 10': 'Number of probes that may be outstanding for a host group',
    '--osscan-limit': 'Limit OS detection to promising targets',
    '--max-os-tries 1': 'Maximum number of OS detection tries against a target',
    '-oX -': 'Send XML output to STDOUT, avoid creating a temp file'
}
__NMAP__FLAGS__ = shlex.split(" ".join(NMAP_HOME_NETWORK_DEFAULT_FLAGS.keys()))

Nmap 文档还建议你可以将总主机列表拆分到 Nmap 的多个实例中(它不能大于运行该工具的服务器中的 CPU 数量)以提高并行度。但这并不是免费提供的。您需要担心运行 Nmap 的并发线程中的竞争条件和同步等问题。