我自己的主机最近被安全部门拉出来做了一次授权巡检nmap 扫完回来看到一堆 open 端口说实话那一刻我心里是有点发虚的。端口扫描这个词听起来像是安全从业者的基本功但真正能把“扫出来的东西”讲清楚、用明白的人实际上并不算多。有人只会敲nmap -sS然后盯着输出发呆有人天天收到 Snort 告警却分不清是真是假也有人想自己搭一个在线端口扫描工具却卡在超时和并发上。这篇不是教科书式的原理复述而是从我实际做安全运维、写检测规则、报漏洞的过程中总结出来的扫描的本质是什么nmap 各种参数到底该怎么选自己写在线端口扫描器要注意哪些细节以及最让值班头疼的“Snort 两个设备同时扫描只记录一条日志”到底是怎么回事。适合一线安全运维、网络管理员以及刚入门想搞懂原理而不是只会“按回车”的朋友。1. 从一次线上巡检聊起端口扫描到底在扫什么先说那次巡检。目标是一台跑着 Web 服务和内部 API 的 CentOS 主机我用 TCP 全连接扫描扫了一遍 1-65535结果里有 22、80、443、3306、6379 这些眼熟的老面孔还有一个 10050 端口也开着。10050 是 Zabbix Agent 的默认端口如果不做访问控制等于把服务器内部状态直接暴露给同网段任何人。这就是端口扫描的核心价值它不是在“黑”你而是在替你回答一个问题——你的门都开在哪里门后面是什么。1.1 端口与三次握手扫描行为的物理本质TCP 端口本质上是一个“门牌号”服务器收到数据包后根据目标端口决定交给哪个进程处理。扫描之所以能够成立靠的是 TCP 三次握手设计里的一个“漏洞”——服务器在收到 SYN 请求后必须做出回应。如果端口开放内核协议栈会回SYNACK意思是“我同意建立连接”。如果端口关闭内核会直接回RST相当于“这里没人别敲了”。如果数据包被防火墙静默丢弃那就什么回应都没有端口处于filtered被过滤状态。所以扫描行为的本质就是主动向目标端口发一个 SYN 包然后根据响应判断端口状态。整个过程并没有真正建立一个完整的应用层连接但是“敲门”这个动作本身已经发生了。这就是为什么端口扫描会被 Snort 之类的 IDS 识别出来——SYN 包在短时间内的异常频率本身就是最明显的特征。1.2 看结果之前先理解 open/closed/filtered 三种状态很多人拿到 nmap 输出就直接照着 445、3306 这些端口去报告漏洞忽略了状态字段的意义。其实open、closed、filtered代表的信息完全不是一回事状态收到的响应实际含义风险等级openSYNACK服务监听中且有程序应答高需要确认服务身份closedRST主机在线但端口未监听低一般不需要处理filtered无响应或ICMP不可达防火墙干预端口状态未知中可能被防火墙保护也可能是隐蔽服务unfiltered仅确认可达无法进一步判断低需要配合其他扫描方式我见过不少人把filtered直接理解成“安全”这是不对的。防火墙放行或丢弃策略的不同会导致完全相同的端口在不同扫描位置下出现不同的状态。真实环境中很多被入侵的服务器端口状态恰恰是filtered——因为攻击者做了内核态端口复用普通发包请求只能看到防火墙的 ICMP 反馈看不到真正的服务响应。后面在做 Snort 检测时这个状态理解尤其重要因为 IDS 也经常把“被防火墙挡掉的扫描”当成真实攻击来报。2. 原理到工具nmap 的扫描手法和策略选择别只会 -sS工具层面的选择本质上是扫描隐蔽性、准确性和速度三者的权衡。先说最常见的-sSSYN 半开扫描和-sTTCP 全连接扫描这俩是绝大多数人最先接触的扫描方式但很多人不知道它们核心差异。2.1 SYN 半开扫描与 TCP 全连接扫描一个不握手一个完整握手-sS的原理是只发送 SYN收到SYNACK后就发送 RST 断开连接不走完三次握手。这样做有两个直接好处速度快因为不需要处理后续 ACK 和连接状态目标应用的访问日志里不会留下记录因为应用层根本不知道有人来过。代价是这种半开扫描需要 root 权限因为要伪造和终结 TCP 连接状态而且在现代 Windows 系统上由于 IP 协议栈行为差异半开扫描的结果可能不够准确。-sT则是完整的 TCP connect 扫描。系统内核会替你完成三次握手然后发送 RST 关闭连接。这个过程不需要特殊权限但会留下完整的连接日志也更容易被目标侧 IDS 或服务日志记录。我用过一种非常典型的内外网扫描对比内网扫自己的服务器时-sS和-sT结果几乎一致但从公网扫描经过云安全组过滤的主机时-sT常常大量报filtered而-sS能更少受中间设备干扰因为某些负载均衡器会直接代答SYNACK导致-sT误判。2.2 服务版本探测 -sV 和操作系统指纹 -O扫描的“最后一公里”光知道端口开没开还不够风险评估最终要落到“端口后面跑的是什么”。nmap 的-sV会主动连接开放端口发送一组探测数据包然后根据服务返回的 banner、协议响应等特征匹配指纹库判断出具体服务及版本。网上常见的一句话“nmap -sV 会打崩脆弱的服务”不是危言耸听。我自己踩过一个坑用-sV扫一台运行老版本 Memcached 的服务器结果是服务直接没响应。原因是某些服务的协议解析器对异常输入处理不完善nmap 的探测包会触发崩溃。所以现在我做生产环境扫描一定遵循“先-sS摸端口再对关键端口单独-sV最后手工复核高危端口”的顺序而不是上来就-A全开。-O的操作系统指纹识别则更激进它通过分析目标 IP 头里的 TTL 初始值、TCP Window Size、TCP 选项顺序、时间戳等参数来推断操作系统。我建议非必要不启用-O因为它在部分网络环境下会触发误报而且扫描耗时明显增加。真正需要做资产盘点时用 nmap 脚本--scriptsmb-os-discovery或--scriptsnmp-sysdescr获取的版本信息远比-O的猜测可靠。2.3 常用扫描工具对照nmap 与 masscan 怎么配合masscan 和 nmap 不是替代关系而是互补关系。masscan 号称可以做到每秒百万级数据包发送适合在短时间内扫描大规模网段的某个特定端口nmap 则强在分类识别和脚本扩展上。我个人的标准流程是用 masscan 快速扫出全网段的高危端口把结果存成文件再交给 nmap 做存活确认和服务识别。工具核心优势典型误用方式适合场景nmap功能全、脚本丰富、结果准确上来就 -A 全扫对几十到几百台主机做精细摸底masscan高速且支持无状态扫描把 rate 调到极限打崩出口带宽大规模网段端口普查nc / bash /dev/tcp轻量、易脚本化无并发控制临时验证单个端口连通性这里额外说一句masscan 的--rate不是越大越好。我在千兆内网里把 rate 调到 10000 时曾经导致交换机 CPU 冲高、同网段其他业务出现延迟。稳妥的做法是先用ping或并发 TCP 探测确认目标网段在线率再根据在网主机数量决定速率一般 1000-3000 已经足够跑完一个 /16 的常见端口。3. 自己动手写一个在线端口扫描器最容易踩的其实是超时和并发网上搜索“在线端口扫描源码”的人不少说明很多人想在 Web 上提供一个类似站长工具的扫描入口。但如果你真的自己写过一遍就会发现核心的 connect 逻辑其实只有几十行真正难点在于并发控制、超时处理和防止工具被滥用。下面是我实现过程中梳理出的几条关键思路。3.1 整体架构与任务队列设计一个完整的在线端口扫描服务通常包含四个部分前端表单、后端任务队列、扫描执行器和结果存储展示。为什么需要任务队列因为不能用户提交一个 IP 就起一个线程池去扫否则几个用户同时提交服务器立刻被拖垮而且出口 IP 一旦被目标防火墙识别为扫描源后续所有扫描都会失真。我的做法是用户提交目标和端口范围后生成一个task_id把任务写入队列后端由一个独立的 worker 进程消费。每个 worker 最多同时跑 100 个探测任务探测结果每完成一个就写入 Redis 缓存前端通过轮询task_id获取进度。这样无论有多少用户提交实际并发扫描源只有一个可控的 IP避免了互相干扰。3.2 端口连通性探测的代码思路非阻塞 connect 与超时在线端口扫描的核心是 TCP 连接的超时控制。如果使用阻塞式 connect默认超时常常要 1-2 分钟扫几十个端口根本没法用。正确思路是使用非阻塞 socket select/poll 轮询控制超时import socket import select def check_port(host, port, timeout2.0): sock socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setblocking(False) try: sock.connect_ex((host, port)) except socket.gaierror: sock.close() return dns_error _, writable, _ select.select([], [sock], [], timeout) if writable: # 连接成功之后拿到 socket error0 表示端口开放 err sock.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) sock.close() return open if err 0 else closed sock.close() return filtered if __name__ __main__: print(check_port(192.0.2.10, 80))这段逻辑的核心在于select.select的第三个参数timeout。我实测下来内网环境超时设 0.5-1 秒足够公网环境设 2-3 秒比较稳妥。超时时间设得太短会把closed误判为filtered设得太长则整个扫描任务会拖到让人失去耐心。所以我在在线工具里给用户提供的“超时时间”选项是固定三档快0.8 秒、标准2 秒、慢5 秒。3.3 防止工具被滥用目标限制、频率限流和结果缓存写在线扫描器时最容易忽略的就是滥用问题。如果没有限制任何人都能用你的带宽去扫任何 IP轻则出口被封重则被别人抓去当跳板。我在这块做了三个基础防护你也可以照搬思路只允许扫描用户自己声明的 IP 段且无法扫描内网保留地址10.0.0.0/8、172.16.0.0/12、192.168.0.0/16以及云厂商的 metadata 地址169.254.169.254。这能直接切断大量指向云内网的恶意扫描。一个 IP 在 5 分钟内最多提交 3 个扫描任务每次最多扫描 100 个端口。扫描结果缓存 10 分钟同一目标端口组合直接复用缓存避免重复扫描。前端加入图形验证码后端记录用户 IP 的提交频率。如果同一 IP 在 1 分钟内提交超过 10 次则封禁 30 分钟。这些限制让工具的性能损失非常小但对于合规性和稳定性来说收益很大。在线扫描工具最忌讳的就是“什么都能扫、谁都能扫”——这绝不是功能强悍的表现而是给自己埋雷。4. 防守侧实践Snort 如何识别端口扫描以及那条“只有一条日志”的排查思路端口扫描不光要做扫描方还得站在防守方的角度看问题。Snort 是我们日常用得很多的免费 IDS它关于端口扫描的检测主要依赖于预处理器sfportscan而不是普通规则。很多值班同事对它的输出一头雾水尤其容易遇到一个诡异现象明明有两台设备同时发起扫描Snort 却只记录了一条日志。4.1 sfportscan 的工作机制与常用配置sfportscan是 Snort 的端口扫描预处理器它在规则引擎之前对流量做协议分析和会话组装。它检测的对象主要是 TCP/UDP 端口扫描行为和端口扫描的工具行为并且能区分出三种典型的扫描类型扫描类型触发条件日志特征portscan单源 IP 在短时间内扫描大量端口源 IP 固定目标端口分散decoy_portscan单源 IP 使用多个伪源 IP 扫描源 IP 频繁变化但特征一致distributed_portscan多个源 IP 对同一目标 IP 进行扫描源 IP 不固定目标 IP 集中配置文件通常在snort.conf中引入然后在snort.debian.conf或自定义配置里启用preprocessor sfportscan: proto { all } \ memcap { 10000000 } \ scan_type { all } \ watch_ip { 192.168.1.0/24 } \ sense_level { high }watch_ip用来声明需要重点监控的网段sense_level控制检测灵敏度。我个人的建议是在内网资产稳定的情况下sense_level不要盲目调成high否则大量自动化运维工具比如配置管理系统的端口探活会被误报成扫描告警疲劳比漏报更危险。4.2 两台设备扫描只记录一条日志根因分析这个问题的本质是sfportscan的事件聚合机制在起作用。Snort 不是每看到一个 SYN 包就产生一条独立告警而是在一个时间窗口内将“同一目标 IP 上发生的扫描行为”聚合成一个事件再以 summary 的形式记入日志。当两台不同源 IP 的设备在同一时间段扫描同一个目标 IP 时sfportscan会通过会话跟踪发现目标 IP 出现了来自多个源 IP、且目标端口分布符合扫描特征的行为。此时它判定为distributed_portscan分布式扫描在事件统计中只对“被扫描的目标 IP”生成一条汇总日志而不是对每个源 IP 各生成一条。所以你在告警里看到的情况是事件显示src 192.168.1.5 - dst 192.168.1.100但实际上参与扫描的源 IP 并不是只有192.168.1.5这一个。Snort 把多个源 IP 的扫描行为合并到了同一条事件里这是设计逻辑不是 bug。4.3 排查链路与验证方法遇到这类告警时直接改配置是不可取的。我建议按下面的链路逐步排查验证先看告警类别。打开 Snort 的 alert 日志确认 signature 是ET SCAN Behavioral Unusual Port Scan这类规则告警还是(portscan) Portscan detected这种预处理器事件。只有后者才存在聚合问题。再查portscan事件详情。在 unified2 日志解析工具中打开对应事件查看 event 里是否包含participating IPs字段。如果该字段列出了多个 IP那么基本可以确认这是分布式扫描的聚合记录。回到实际流量验证。用 tcpdump 在目标主机侧抓包观察同一时间窗口内是否有来自不同源 IP 的 SYN 包到达同一个目标端口段tcpdump -i eth0 tcp[tcpflags] tcp-syn ! 0 and tcp[tcpflags] tcp-ack 0 -nn -c 500如果抓包结果确认了多个源 IP 参与扫描那么 Snort 的记录方式就是准确的只是展示方式不够直观。这时候如果想要更细粒度的记录就不能只依赖sfportscan需要另加一条规则按源 IP 拆开alert tcp any any - $HOME_NET any (msg:PORTSCAN from single source; flow:stateless; flags:S; threshold:type both, track by_src, count 20, seconds 10; sid:10000001; rev:1;)这条规则的意思是在 10 秒内从同一个源 IP 发往内网任意端口的 SYN 包达到 20 个就产生一条告警。由于track by_src的存在每个源 IP 会被单独计数、单独触发从而解决“两个设备只记录一条日志”的问题。当然代价是告警量会明显上升所以count和seconds的取值需要根据自己的网络环境去调整我内网是 20/10外网边界我一般会放宽到 50/10避免被批量扫描误伤。5. 关于扫描速度、准确性和合法边界一些实践心得端口扫描做久了我最大的体会是扫描不是为了“把端口全列出来”而是为了在合适的时间用合适的方法得到可信的结果。速度、准确性、以及合规性这三者必须同时被尊重。5.1 并发速度与误报率的取舍把并发调高扫描速度确实能上去但代价是误报率和漏报率的双重上升。并发过高时目标机可能因为协议栈队列溢出而丢弃 SYN导致端口被误判成filtered同时目标侧的防火墙或 WAF 也会更容易在更短的时间内识别出扫描行为并触发封禁反而拿不到完整数据。我做过一个对比nmap 的-T4和-T5扫描同一台公网主机-T5用时更短但约有 7% 的端口显示为filtered而-T4扫描下这些端口其实是可以正常访问的。原因就是-T5下 nmap 的时间间隔太短部分探测包在目标侧被丢弃。所以日常扫描我的建议是-T4封顶除非对象是内网自己完全掌控的服务器否则不建议再往上调。5.2 分阶段扫描比一把梭更高效我以前也干过 “nmap -sS -sV -O -A 全扫” 的事后来在生产环境里被狠狠教育了。正确流程是分阶段先做存活主机发现再用高速扫描找出开放端口然后针对每个开放端口做服务识别最后按优先级进行人工验证。这样做的原因有两点第一每个阶段的结果直接决定下一阶段的参数比如扫端口时知道目标在线才去做服务探测能减少大量无效请求第二每个阶段都可以设置独立的并发值和时间窗口避免一次性高负载引起目标防御系统的注意。5.3 经验清单一份可以直接抄走的检查表最后整理一份我实际工作中反复使用的检查清单送给准备开始做扫描或正在为扫描告警头疼的朋友环节建议做法一句话原因授权扫描前拿到书面授权明确扫描范围和禁止行为没授权就不要扫出了事没人能保你存活发现先 ping/ARP/无状态 SYN 摸清在网主机对下线主机做端口扫描纯属浪费时间端口扫描用 masscan 做端口普查nmap 做精确确认masscan 快但易误报nmap 准但慢服务识别只对 open 端口做 -sV避免对 closed/filtered 端口做无意义探测告警监控Snort 事件出现聚合日志时抓包确认真实参与者聚合日志是设计行为需要靠展示层去还原结果记录扫描结果保存为 JSON记录目标 IP、时间、端口、服务指纹没有记录就等于白扫后续复盘无据可查收尾清理确认扫描器关闭、无残留定时任务我见过线上出问题后扫描器还在后台继续跑的情况端口扫描不是一条命令就完事的工作它的价值取决于你对手上数据理解得有多深。我自己也还在不断踩坑和补齐细节尤其是 Snort 预处理器和防火墙联动那部分每个网络环境都能给出不一样的答案。如果你也遇到过类似“日志数量对不上”的情况建议从事件聚合角度先排查再考虑要不要动规则参数这个方向一般不会白折腾。
