识别网络中的所有公共服务
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)
如果您注意到,“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
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 的并发线程中的竞争条件和同步等问题。