ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python漏洞扫描系统实战:从环境搭建到报告生成

Python漏洞扫描系统实战:从环境搭建到报告生成 简介这份毕业设计资料包围绕基于Python的漏洞扫描系统展开面向计算机、网络安全相关专业的学生及需要完成同类课题的开发者提供从理论到实现的完整参考。内容涵盖网络安全与漏洞扫描技术基础、系统设计目标与可行性分析以及用户登录、扫描首页、端口扫描、扫描列表等模块的实现成果可帮助读者理解漏洞扫描工具的整体架构与开发流程。资源共380个文件包含26个py源码、53个js脚本、26个css样式、15个tmpl模板及大量gif、jpg、png界面素材另有sql数据库文件、doc说明文档与pptx演示文稿压缩包约84.69MB目录结构清晰便于按模块查阅。目前已有3163人学习下载适合作为课程设计或毕业设计的参考方案也可用于学习Python网络编程与安全检测的基础实践。1. 从一份毕业设计标题拆开看Python 漏洞扫描系统到底在做什么很多人看到「基于 Python 的漏洞扫描系统」这个标题第一反应是去搜现成源码下载下来跑一遍交差。但真正做过这类系统的人知道能跑起来和能讲清楚中间发生了什么是两码事。答辩老师问一句「你这个扫描器怎么判断一个端口存在漏洞」如果只能回答「调了个库」基本就凉了。这个系统的本质是一个「信息收集 → 规则匹配 → 结果输出」的流水线。它要解决的核心问题是给定一个目标 IP 或域名自动发现开放端口、识别服务类型、比对已知漏洞特征最后生成一份可读的报告。适合谁适合正在做安全方向毕业设计的学生也适合想入门 Python 网络编程、理解扫描器工作原理的开发者。它不需要你从零写一个 Nmap但需要你理解每一步的数据是怎么流转的。我见过太多人卡在环境配置上——Python 装好了pip 装不上库库装上了扫描本机都报错。所以这篇笔记不打算只讲架构图而是从环境搭建、数据库设计、扫描逻辑到报告生成把每个环节的可复现步骤和踩坑点都摊开讲。你照着做至少能跑通一个能演示、能答辩、能继续扩展的版本。2. 环境搭建与项目骨架把 Python 漏洞扫描系统跑起来的第一步2.1 Python 版本选择与依赖库安装这个项目对 Python 版本没有极端要求但建议用 3.8 到 3.11 之间的版本。3.12 在某些 socket 库的兼容性上还有小问题我实测时遇到过socket.timeout行为变化导致扫描线程卡死的情况。安装 Python 本身不复杂Windows 去官网下载安装包勾选「Add Python to PATH」Linux 下用系统包管理器或者源码编译都行。关键是装完之后确认python --version和pip --version都能正常输出。依赖库方面核心就几个socket是标准库不用装threading和queue也是标准库。需要额外安装的是python-nmap如果你打算封装 Nmap、requests用于 Web 层探测、pymysql或sqlite3用于数据库操作。我一般会建一个requirements.txt内容如下# requirements.txt python-nmap0.7.1 requests2.31.0 pymysql1.1.0安装命令就是一行pip install -r requirements.txt这里有个血泪经验如果你在 Windows 上装python-nmap它本身不包含 Nmap 二进制文件你还需要单独去 Nmap 官网下载安装包并把安装路径加到系统环境变量里。否则代码里nmap.PortScanner()会直接抛PortScannerError提示找不到 nmap 可执行文件。Linux 下用apt install nmap或yum install nmap就行。提示不要用pip install nmap那个包和python-nmap不是同一个东西装错了会报模块导入错误。2.2 项目目录结构与数据库表设计一个能讲清楚、能扩展的漏洞扫描系统目录结构应该清晰。我一般会这样组织vuln_scanner/ ├── main.py # 入口命令行参数解析 ├── scanner/ │ ├── port_scan.py # 端口扫描模块 │ ├── service_detect.py # 服务识别模块 │ └── vuln_check.py # 漏洞匹配模块 ├── db/ │ ├── init_db.py # 建表脚本 │ └── vuln_data.py # 漏洞库数据导入 ├── report/ │ └── generate.py # 报告生成 └── config.py # 全局配置数据库用 SQLite 就够了毕业设计不需要上 MySQL 那么重。但如果你想让项目看起来更「企业级」用 MySQL 也行只是部署时多一步安装和配置。我建议 SQLite因为它是文件型数据库拷贝项目时直接带着.db文件走答辩演示不会因为数据库连不上翻车。核心表设计三张scan_task记录每次扫描任务scan_result记录每个端口的扫描结果vuln_lib存漏洞特征库。建表 SQL 如下-- db/init_db.py 中执行的建表语句 CREATE TABLE IF NOT EXISTS scan_task ( id INTEGER PRIMARY KEY AUTOINCREMENT, target_ip TEXT NOT NULL, scan_time DATETIME DEFAULT CURRENT_TIMESTAMP, status TEXT DEFAULT running ); CREATE TABLE IF NOT EXISTS scan_result ( id INTEGER PRIMARY KEY AUTOINCREMENT, task_id INTEGER, port INTEGER, service TEXT, banner TEXT, is_vuln INTEGER DEFAULT 0, vuln_desc TEXT, FOREIGN KEY (task_id) REFERENCES scan_task(id) ); CREATE TABLE IF NOT EXISTS vuln_lib ( id INTEGER PRIMARY KEY AUTOINCREMENT, service_name TEXT, version_pattern TEXT, vuln_name TEXT, risk_level TEXT, description TEXT );scan_task的status字段用来标记任务状态扫描开始设为running结束改成finished中途出错改成failed。scan_result里的banner存服务返回的原始指纹信息is_vuln是布尔标记方便报告统计。vuln_lib是漏洞库version_pattern用正则表达式匹配版本号比如vsftpd 2.3.4对应.*2\.3\.4.*。注意SQLite 的AUTOINCREMENT必须配合INTEGER PRIMARY KEY使用写成INT PRIMARY KEY AUTOINCREMENT会报语法错误。这个坑我踩过调试了半小时才发现。2.3 命令行入口与参数解析main.py负责接收用户输入调用扫描模块最后触发报告生成。用argparse标准库就够了不需要装click或fire。核心代码# main.py import argparse from scanner.port_scan import scan_ports from scanner.service_detect import detect_service from scanner.vuln_check import check_vuln from db.init_db import init_database from report.generate import generate_report def main(): parser argparse.ArgumentParser(descriptionPython 漏洞扫描系统) parser.add_argument(-t, --target, requiredTrue, help目标 IP 或域名) parser.add_argument(-p, --ports, default1-1000, help端口范围如 1-1000) parser.add_argument(-o, --output, defaultreport.html, help报告输出路径) args parser.parse_args() init_database() # 确保表存在 ports parse_port_range(args.ports) open_ports scan_ports(args.target, ports) for port in open_ports: banner detect_service(args.target, port) check_vuln(args.target, port, banner) generate_report(args.output) print(f扫描完成报告已生成{args.output}) def parse_port_range(port_str): if - in port_str: start, end map(int, port_str.split(-)) return range(start, end 1) return [int(port_str)] if __name__ __main__: main()parse_port_range处理1-1000这种格式返回一个 range 对象。scan_ports返回开放端口列表detect_service返回 banner 字符串check_vuln把结果写进数据库。最后generate_report从数据库读数据生成 HTML。参数说明-t是必填项-p默认扫 1 到 1000 端口-o默认输出report.html。如果你想扫全端口传-p 1-65535但扫描时间会显著增加建议配合多线程。这里先不展开多线程下一章会专门讲。3. 端口扫描与服务识别从 socket 连接到 banner 抓取3.1 TCP 全连接扫描的实现与超时控制端口扫描最基础的方式是 TCP 全连接扫描也就是调用socket.connect()尝试连接目标端口。连上了就是开放抛异常就是关闭或过滤。代码不复杂但超时设置是玄学——设短了误报多设长了扫描慢。# scanner/port_scan.py import socket import threading from queue import Queue def scan_single_port(target, port, timeout1.0): 扫描单个端口返回 True 表示开放 try: s socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(timeout) result s.connect_ex((target, port)) s.close() return result 0 except socket.error: return False def scan_ports(target, ports, timeout1.0, max_threads100): 多线程扫描端口范围返回开放端口列表 open_ports [] queue Queue() for port in ports: queue.put(port) def worker(): while not queue.empty(): port queue.get() if scan_single_port(target, port, timeout): open_ports.append(port) queue.task_done() threads [] for _ in range(min(max_threads, len(ports))): t threading.Thread(targetworker) t.daemon True t.start() threads.append(t) queue.join() return sorted(open_ports)connect_ex返回 0 表示连接成功其他值对应不同的错误码。用connect_ex而不是connect的好处是它不抛异常直接返回错误码省去了 try-except 的开销。settimeout(1.0)表示 1 秒超时这个值在局域网扫描够用公网目标建议设 2 到 3 秒。多线程部分用Queue做任务分发max_threads控制并发数。我一般设 100再高容易触发目标主机的防护机制也容易把自己的文件描述符耗尽。daemonTrue让线程随主线程退出避免程序卡住不结束。提示Windows 下socket.connect_ex对某些过滤端口的超时行为不太准可能会等到系统默认超时约 20 秒。如果发现扫描特别慢把timeout设小一点或者改用asyncio异步方案。3.2 banner 抓取与服务指纹识别拿到开放端口后下一步是抓取服务 banner。不同服务的 banner 格式不一样HTTP 返回头、SSH 返回版本字符串、FTP 返回欢迎语。通用做法是连上端口后发一个换行符或者直接读等对方返回数据。# scanner/service_detect.py import socket def grab_banner(target, port, timeout3.0): 抓取服务 banner返回字符串 try: s socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(timeout) s.connect((target, port)) # 有些服务需要先发个请求才返回 banner if port in (80, 8080, 443): s.send(bHEAD / HTTP/1.0\r\n\r\n) else: s.send(b\r\n) banner s.recv(1024).decode(utf-8, errorsignore).strip() s.close() return banner except Exception: return def detect_service(target, port): 根据端口和 banner 识别服务 banner grab_banner(target, port) service_map { 21: FTP, 22: SSH, 23: Telnet, 25: SMTP, 80: HTTP, 443: HTTPS, 3306: MySQL, 6379: Redis } service service_map.get(port, Unknown) return {port: port, service: service, banner: banner}grab_banner里对 HTTP 端口特殊处理发一个HEAD请求因为 HTTP 服务不会主动发 banner。其他端口发个换行符触发对方响应。recv(1024)读 1KB 数据一般够用banner 不会太长。decode时用errorsignore防止二进制数据导致解码报错。detect_service先用端口号做初步映射再结合 banner 内容做二次判断。比如 3306 端口返回的 banner 里包含mysql字样就确认是 MySQL。这个逻辑可以扩展成正则匹配把常见服务的指纹都写进去。3.3 把扫描结果写入数据库扫描结果要持久化方便后续生成报告和复查。写库操作放在vuln_check模块里因为漏洞匹配和结果写入是连贯的。# scanner/vuln_check.py import sqlite3 import re def check_vuln(target, port, service_info, db_pathvuln_scanner.db): 匹配漏洞库并写入结果 conn sqlite3.connect(db_path) cursor conn.cursor() # 查询漏洞库中匹配该服务的规则 cursor.execute( SELECT vuln_name, risk_level, description, version_pattern FROM vuln_lib WHERE service_name ?, (service_info[service],) ) rules cursor.fetchall() is_vuln 0 vuln_desc for rule in rules: vuln_name, risk_level, description, pattern rule if pattern and re.search(pattern, service_info[banner], re.IGNORECASE): is_vuln 1 vuln_desc f{vuln_name} ({risk_level}): {description} break # 写入 scan_result 表 cursor.execute( INSERT INTO scan_result (task_id, port, service, banner, is_vuln, vuln_desc) VALUES (?, ?, ?, ?, ?, ?), (1, port, service_info[service], service_info[banner], is_vuln, vuln_desc) ) conn.commit() conn.close() return is_vuln这里task_id暂时写死为 1实际项目中应该在main.py里创建任务后拿到真实 ID 传进来。re.search用IGNORECASE忽略大小写因为 banner 里的大小写不固定。匹配到第一条规则就break避免同一个端口被标记多次。注意SQLite 的?占位符在execute里传参时参数必须是元组。如果只有一个参数要写成(value,)少了逗号会报ProgrammingError。这个坑新手经常踩。4. 漏洞匹配与报告生成让扫描结果能看懂、能答辩4.1 漏洞库的数据结构与匹配策略漏洞库的设计直接决定扫描器的实用性。最简单的做法是「服务名 版本正则 漏洞描述」三字段。但实际匹配时banner 里可能包含多个版本号或者版本号格式不统一。我一般会在vuln_lib里预置几十条常见漏洞覆盖 vsftpd、OpenSSH、Apache、Nginx、MySQL 这些高频服务。匹配策略上优先用正则匹配版本号匹配不到再降级为关键词匹配。比如vsftpd 2.3.4的经典后门正则写vsftpd.*2\.3\.4只要 banner 里出现这个组合就命中。如果 banner 里只有vsftpd没有版本号那就只能标记为「疑似」不能直接判定漏洞。# db/vuln_data.py 中预置数据的片段 VULN_DATA [ (FTP, rvsftpd.*2\.3\.4, vsftpd 2.3.4 后门, 高危, 该版本存在笑脸后门可被远程执行命令), (SSH, rOpenSSH.*[1-7]\.[0-9], OpenSSH 旧版本漏洞, 中危, 旧版本可能存在用户枚举或拒绝服务漏洞), (HTTP, rApache.*2\.4\.[0-9], Apache 2.4.x 解析漏洞, 中危, 多后缀文件解析可能导致绕过), (MySQL, rMySQL.*5\.[0-6], MySQL 旧版本权限问题, 中危, 旧版本存在提权或未授权访问风险), ]导入时遍历这个列表逐条插入vuln_lib表。正则里的\.要转义否则.会匹配任意字符导致误报。[1-7]\.[0-9]这种范围写法能覆盖大部分旧版本但要注意别把新版本也匹配进去。提示漏洞库不要贪多毕业设计预置 20 到 30 条就够。每条都要能说清楚漏洞原理和影响答辩时被问到「你这个规则怎么来的」能答上来比数量多更重要。4.2 用 Jinja2 模板生成 HTML 报告报告生成用 Jinja2 模板引擎比手写字符串拼接优雅得多。先定义一个 HTML 模板然后用扫描结果渲染。# report/generate.py import sqlite3 from jinja2 import Template HTML_TEMPLATE !DOCTYPE html html headmeta charsetutf-8title漏洞扫描报告/title/head body h1扫描报告/h1 p目标{{ target }}/p p扫描时间{{ scan_time }}/p table border1 trth端口/thth服务/ththBanner/thth是否漏洞/thth漏洞描述/th/tr {% for row in results %} tr td{{ row[0] }}/tdtd{{ row[1] }}/tdtd{{ row[2] }}/td td{{ 是 if row[3] else 否 }}/tdtd{{ row[4] }}/td /tr {% endfor %} /table /body /html def generate_report(output_pathreport.html, db_pathvuln_scanner.db): conn sqlite3.connect(db_path) cursor conn.cursor() cursor.execute(SELECT port, service, banner, is_vuln, vuln_desc FROM scan_result ORDER BY port) results cursor.fetchall() cursor.execute(SELECT target_ip, scan_time FROM scan_task ORDER BY id DESC LIMIT 1) task cursor.fetchone() conn.close() template Template(HTML_TEMPLATE) html template.render( targettask[0] if task else 未知, scan_timetask[1] if task else 未知, resultsresults ) with open(output_path, w, encodingutf-8) as f: f.write(html)模板里用{% for %}循环渲染表格行{{ 是 if row[3] else 否 }}是 Jinja2 的条件表达式。render时传入target、scan_time和results三个变量。生成的 HTML 直接用浏览器打开就能看答辩演示很方便。如果你想让报告更专业可以加 CSS 样式把高危漏洞标红。但不要花太多时间在美化上内容完整比好看重要。4.3 扫描性能调优与并发控制扫描 1000 个端口单线程可能要几分钟多线程能压到几秒。但线程不是越多越好我实测在普通笔记本上200 个线程扫局域网目标丢包率明显上升有些开放端口会被误判为关闭。调优思路先按端口范围分段每段用固定线程池扫描扫完一段再扫下一段。这样避免一次性创建太多线程。另外超时时间可以动态调整——第一次扫描用 1 秒超时如果发现大量端口超时说明网络质量差第二次扫描把超时提到 2 秒。# 分段扫描示例 def scan_ports_segmented(target, ports, segment_size500, timeout1.0): all_open [] ports_list list(ports) for i in range(0, len(ports_list), segment_size): segment ports_list[i:isegment_size] open_ports scan_ports(target, segment, timeouttimeout) all_open.extend(open_ports) return sorted(all_open)segment_size设 500每段扫完再扫下一段。这样内存占用稳定也不会因为线程过多导致系统卡顿。如果目标有防护设备分段扫描还能降低被封锁的概率。注意多线程写open_ports列表时append操作在 CPython 里是线程安全的因为 GIL 的存在。但如果你换成multiprocessing就需要用Manager().list()或者加锁。这个区别要搞清楚不然换并发模型时会翻车。5. 避坑与排查那些让扫描器跑不起来的常见问题5.1 端口全开但 banner 为空现象扫描结果显示端口开放但banner字段是空字符串漏洞匹配全部失效。原因目标服务不主动发送 banner或者发送的 banner 被防火墙截断。HTTP 服务如果不发请求就不会返回任何数据某些 SSH 服务配置了Banner none也不返回版本信息。解决在grab_banner里增加主动探测逻辑。对 HTTP 端口发HEAD请求对 SSH 端口发SSH-2.0-Client\r\n对 FTP 端口发USER anonymous\r\n。如果还是空就标记为「banner 不可用」在报告里注明需要手动验证。5.2 扫描本机正常扫外网目标全部超时现象scan_ports扫127.0.0.1秒出结果扫公网 IP 全部返回空列表。原因本机扫描走的是回环接口不经过物理网卡和防火墙。扫外网时目标可能屏蔽了 ICMP 或 TCP SYN或者你的网络出口限制了并发连接数。解决先用ping确认目标可达再用telnet target port手动测试单个端口。如果手动能通但代码不通检查timeout是否太短或者max_threads是否太高导致连接被重置。把线程数降到 50超时提到 3 秒再试。5.3 SQLite 数据库被锁住现象扫描过程中报sqlite3.OperationalError: database is locked。原因多个线程同时写同一个 SQLite 文件SQLite 默认的锁机制不支持高并发写入。check_vuln里每个端口都开一个连接线程多了就互相锁。解决把数据库写入操作集中到一个线程里或者用sqlite3.connect(db_path, timeout10)增加等待时间。更好的做法是扫描阶段只把结果放内存队列扫描结束后统一写库。这样既避免锁冲突也减少 IO 次数。5.4 正则匹配导致误报现象明明服务版本是新的却被标记为存在旧版本漏洞。原因正则写得太宽泛比如OpenSSH.*[1-7]\.[0-9]会把OpenSSH_8.9也匹配进去因为8不在[1-7]里但.*可能匹配到其他位置的数字。解决正则要锚定版本号的位置用\b单词边界或者^开头。比如OpenSSH[_\s]([1-7]\.[0-9])把版本号分组提取出来再判断主版本号是否小于 8。不要依赖单一正则提取出版本号后用代码逻辑判断更可靠。5.5 报告中文乱码现象生成的 HTML 报告在浏览器里打开中文显示为乱码。原因open(output_path, w)没有指定编码Windows 默认用 GBK而 HTML 模板里声明的是 UTF-8。解决写文件时显式指定encodingutf-8并且在 HTML 的head里加meta charsetutf-8。两个地方都写对就不会乱码。这个坑在 Windows 上特别常见Linux 下默认 UTF-8 反而不会遇到。6. 进阶技巧把扫描器从「能跑」推到「能打」6.1 用异步 IO 替代多线程多线程扫描在端口数量大时线程切换开销明显。Python 的asyncio配合asyncio.open_connection能做到单线程并发扫描资源占用更低。核心改动是把scan_single_port改成协程import asyncio async def scan_single_port_async(target, port, timeout1.0): try: reader, writer await asyncio.wait_for( asyncio.open_connection(target, port), timeouttimeout ) writer.close() await writer.wait_closed() return port except (asyncio.TimeoutError, ConnectionRefusedError, OSError): return None async def scan_ports_async(target, ports, timeout1.0, concurrency500): semaphore asyncio.Semaphore(concurrency) async def limited_scan(port): async with semaphore: return await scan_single_port_async(target, port, timeout) tasks [limited_scan(p) for p in ports] results await asyncio.gather(*tasks) return sorted([p for p in results if p is not None])Semaphore控制并发数asyncio.gather收集所有结果。实测扫 1000 个端口异步方案比 100 线程的多线程方案快 30% 左右内存占用也更低。但异步代码调试起来比多线程麻烦如果对性能要求不高多线程够用。6.2 漏洞库的扩展与版本比对预置漏洞库只能覆盖常见服务想让扫描器更实用可以接入 CVE 数据库的离线导出。NVD 提供 JSON 格式的漏洞数据下载后解析出cpe_match字段提取服务名和版本范围转成自己的vuln_lib格式。版本比对不要只用正则用packaging.version.parse做语义化版本比较更准from packaging.version import parse def version_in_range(version_str, min_ver, max_ver): try: v parse(version_str) return parse(min_ver) v parse(max_ver) except Exception: return Falseparse能处理2.3.4、2.3.4-beta这些格式比手动拆字符串靠谱。把vuln_lib的version_pattern改成min_version和max_version两个字段匹配逻辑从正则换成版本比较误报率会明显下降。6.3 扫描结果的验证与复现扫描器报出漏洞后怎么确认不是误报我的习惯是手动复现一次。比如报vsftpd 2.3.4 后门就用telnet target 21连上去看 banner 里有没有vsftpd 2.3.4。如果 banner 对得上再尝试发送USER test:)看服务是否返回530错误——这是后门触发的特征。对于 Web 漏洞用curl -I看响应头里的Server字段和扫描结果比对。如果扫描器报 Apache 2.4.x 解析漏洞但响应头显示Server: nginx那肯定是误报需要检查端口和服务映射逻辑。提示答辩时如果老师问「你怎么保证扫描结果准确」把「正则匹配 版本比对 手动复现」这三层验证讲出来比只说「用了 Nmap」有说服力得多。6.4 我踩过的最大的坑这个项目我做过三版第一版扫本机都报错原因是socket.connect_ex在 Windows 上对localhost解析成了 IPv6 地址::1而目标服务只监听 IPv4。后来强制用socket.AF_INET才解决。第二版数据库锁死扫描 100 个端口后程序卡住改成扫描完统一写库才顺畅。第三版报告乱码加了encodingutf-8才好。这些坑没有一个是「高深技术」但每一个都能让项目跑不起来。毕业设计考察的不是你用了多牛的算法而是你能不能把一条链路完整跑通并且说清楚每一步在干什么。如果你正在做这个方向先把最小可运行版本跑起来再逐步加功能。别一上来就想着接 CVE 数据库、上异步 IO那些是锦上添花不是雪中送炭。希望帮到你。本文还有配套的精品资源点击获取
返回列表