简介:这份资源是面向初级运维人员与初级网络安全研究者的漏洞扫描系统设计与实现文档,围绕Python、Django、Docker与Nmap等技术,讲解如何构建一个低学习成本的B/S架构扫描平台,帮助缺乏专业安全技能的用户开展基础网络安全检查。压缩包内共1个docx文件,约2.18MB,内容涵盖绪论、技术简介、系统分析与设计等章节,涉及用户认证、信息管理、漏洞扫描、日志文章与权限管理等模块,并配有摘要与目录,便于按章节查阅。目前已有345人学习下载。读者可从中获取完整的系统设计思路、功能模块划分与集成方案,理解Django快速开发与Docker轻量级虚拟化结合Nmap的实践路径,适合作为课程设计、毕业设计或安全入门项目的参考材料。
1. 从一份 docx 标题说起:Python 漏洞扫描系统到底在扫什么
很多人第一次看到「基于 Python 的漏洞扫描系统的设计与实现」这个标题,脑子里浮现的是 Nessus、OpenVAS 那种界面复杂、插件成千上万的商业工具,觉得自己用 Python 加 Django 搭一个纯属玩具。我一开始也这么想,直到真正接手一个内部资产梳理的活儿:几百台机器散落在几个网段,端口开着什么、跑着什么服务、有没有明显的弱口令和过期组件,全靠人工一台台登上去看根本不现实。这时候一个能定时跑、能出报告、能按资产维度归档的轻量扫描系统,价值就出来了。
这个标题拆开看其实是三件事:扫描引擎负责发现(Nmap 做端口和指纹识别)、漏洞判定负责匹配(把指纹和已知漏洞规则对上)、系统平台负责管理(Django 做资产、任务、报告和权限)。它解决的不是「替代商业扫描器」,而是「把扫描这件事工程化、可复现、可追溯」。适合谁?适合手里有一堆内网资产要盘、又不想为每个小需求买授权的一线运维和安全同学,也适合想拿一个完整项目练 Django + Docker + Nmap 组合的开发者。下面我按自己实际搭过的一版思路,把选型、落地和踩坑讲清楚。
2. 扫描引擎与平台选型:为什么是 Nmap + Django + Docker 这套组合
2.1 扫描能力为什么优先选 Nmap 而不是自己写 socket
自己用 Python 的 socket 写端口扫描,入门练手可以,真上生产会立刻翻车。原因很直接:TCP 连接扫描要处理超时、重传、并发、半开连接,还要面对目标主机的防火墙策略和速率限制,这些 Nmap 已经打磨了二十多年。Nmap 提供的不只是端口状态,还有服务指纹(-sV)、操作系统识别(-O)、脚本引擎(NSE)能直接跑漏洞探测脚本,这对一个扫描系统来说是现成的能力池。
我一般把 Nmap 当成「扫描执行器」,Python 只负责调度和解析。调用方式有两种:一是subprocess直接调命令行,二是用python-nmap库封装。前者灵活、可控参数多,后者省去解析文本的麻烦。实际项目里我更倾向subprocess+ XML 输出(-oX),因为 XML 结构稳定,解析起来比 grep 文本靠谱得多。
# 一次典型的扫描:快速端口 + 服务版本 + 输出 XML nmap -sS -sV -T4 --top-ports 1000 -oX /tmp/scan_result.xml 192.168.1.0/24这条命令里-sS是 SYN 半开扫描,速度快且不易被应用层日志记录;-sV做服务版本探测,是后续漏洞匹配的关键;-T4是时序模板,内网可以用激进一点;--top-ports 1000只扫常见端口,全端口 65535 太慢,除非有明确需求。输出 XML 给解析层用,不要直接读 stdout。
提示:
-sS需要 root 或 CAP_NET_RAW 权限,容器里跑要注意加--cap-add=NET_RAW,否则会静默退化成-sT全连接扫描,速度差一大截。
2.2 Django 在这套系统里承担什么角色
Django 不是扫描器,它是「资产与任务的管理中枢」。一个扫描系统跑起来会产生大量结构化数据:资产表、端口表、服务表、漏洞表、任务表、报告表。用 Django 的 ORM 建模,配合 admin 后台,几乎不用写多少前端就能把资产管理界面搭出来。热搜里常出现「django 创建 app」「django 项目实战新手」,说明很多人卡在工程组织上。我的做法是按职责拆 app:
assets:资产、网段、标签scanner:扫描任务、调度、Nmap 调用封装vulns:漏洞规则、匹配结果、风险等级reports:报告生成与导出
这样拆的好处是每个 app 的模型边界清晰,迁移(migration)不会互相打架。Django 的manage.py startapp创建后记得在settings.py的INSTALLED_APPS里注册,否则模型不会建表,这是新手最常见的「表怎么没生成」问题。
# scanner/models.py 任务模型的核心字段 class ScanTask(models.Model): target = models.CharField(max_length=64) # 目标网段或 IP ports = models.CharField(max_length=128, default="top1000") status = models.CharField(max_length=16, default="pending") # pending/running/done/failed created_at = models.DateTimeField(auto_now_add=True) result_file = models.CharField(max_length=256, blank=True) # XML 结果路径status字段是整个调度的心脏,扫描是异步的,任务提交后不能阻塞 Web 请求,所以必须有状态机。result_file存路径而不是把 XML 塞进数据库,是因为扫描结果动辄几 MB,进库会让查询变慢。
2.3 Docker 把 Nmap 和 Django 的依赖地狱一次性解决
Nmap 的版本、NSE 脚本库、Python 依赖、数据库驱动,这几样在不同机器上装一遍能折腾一下午。Docker 的价值在这里体现得最明显:把扫描器和 Web 服务分别打包,用docker compose编排。热搜里「docker 安装 mysql 失败」「docker desktop failed to start」这类问题,本质多是环境没理顺,而不是 Docker 本身难用。
# docker-compose.yml 精简版 services: web: build: . ports: - "8000:8000" depends_on: - db db: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: example MYSQL_DATABASE: vulnscan volumes: - db_data:/var/lib/mysql volumes: db_data:depends_on只保证启动顺序,不保证 MySQL 已经能接受连接,所以 Django 启动脚本里要加重试逻辑,否则第一次migrate必然报连接拒绝。这是「docker 安装 mysql8.0 并使用」场景里最高频的坑。
3. 从零跑通最小扫描闭环:建表、调 Nmap、存结果
3.1 环境准备与依赖安装的最小步骤
先把 Python 环境和依赖固定下来,避免版本漂移。我一般用requirements.txt锁版本,Python 3.8 到 3.11 都能跑,但 Nmap 的 Python 封装库对版本敏感,建议固定。
# 宿主机或容器内安装系统依赖 apt-get update && apt-get install -y nmap # Python 依赖 pip install django mysqlclient python-nmapmysqlclient需要系统里有libmysqlclient-dev和编译工具,否则pip install会报mysql_config not found。如果不想折腾编译,可以换pymysql并在__init__.py里打补丁,但性能和兼容性上mysqlclient更稳。Nmap 本体一定要在运行扫描的容器里装,Web 容器和扫描容器如果是分开的,扫描容器才需要 Nmap。
3.2 用 subprocess 封装一次 Nmap 调用并解析 XML
核心逻辑是:拼命令、执行、读 XML、转成 Python 字典、写库。下面这段是我实际用过的简化版。
import subprocess import xml.etree.ElementTree as ET def run_nmap(target, ports="top1000"): # -oX - 让 nmap 把 XML 输出到 stdout,省去临时文件 cmd = ["nmap", "-sS", "-sV", "-T4", "-oX", "-", target] if ports == "top1000": cmd.insert(4, "--top-ports") cmd.insert(5, "1000") proc = subprocess.run(cmd, capture_output=True, text=True, timeout=1800) if proc.returncode != 0: raise RuntimeError(f"nmap failed: {proc.stderr[:200]}") return parse_xml(proc.stdout) def parse_xml(xml_str): root = ET.fromstring(xml_str) hosts = [] for host in root.findall("host"): addr = host.find("address").get("addr") ports = [] for port in host.findall(".//port"): state = port.find("state").get("state") service = port.find("service") ports.append({ "port": port.get("portid"), "state": state, "service": service.get("name") if service is not None else "", "version": service.get("version") if service is not None else "", }) hosts.append({"ip": addr, "ports": ports}) return hoststimeout=1800是硬性保护,防止某个网段卡死拖垮整个任务队列。-oX -把 XML 打到标准输出,避免并发任务写同一个临时文件互相覆盖。解析时只取state为open的端口入库,filtered和closed存了也没意义,反而让报告变臃肿。service的name和version是后续漏洞匹配的输入,比如识别出Apache httpd 2.4.49就能对上 CVE-2021-41773 这类路径穿越漏洞。
3.3 把扫描结果落库并生成一份可读报告
解析出来的字典要写进 Django 模型。这里有个性能点:一个 C 段扫下来可能几百个 host、上千个端口,逐条save()会慢得离谱,用bulk_create批量插入。
from django.db import transaction from assets.models import Asset, Port def save_scan_result(hosts): with transaction.atomic(): for h in hosts: asset, _ = Asset.objects.get_or_create(ip=h["ip"]) port_objs = [ Port(asset=asset, number=p["port"], state=p["state"], service=p["service"], version=p["version"]) for p in h["ports"] if p["state"] == "open" ] Port.objects.filter(asset=asset).delete() # 先清旧数据再写 Port.objects.bulk_create(port_objs)transaction.atomic()保证一个资产要么全写成功要么全回滚,避免扫到一半失败留下半截数据。先删后插是因为端口状态会变,增量更新逻辑复杂且容易出错,全量覆盖更简单可靠。报告生成可以用 Django 模板渲染 HTML,再转 PDF,或者直接导出 CSV 给运维同事,别一上来就追求花哨的 PDF 排版。
4. 漏洞匹配与任务调度:让扫描系统真正「有结论」
4.1 指纹到漏洞的匹配规则怎么设计
扫描出端口和服务只是原料,漏洞判定才是结论。最朴素也最可控的做法是维护一张规则表:服务名 + 版本范围 → CVE 编号 + 风险等级 + 修复建议。不要一上来就接庞大的漏洞库,先覆盖自己环境里最常见的几十条,准确率比覆盖面重要。
| 服务 | 版本条件 | 漏洞编号 | 等级 | 建议 |
|---|---|---|---|---|
| Apache httpd | 2.4.49 | CVE-2021-41773 | 高 | 升级到 2.4.51+ |
| OpenSSH | < 8.0 | 弱加密算法 | 中 | 禁用 ssh-dss |
| MySQL | 5.7 未授权 | 空口令 | 高 | 设置强密码 |
| Redis | 无密码 | 未授权访问 | 高 | 绑定内网 + requirepass |
匹配逻辑用 Python 写就是字符串和版本号比较,版本比较别用字符串直接比,2.4.9和2.4.10字符串比会出错,用packaging.version或自己拆成元组。
from packaging import version def match_vuln(service, ver): rules = [ ("httpd", "<2.4.51", "CVE-2021-41773", "高"), ("openssh", "<8.0", "弱加密算法", "中"), ] for name, cond, cve, level in rules: if name in service.lower(): op, target = cond[0], cond[1:] if op == "<" and version.parse(ver) < version.parse(target): return cve, level return None, Noneversion.parse能正确处理多段版本号,比手写比较函数省心。规则表建议放数据库而不是硬编码,方便运营同学自己加规则,改规则不用重新发版。
4.2 异步任务调度:别让扫描阻塞 Web 请求
扫描一个 C 段可能几分钟到几十分钟,绝对不能放在 Django 的请求-响应周期里同步执行,否则浏览器转圈转到超时。常见做法是引入 Celery + Redis 做任务队列,Django 只负责提交任务和查状态。
# scanner/tasks.py from celery import shared_task from .models import ScanTask from .nmap_runner import run_nmap, save_scan_result @shared_task def execute_scan(task_id): task = ScanTask.objects.get(id=task_id) task.status = "running" task.save() try: hosts = run_nmap(task.target, task.ports) save_scan_result(hosts) task.status = "done" except Exception as e: task.status = "failed" task.result_file = str(e)[:200] task.save()@shared_task让任务可以被 Celery worker 消费,Web 进程只调用execute_scan.delay(task_id)就立刻返回。状态字段让前端可以轮询进度。如果不想引入 Celery,退而求其次可以用threading起后台线程,但进程重启任务就丢了,生产环境不推荐。热搜里「docker 安装 redis 主从」说明 Redis 在大家环境里很常见,正好拿来当 broker。
4.3 扫描频率与并发控制的实际参数
并发不是越高越好。Nmap 本身有--min-rate和--max-rate控制发包速率,系统层面还要限制同时运行的任务数,否则把目标网络打挂或者把自己的出口带宽占满。我一般这样设:单任务-T4,同时运行任务数不超过 3,扫描时间避开业务高峰。Celery 的worker_concurrency设成 2 到 4,配合--max-tasks-per-child防止内存泄漏累积。
注意:对生产网段做全端口扫描前一定要拿到书面授权,扫描行为在多数环境里会被 IDS 告警,别让自己变成「被通报的那个人」。
5. 避坑与排查:这套系统最容易翻车的五个地方
5.1 容器里 Nmap 报权限不足,扫描结果全是 filtered
现象:容器内跑-sS扫描,返回的端口状态全是filtered,或者直接报You requested a scan type which requires root privileges。原因:Docker 默认不给容器 NET_RAW 能力,SYN 扫描发不了原始包。解决:启动容器时加--cap-add=NET_RAW --cap-add=NET_ADMIN,或者干脆用-sT全连接扫描,代价是慢且容易被目标记录。
5.2 Django migrate 报 MySQL 连接被拒
现象:docker compose up后 Web 容器启动就报django.db.utils.OperationalError: (2002, "Can't connect to MySQL server")。原因:depends_on只保证容器启动顺序,MySQL 初始化要几十秒,Django 启动太快。解决:在启动脚本里加等待循环,用mysqladmin ping或 Python 的 socket 探测,直到数据库可连接再执行migrate。
5.3 扫描结果里中文服务名乱码
现象:报告里服务描述出现乱码。原因:Nmap XML 默认 UTF-8,但某些系统 locale 不是 UTF-8,subprocess读取时用了错误编码。解决:subprocess.run(..., encoding="utf-8", errors="replace")显式指定编码,别依赖系统默认。
5.4 任务状态永远停在 running
现象:Celery 任务提交后状态不更新,前端一直转圈。原因:worker 进程崩了或者任务超时被 kill,但except没覆盖到BaseException,或者 worker 根本没启动。解决:确认celery -A proj worker在跑,给任务加soft_time_limit,并在finally里兜底更新状态,别让异常路径漏掉状态写回。
5.5 规则匹配误报太多,运维不信报告
现象:报告里一堆「高危」,点进去发现版本号根本没匹配上,或者服务名识别成了泛化名称。原因:-sV的版本探测有时只能拿到服务名拿不到精确版本,规则却按精确版本匹配。解决:版本拿不到时降级为「疑似」而不是「确认」,风险等级区分开;规则里对服务名做模糊匹配但版本条件放宽,宁可漏报不要误报,报告可信度比数量重要。
6. 进阶:把扫描系统做成能长期维护的资产台账
跑通最小闭环之后,真正决定这套系统能不能活下去的,是它能不能沉淀成资产台账,而不是每次扫完就丢。我后来加的一个关键能力是「资产变更对比」:每次扫描结果和上一次做 diff,新增端口、消失服务、版本变化都单独列出来。这个功能比漏洞列表更受运维欢迎,因为它直接告诉你「昨天到今天,哪台机器变了」。
实现上不难,给Port表加个last_seen时间戳,每次扫描更新,超过 N 天没出现的端口标记为「已下线」而不是直接删。对比逻辑用集合运算:
def diff_ports(old_set, new_set): added = new_set - old_set removed = old_set - new_set return {"added": added, "removed": removed}old_set和new_set都是(port, service, version)元组集合,集合运算天然去重且快。变更记录单独存一张表,报告里按时间线展示,运维一眼就能看出异常。
另一个值得投入的点是扫描任务的「模板化」。别让用户每次手填网段和端口,预置几套模板:内网快速巡检(top1000 + 常见漏洞脚本)、Web 资产专项(80/443/8080 + http 相关 NSE)、数据库专项(3306/5432/6379/27017)。模板存数据库,任务创建时选模板即可,降低使用门槛。
验证这套系统有没有做对,我的习惯是拿一台自己完全掌握的测试机,故意开几个已知有问题的服务(比如老版本 httpd、无密码 Redis),看系统能不能准确识别并给出正确等级。如果连自己搭的靶机都测不准,就别指望它在真实环境里靠谱。这个自测习惯帮我省了无数次在领导面前翻车的尴尬。
最后说一句实在的:这套东西的价值不在代码多优雅,而在它能不能每周自动跑一次、结果能不能被人看懂、变更能不能被追踪。把这三件事做扎实,比堆一百条漏洞规则都有用。希望帮到你。
本文还有配套的精品资源,点击获取