Python漏洞扫描系统实战:从端口扫描到定时巡检
发布时间:2026/9/28 1:37:48来源:尧图网络
简介面向Python学习者和毕业设计开发者的漏洞扫描系统完整项目基于Django框架实现包含项目源码、数据库脚本、配套论文文档及PPT适合用于课程设计、毕业设计和技术学习。项目共380个文件压缩包大小为83.89MB文件类型涵盖Python源码.py/.pyc、数据库脚本.sql、前端样式与脚本.css/.js、演示动图.gif以及办公文档等目录组织清晰便于按模块查阅。目前已有182人学习适合需要参考完整项目结构、学习Django开发或完成毕设课题的读者可帮助掌握Web应用开发与常见漏洞扫描功能的实现思路。通过分析源代码能深入理解系统的整体架构、Django开发模式、数据库集成及接口实现数据库脚本可直接导入数据库构建立即可用的运行环境配套文档和PPT则便于项目说明与答辩展示既支撑毕业设计答辩也可作为技术分享与二次开发的实战基础。1. Python漏洞扫描系统毕设项目里你以为的“扫描”到底在扫什么一个Python漏洞扫描系统的工程量通常不在“扫描”本身而在数据库脚本的初始化、指纹规则的维护、结果落库和页面展示这一整条链路上。很多拿到这种项目源码的人第一步不是写代码而是先把环境跑起来再对照文档理解每张表是干什么用的。这个方向适合网络安全方向的毕设、转行安全岗位的项目经历也适合想给团队做个极简巡检工具的后端工程师。它扫的是授权目标。扫描器先判断目标端口是否开放再识别端口上跑的中间件和版本最后把命中规则的结果写进数据库形成一份可以筛选、导出的报告。LW、PPT和源码包说明这是一套完整的毕业设计产出物你需要做的不是重写而是看懂它的分层结构再把扫描任务从“一次跑完”改成“能定时跑、能看差异、能不误报”。2. 先看系统的三层骨架扫描引擎、规则库存哪、结果怎么落2.1 TCP端口扫描为什么是这一切的地基大多数漏洞扫描系统的第一步都长一个样先探测目标主机哪些TCP端口是开放的。这一步决定了后续所有指纹识别和漏洞探测往哪儿发请求。常见的实现方式叫TCP connect扫描也就是直接用socket去和目标端口完成一次完整的三次握手握手成功就认为端口开放。Python里做这个动作的常用接口是connect_ex()它比connect()更好用因为connect()失败会直接抛异常而connect_ex()返回一个整数0表示连接成功其余值对应常见的errno。import socket def tcp_connect_scan(ip: str, port: int, timeout: float 1.0) - bool: # 创建一个TCP套接字AF_INET表示IPv4SOCK_STREAM表示流式连接 client socket.socket(socket.AF_INET, socket.SOCK_STREAM) # timeout是单个端口的等待上限单位秒设太短容易漏报 client.settimeout(timeout) code client.connect_ex((ip, port)) client.close() return code 0这段逻辑里需要注意的参数是timeout。1.0表示对一个端口最多等1秒如果目标主机在公网上、网络抖动明显1秒可能不够但如果扫的是内网192.168.x.x段1秒能明显加快整体速度。毕设项目里通常把这个参数放在扫描任务的配置里而不是写死在函数里方便按场景调整。connect_ex之所以比connect常用就是因为它的返回值能直接进入分支判断少一层try/except的语义噪音。还有一种SYN扫描方式更快它不完成完整握手只发SYN包然后看回包但Python标准库做不到这件事需要scapy库或裸raw socket而且在大多数操作系统上需要root权限。毕设和拿来做内网巡检的场景基本不碰这层用TCP connect扫描就够应付论文里的“漏洞扫描系统”设计了。避免“扫不到就把锅甩到扫描方式上”的常见误解。2.2 指纹匹配与Web探测从端口开放到服务识别端口开放只是第一个信号真正给结果定性的是端口上跑的服务和版本。系统会先抓banner也就是服务监听端口之后主动回给客户端的那段欢迎信息。SSH回SSH-2.0-OpenSSH_7.6p1Nginx的HTTP响应头里带Server: nginx/1.18.0这些字符串就是指纹规则要匹配的对象。这里有一个很容易被忽略的点HTTP服务虽然同时监听80和8080但两种端口对应的应用可能完全不同。所以指纹规则的匹配条件里一般会写上端口约束避免把8080上跑的Tomcat当成Nginx。我见过不少毕设系统的规则库里没有端口字段结果拿80端口规则去匹配8080端口误报率直线上升。FINGERPRINT_RULES [ { name: nginx, default_port: [80, 443], hints: [Server: nginx, nginx/], }, { name: Apache Tomcat, default_port: [8080, 8000], hints: [Apache-Coyote/1.1, Server: Apache-Coyote], }, ] def match_fingerprint(service: str, banner: str, port: int) - list: matched [] for rule in FINGERPRINT_RULES: # 端口先过滤避免跨端口误匹配 if port not in rule[default_port]: continue for hint in rule[hints]: if hint.lower() in banner.lower(): matched.append(rule[name]) break return matched这段代码里的大小写折叠很关键。Banner格式在真实环境中极不规范有的回Server: nginx有的回Server: Nginx不统一转小写就会漏报。default_port数组的用意是让一条规则覆盖多个常见端口但前提是确认业务确实可能部署在其中每个端口上。把规则挂在名称不存在的端口上只会让报告里的服务识别结果看起来毫无章法。2.3 数据库脚本里的三张核心表与存储逻辑数据库脚本在这个项目里的地位被严重低估了。它不仅是建表还承担了两项任务初始化漏洞规则数据以及给前端报表提供可查询的模型。大部分毕设项目的数据库脚本是一个.sql文件里面顺序通常是建库、建表、插入规则数据。三张表是避不开的scan_task记录每次扫描任务scan_result存具体端口和服务发现结果vuln_rule是漏洞规则库。下面这张scan_task表是这类系统里最常见的形态DROP TABLE IF EXISTS scan_task; CREATE TABLE scan_task ( id INT AUTO_INCREMENT PRIMARY KEY, task_name VARCHAR(64) NOT NULL COMMENT 任务名称, target VARCHAR(128) NOT NULL COMMENT 目标地址IP、网段或域名, port_range VARCHAR(64) DEFAULT 1-1000 COMMENT 端口范围, status TINYINT NOT NULL DEFAULT 0 COMMENT 0等待 1运行中 2完成 3失败, started_at DATETIME NULL COMMENT 开始时间, finished_at DATETIME NULL COMMENT 结束时间, KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT扫描任务表;port_range用字符串而不是用整数拆成两个字段是因为不同来源的任务表达习惯不一样有的写1-65535有的写80,443,3306。改成字符串可以省去一层解析坏处是排序和区间过滤变麻烦。系统内部一般把字符串转成端口列表再交给扫描引擎。scan_result表比scan_task表更容易出设计问题。常见字段是id、task_id、host、port、service、risk_level、rule_id、description、found_at并且会给(host, port, rule_id)加唯一索引防止同一任务重复上报同一条漏洞。risk_level一般为0到4的整数对应低、中、高、严重前端报表拿到这个字段后直接渲染颜色。vuln_rule表则存规则的元信息规则名称、漏洞类型、匹配指纹、修复建议、参考链接。匹配指纹这一列在毕设项目里可能就是一条正则表达式或一个包含多个关键字的JSON字符串。数据库脚本初始化时往这张表插几十条常见规则的记录CVE和具体PoC一般不会全量放在这里而是放在文档里说明原理。3. 把项目源码部署起来数据库脚本、依赖安装与第一次扫描3.1 环境准备与依赖安装requirements.txt里到底有什么拿到源码包之后不要急着跑主程序。这类项目十有八九依赖第三方库直接把项目放在系统全局Python环境里装依赖很可能会和机器上已有的包冲突。常见做法是建虚拟环境把隔离问题挡在第一步。下面是我在部署这类源码包时的标准流程cd vuln_scanner python -m venv .venv source .venv/bin/activate pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/python -m venv .venv是Python 3.3之后标准库自带的能力不需要额外安装virtualenv。-i参数指定的是国内镜像源在不方便直连PyPI的网络环境里能明显缩短下载时间如果你已有可用源换掉这一行也不影响结果。requirements.txt里通常锁着几个固定依赖requests做HTTP请求flask写管理页面pymysql连MySQLpython-nmap可选用来调用nmap做端口扫描增强。版本号一般会写成requests2.28.1这种精确锁定形式因为漏洞扫描系统对响应包解析很敏感requests大版本升级可能改变默认行为导致字段解析失败。如果requirements.txt里没有锁版本建议你手动把它们固定成当前能跑通的版本做好这件事能省掉后面一多半排错时间。3.2 执行数据库脚本建库、导规则、初始化管理员数据库脚本是这个项目的核心资产执行顺序错了或者字符集不对后面全白搭。我比较推荐的验证方式是先建库再导入脚本最后查一下规则数量确认脚本真的生效了。mysql -uroot -p -e CREATE DATABASE IF NOT EXISTS vuln_scan DEFAULT CHARACTER SET utf8mb4; mysql -uroot -p vuln_scan sql/init.sql mysql -uroot -p -e SELECT COUNT(*) FROM vuln_rule;第一行命令指定了utf8mb4字符集这一步不是可选项。如果漏掉脚本里的中文漏洞描述大概率以latin1或utf8入库后面页面展示就是一片问号。第二行把整个init.sql喂给vuln_scan库脚本里通常会包含建表语句和INSERT语句顺序是固定的不要拆开执行。init.sql里大概率还会插入一个管理员账号。因为论文和答辩演示需要展示登录页面源码里默认账号密码一般是admin/admin123之类这类默认口令只适合演示环境部署到真实内网一定要先改掉。导入完成后用第三行命令确认规则表有数据如果COUNT(*)返回0说明脚本里可能带了一个单独的数据文件你需要继续找import_data.sql或者data.sql把它也导入一遍。3.3 命令行跑通第一次扫描并核对结果大部分此类项目的入口是cli.py它接收目标地址、端口范围和输出文件三个参数。第一次扫描建议先扫本机或者一台你完全可控的虚拟机不要一上来就打网关或者公网段先确认链路能通再扩大范围。python cli.py -t 127.0.0.1 -p 1-1000 -o report.html这条命令的意思是对127.0.0.1的1到1000号端口做扫描结果渲染成report.html。扫描结束后去数据库里验证结果有没有真正落进去SELECT task_id, host, port, service, risk_level FROM scan_result ORDER BY found_at DESC LIMIT 10;如果表里查到刚才那台主机的开放端口说明从扫描引擎到数据库的完整链路没问题。如果scan_result是空的优先去查scan_task表里任务是不是处于“2完成”状态如果任务状态还停在“1运行中”多半是扫描线程卡在某个端口上需要回头调整超时时间如果状态已完成却没有结果问题大概率出在结果上报环节也就是扫描结果没有正确写入到数据库。report.html只是给人看的展示层真正可靠的数据源是数据库里的scan_result表。你应该养成习惯以数据库查询结果为准HTML报告只在答辩和汇报时使用。4. 核心代码拆解线程池、指纹规则与结果落库4.1 单线程扫描器如何改成并发任务队列刚跑通的扫描器大概率是单线程的也就是一个端口一个端口连一遍扫一个B段几百台主机可能要跑几小时。论文里能写但实用性不行。改造思路是用线程池加队列把端口分发到多线程去并发连接每个工作线程负责从队列里取端口扫描结果写回共享列表。import socket import threading from queue import Queue from typing import List def check_port(ip: str, port: int, timeout: float) - bool: sock socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(timeout) try: result sock.connect_ex((ip, port)) return result 0 finally: sock.close() def scan_ports_concurrent(ip: str, ports: List[int], workers: int 50, timeout: float 1.0) - List[int]: task_queue Queue() result_lock threading.Lock() open_ports [] for port in ports: task_queue.put(port) def worker() - None: while True: try: port task_queue.get_nowait() except Queue.Empty: break if check_port(ip, port, timeout): with result_lock: open_ports.append(port) task_queue.task_done() thread_list [] for _ in range(min(workers, len(ports))): t threading.Thread(targetworker) t.start() thread_list.append(t) for t in thread_list: t.join() return sorted(open_ports)这段代码里有三个参数值得留意workers控制并发线程数线程数不是越大越好50在大多数普通网络环境比较均衡设到200以上容易触发目标主机上的防护策略导致后续请求被bantimeout沿用前面的单端口超时result_lock是必需的因为多个线程同时往open_ports里append会在极端情况下丢数据加锁后屏蔽掉这个风险。Queue.Empty是新版Python里推荐的写法用来替代捕获queue.Empty的旧写法。get_nowait()在队列为空时立刻抛异常配合while True构成优雅退出条件不会出现线程卡死在get()上的问题。t.join()的作用是等所有线程都退出后再返回结果列表保证外面拿到的列表是完整的。4.2 指纹匹配用响应头判定中间件指纹匹配在扫描链路中的位置是端口确认开放后、写结果表之前。它的输入是探测时抓到的banner输出是服务名称和风险等级。HTTP服务通常直接发一个HEAD请求然后把Server和X-Powered-By两个响应头作为匹配素材SSH、MySQL这类非HTTP协议就依赖端口回显的握手包。import requests def detect_http_server(host: str, port: int, timeout: float 3.0) - str: # 只取响应头不下载body减少扫描流量和时间 url fhttp://{host}:{port}/ try: resp requests.head(url, timeouttimeout, allow_redirectsTrue) header_map dict(resp.headers) except requests.RequestException: return unknown server_value header_map.get(Server, ).lower() powered_by header_map.get(X-Powered-By, ).lower() if nginx in server_value: return nginx if apache in server_value: return apache if openresty in server_value or openresty in powered_by: return openresty return server_value or powered_by or unknownrequests.head()比requests.get()更适合指纹探测因为HEAD请求不会被Web应用真正解析能把对目标的影响降到最低速度也更快。allow_redirectsTrue是必须保留的因为很多站会把根路径重定向到/index.html不走重定向就拿不到真正的响应头。这个方法能识别常见中间件但覆盖不了加了WAF或反代混淆的站点。真实场景里Nginx前面可能挂一层云WAF返回的Server头变成WAF或一串随机字符串这时候指纹库再怎么加规则都匹配不上。问题不在于代码逻辑而在于规则集的覆盖度。我一般会在设计文档里写清楚指纹识别只能作为参考不能作为漏洞判定的唯一依据。4.3 结果入库与前端展示结果表的字段设计扫描结果入库是整套系统里最容易出性能问题的一环。每扫到一个端口就执行一次INSERT扫描量大了之后数据库连接会被频繁打开关闭拖慢整体速度。常见做法是把单条入库改成批量入库要么攒一批再写要么用executemany()一次提交多行。import pymysql from datetime import datetime def save_results_batch(results: list, task_id: int, db_config: dict) - int: connection pymysql.connect(**db_config, charsetutf8mb4) try: now datetime.now() rows [] for r in results: rows.append(( task_id, r[host], r[port], r[service], r[risk_level], r[description], now, )) with connection.cursor() as cursor: sql ( INSERT INTO scan_result (task_id, host, port, service, risk_level, description, found_at) VALUES (%s, %s, %s, %s, %s, %s, %s) ) # executemany提交一个列表一次网络往返写入多行优于循环execute cursor.executemany(sql, rows) connection.commit() return len(rows) except Exception as e: connection.rollback() raise RuntimeError(f批量入库失败: {e}) finally: connection.close()executemany()会把二维列表压缩成一条带多个参数组的SQL发给MySQL传输次数从N次降到1次。connection.rollback()放在异常分支是防止写了一半时数据库停留在不确定状态。charsetutf8mb4这句必须和建库时保持一致否则中文描述写入MySQL后可能变成乱码标记前端展示时再读出来又是一堆问号。前端展示这一层Flask模板通常只做两件事按任务查询结果列表按risk_level渲染彩色标签。状态统计卡片的数据来源是SQL聚合查询比如按风险等级分组统计数量这部分逻辑简单但要注意在模板里不要把None和0混为一谈否则前端可能出现空白行。5. 避坑指南漏报、误报、扫不动的四类现场5.1 端口明明开放却扫不到超时参数没调对现象是同一台主机用nmap扫出了80、443自己的扫描器却只报443。原因是默认timeout1.0在公网或者目标主机CPU被打满的时候TCP握手响应超过1秒连接被当作超时丢弃。超时太短导致漏报在实时扫描任务里很常见。解决分两步。第一步把单一timeout拆成连接超时和读超时连接阶段等3秒读banner阶段再多给2秒第二步在扫描配置里允许按网段覆盖默认值比如内网段用1.0公网段用3.0。这个参数应该暴露成命令行参数而不是写死在代码里因为不同批次的巡检任务网络质量可能完全不同。5.2 数据库写入乱码和中文规则丢失现象是漏洞描述在MySQL客户端里看着正常但扫描系统页面上全是?????或者导入init.sql之后规则库里的中文修复建议变成乱码。原因基本可以锁定在两处建库时没有指定utf8mb4字符集或者连接数据库时没有显式声明charsetutf8mb4。解决的检查顺序是先看建库语句是否带了DEFAULT CHARACTER SET utf8mb4如果没有把库删掉按正确字符集重建再确认init.sql文件本身是UTF-8编码而不是Windows记事本默认的ANSI编码可以通过file -bi sql/init.sql查看编码最后检查代码里的数据库连接参数pymysql.connect()里加上charsetutf8mb4。这三步做完乱码问题基本不会再出现。5.3 requests库升级后出现SSL报错现象是某一个依赖版本升级后原本能正常扫描的HTTPS站点突然全部报SSLError连日志都被刷屏。原因是目标站点还在用TLS 1.0或TLS 1.1协议而新版本requests和urllib3默认只启用TLS 1.2以上握手直接失败。这在老企业内部系统上尤其常见。解决方式有两条路。稳妥一点是给requests的HTTPS请求指定适配器允许它降级到TLS 1.0图省事的话对已知的测试站点关闭证书校验即verifyFalse但这一步要在代码注释里明确标注“仅限授权测试环境”避免被误当成生产系统的默认行为。我的建议是优先走降级方案而不是关证书校验因为一旦开了verifyFalse规则里所有HTTPS请求都会失去证书信任问题之后排查别的连接问题时会多一个干扰源。5.4 并发扫描把结果表写乱了现象是单线程扫描结果正常改成线程池之后就出现结果缺失、重复记录、甚至任务状态没更新。原因是多线程同时连接MySQL共用同一个连接对象或者结果列表被多个线程同时写入。数据库连接不是线程安全的这是并发改造里最典型的坑。解决方法是给每个线程用独立的数据库连接或者把扫描结果先汇总到内存列表等所有线程结束之后再统一做批量入库。优先级最高的是杜绝多个线程读写同一个connection这会造成连接内部状态错乱。第二个选择是给scan_result表加唯一索引(host, port, rule_id)这样即使偶发重复上报数据库也能拦截掉重复行让报告数据保持干净。6. 进阶把扫描任务做成定时巡检并接入告警扫描器跑通一次只是起点更有价值的用法是让它按计划巡检对比两次结果之间的差异把新出现的开放端口及时报出来。这里的骨架是调度器加扫描命令调度器负责触发时机扫描命令负责执行任务。我一般用APScheduler而不是系统cron因为APScheduler可以随应用一起启停不需要修改系统的crontab配置。from apscheduler.schedulers.blocking import BlockingScheduler import subprocess from datetime import datetime def nightly_scan() - None: timestamp datetime.now().strftime(%Y%m%d_%H%M) report_name freport_{timestamp}.html subprocess.run( [python, cli.py, -t, 192.168.1.0/24, -p, 1-65535, -o, report_name], checkFalse, ) scheduler BlockingScheduler() # 每天凌晨2点跑一次避开业务高峰 scheduler.add_job(nightly_scan, cron, hour2, minute0, idnight_scan) scheduler.start()做完定时扫描之后可以把两次任务的结果表做一次对比查询用LEFT JOIN找出本次新发现的端口再把差异结果发到Webhook或邮件。这个能力才是扫描系统从毕设作品走向实用工具的拐点。而把这个增量对比做成一张报告表前后两次端口开放一目了然也就解决了“扫描结果没人看”的问题。我自己习惯在每个规则入库时都留一条可复现命令比如curl -v http://10.0.0.5/test.jsp这样后续出现误报时能快速回放而不是翻半天代码。把扫描器从“跑通一次”变成“每天都在跑”才是这类项目最大的价值所在。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网