基于Django的Web渗透测试系统:从子域枚举到漏洞报告的设计实践
2026/9/16 15:53:55 网站建设 项目流程

简介:这是一份基于Django的多功能Web安全渗透测试系统毕业设计项目,主要面向计算机相关专业的高校学生,适用于毕业设计、课程设计、期末大作业以及Python Web安全方向的实战练习。项目经导师指导并获高分,代码完整、可直接运行,即使基础较弱也能按说明完成部署与演示。资源包共2000个文件,以275个Python源码文件、1325个SVG图标、71个CSS、67个JS、34个HTML为主体,同时包含SQLite数据库、GeoIP库、日志与字体等,整体16.88MB,前端采用Bootstrap和Xenon/Tabler管理模板,目录结构清晰。当前已有46人学习下载,属于人气逐步上升的优质毕业设计资源。除可运行的完整系统外,另附使用说明与全部配套资料,便于理解整体架构、模块划分、安全检测流程及Django项目配置方法,是完成毕业设计或构建安全测试演示系统的高性价比参考。

1. 基于Django的Web渗透测试系统:课设选题为什么选它

做毕业设计或课程设计时,安全类选题最容易踩两个坑:要么只做前端界面,后台全是mock数据;要么堆一堆工具脚本,没有Web化管理界面,答辩时讲不出工程化的东西。基于Django的多功能Web安全渗透测试系统恰好把两端都补上了——用Django做任务调度、结果展示和报告生成,用底层模块实现子域枚举、端口扫描、SQL注入与XSS检测,构成一条完整的“目标录入→扫描任务→漏洞入库→报告导出”链路。这套设计贴合企业安全测试平台(类似极光扫描器或AWVS)的最小可行实现,既有安全领域的深度,又有Django Web开发的工程量,对找工作的项目经验也是一个能讲透的亮点。源码里附带使用说明和完整项目资料,按步骤跑起来就能演示,适合计算机相关专业的学生以及想上手安全开发方向的Python学习者。

2. 系统架构与Django应用拆分

2.1 MVT模式下的功能模块划分

这个系统的业务流可以抽象成三块:目标管理、扫描执行、结果展示。对应到Django的MVT架构里,Model层承载扫描目标、任务记录、漏洞结果三类核心数据,View层负责接收前端请求并调度扫描任务,Template层用Bootstrap等前端资源搭建操作台界面。项目里能看到xenon.css、tabler.css、bootstrap.css这类样式文件,说明前端采用的是现成的后台管理模板,开发重心放在业务逻辑而不是审美调优,这也是课设项目控制工期的常见策略。

模块拆分我建议按Django app的维度来组织,而不是把所有逻辑塞进一个app,否则后续加功能会很难受:

project_name/ ├── manage.py ├── config/ # 项目配置:settings、urls、wsgi ├── users/ # 用户认证模块 ├── targets/ # 扫描目标管理:域名、IP录入 ├── scanner/ # 扫描引擎:子域枚举、端口扫描、漏洞检测 ├── tasks/ # 异步任务调度:Celery任务定义 └── reports/ # 报告生成:PDF、HTML导出

这样的结构下,每个app承担单一职责,scanner里只写扫描逻辑,不写数据库操作,tasks只做任务调度,不写业务规则。后面做单元测试或替换扫描模块时,影响面被限制在一个app内部,这比把所有视图函数堆在views.py里要安全得多。

2.2 核心数据模型:ScanTask与VulnResult

数据模型是整个系统能不能撑住“多功能”定位的关键。我的建议是至少设计四张核心表:ScanTarget存目标信息,ScanTask存每一次扫描的任务记录,VulnResult存检测出的漏洞详情,ScanReport存报告文件路径。下面是核心模型的参考实现:

# scanner/models.py from django.db import models from django.contrib.auth.models import User class ScanTarget(models.Model): """扫描目标""" name = models.CharField(max_length=255, verbose_name="目标名称") target_url = models.URLField(max_length=500, verbose_name="目标URL") target_type = models.CharField(max_length=20, choices=( ('domain', '域名'), ('ip', 'IP地址'), ('url', '单条URL'), ), default='domain', verbose_name="目标类型") owner = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) description = models.TextField(blank=True, verbose_name="备注") class Meta: db_table = 'scan_target' verbose_name = "扫描目标" class ScanTask(models.Model): """扫描任务""" STATUS_CHOICES = ( ('pending', '等待中'), ('running', '扫描中'), ('completed', '完成'), ('failed', '失败'), ) target = models.ForeignKey(ScanTarget, on_delete=models.CASCADE, related_name='tasks') task_type = models.CharField(max_length=50, verbose_name="扫描类型", help_text="subdomain/port/sqlmap/xss") status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending') progress = models.IntegerField(default=0, verbose_name="进度百分比") created_at = models.DateTimeField(auto_now_add=True) finished_at = models.DateTimeField(null=True, blank=True) logs = models.TextField(blank=True, verbose_name="扫描日志") class Meta: db_table = 'scan_task' ordering = ['-created_at'] class VulnResult(models.Model): """漏洞结果""" SEVERITY_CHOICES = ( ('high', '高危'), ('medium', '中危'), ('low', '低危'), ('info', '信息'), ) task = models.ForeignKey(ScanTask, on_delete=models.CASCADE, related_name='vulns') name = models.CharField(max_length=255, verbose_name="漏洞名称") url = models.URLField(max_length=500, verbose_name="漏洞URL") severity = models.CharField(max_length=20, choices=SEVERITY_CHOICES) description = models.TextField(verbose_name="漏洞描述") evidence = models.TextField(blank=True, verbose_name="证据数据") suggested_fix = models.TextField(blank=True, verbose_name="修复建议") created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = 'vuln_result' indexes = [ models.Index(fields=['severity'], name='idx_vuln_severity'), ]

代码逻辑说明:ScanTask里的task_type字段决定了这次任务跑哪个检测模块,status字段配合后文要讲的Celery异步任务做状态流转。VulnResult独立成表的好处是,一个扫描任务可以产生多条漏洞记录,反向查询通过related_name='vulns'直接拿到,不需要额外写复杂的连表查询。

参数说明:on_delete=models.CASCADE表示删除目标时级联清理关联任务和漏洞,避免脏数据堆积;db_table显式指定表名,便于后期SQL审计或者对接外部数据平台;indexesseverity字段上建索引,因为按漏洞等级筛选是报告页最频繁的操作。

2.3 用户权限与操作审计

渗透测试系统比普通业务系统更注重权限边界,不能让任意注册用户随便扫描任意目标,否则容易被滥用。这里采用两层控制:第一层是Django自带的认证系统,登录后才有操作入口;第二层是数据隔离,普通用户只能看到和删除自己创建的目标任务。

# targets/views.py from django.contrib.auth.decorators import login_required from django.core.exceptions import PermissionDenied from django.shortcuts import get_object_or_404 @login_required def target_detail(request, target_id): target = get_object_or_404(ScanTarget, id=target_id, owner=request.user) tasks = target.tasks.all()[:10] return render(request, 'targets/detail.html', {'target': target, 'tasks': tasks})

这段视图通过owner=request.user做了查询过滤,即使攻击者手动拼URL里的target_id,也拿不到别人的数据。login_required装饰器保证未登录请求会重定向到登录页。这类权限校验逻辑在每个操作类视图中都要写,不能省,答辩时这也是一个很好的技术加分点,可以主动讲“我做了越权数据访问防护”。

3. 扫描引擎与漏洞检测模块实现

3.1 子域枚举:基于字典与DNS解析

子域枚举是信息收集的第一步,常见做法是借助字典文件拼接候选子域,再通过DNS解析判断域名是否存在。Python里可以用socket.gethostbyname,也可以用dnspython库做更精细的解析。下面是一个带并发控制的实现:

# scanner/subdomain.py import socket import concurrent.futures def load_subdomain_dict(path: str) -> list: """加载子域名字典""" with open(path, 'r', encoding='utf-8') as f: return [line.strip() for line in f if line.strip() and not line.startswith('#')] def check_subdomain(domain: str, prefix: str) -> tuple: """校验单个子域名是否存在""" sub = f"{prefix}.{domain}" try: ip = socket.gethostbyname(sub) return (sub, ip, True) except socket.gaierror: return (sub, None, False) def run_subdomain_enum(domain: str, dict_path: str, max_workers: int = 20) -> list: """并发枚举子域""" results = [] prefixes = load_subdomain_dict(dict_path) with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: future_map = {executor.submit(check_subdomain, domain, p): p for p in prefixes} for future in concurrent.futures.as_completed(future_map): sub, ip, exists = future.result() if exists: results.append({"subdomain": sub, "ip": ip}) return results

逻辑说明:ThreadPoolExecutor把字典里的每个前缀丢到线程池里并发解析,max_workers控制并发数,避免一次性发起太多DNS请求被目标服务器的防火墙封IP。as_completed方法会在任意一个future完成时立即返回结果,而不是等待全部完成,这样整体耗时约等于最慢的单次解析时间。

参数说明:max_workers=20在公网DNS解析场景下是一个比较温和的并发值,如果扫描的是内网环境可以降到5~10;socket.gethostbyname收到gaierror异常说明域名不存在或DNS没有响应,这种情况直接过滤掉。注意,这里没有用subprocess调系统命令,所有步骤都是纯Python,这也方便后面接到Celery异步任务里。

3.2 端口扫描:TCP Connect与Banner识别

端口扫描模块我会用TCP Connect模式而不是SYN半开扫描。原因很实际:SYN扫描需要构造原始套接字,在Windows上还要装Npcap,部署成本高,而TCP Connect只需要调用标准库socket,虽然速度稍慢,但对课设项目完全够用,而且不容易被系统防火墙拦截。

# scanner/portscan.py import socket from dataclasses import dataclass from typing import List @dataclass class PortResult: port: int state: str service: str = "" def tcp_connect_scan(host: str, port: int, timeout: float = 1.0) -> bool: """TCP Connect扫描单个端口""" sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(timeout) try: result = sock.connect_ex((host, port)) return result == 0 finally: sock.close() def scan_ports(host: str, ports: List[int], timeout: float = 1.0) -> List[PortResult]: results = [] for port in ports: is_open = tcp_connect_scan(host, port, timeout) if is_open: results.append(PortResult(port=port, state="open")) return results

逻辑说明:connect_exconnect好用之处在于它不会抛异常,而是返回错误码,0表示连接成功,其他值表示不同错误类型。扫描到开放端口后,通常还需要做Banner识别(抓取服务返回的版本信息),这可以在上面代码的is_open分支里再发一次数据接收操作。端口列表可以从配置文件里读取,一般默认扫描常用端口集合:

21,22,23,25,53,80,110,135,139,143,443,445,993,995,1433,1521,3306,3389,5432,6379,8000,8080,8443

参数说明:timeout控制单端口探测超时时间,太短在普通网络环境下会把慢速服务误判为关闭,太长会拖慢整体速度,1.0秒是局域网和公网环境都比较稳妥的值;如果想做快速扫描,可以降到0.3~0.5秒,但要接受误报率上升。

3.3 SQL注入与XSS检测:从请求构造到特征匹配

SQL注入检测的常见思路是:往目标URL的参数里注入payload,然后等价的请求发送两次,一次带payload,一次不带,通过响应体的差异判断是否存在注入。这个差异分析有两种落地方式,一种是基于状态码与响应体长度的简单对比,另一种是基于错误特征(如SQL syntaxmysql_fetch)的匹配。

# scanner/sqli.py import requests PAYLOADS = [ "'", "' OR '1'='1", "' OR 1=1-- ", "') OR ('1'='1--", "' UNION SELECT NULL-- ", ] def request_with_timeout(url: str, params: dict, timeout: int = 5) -> requests.Response | None: """发送带超时的HTTP请求""" try: resp = requests.get(url, params=params, timeout=timeout, verify=False, headers={"User-Agent": "Mozilla/5.0"}) return resp except requests.RequestException: return None def detect_sqli(url: str, param_name: str) -> dict: """检测单个参数的SQL注入""" # 先发正常请求 normal_params = {param_name: "1"} normal_resp = request_with_timeout(url, normal_params) if not normal_resp: return {"param": param_name, "vulnerable": False, "reason": "request_failed"} for payload in PAYLOADS: test_params = {param_name: payload} resp = request_with_timeout(url, test_params) if not resp: continue # 特征1:响应体长度差异 > 20% 或 状态码变化 len_diff = abs(len(resp.text) - len(normal_resp.text)) if len_diff > len(normal_resp.text) * 0.2 or resp.status_code != normal_resp.status_code: return {"param": param_name, "vulnerable": True, "payload": payload} # 特征2:数据库错误关键字 db_errors = ["SQL syntax", "mysql_fetch", "ORA-", "PostgreSQL", "sqlite"] if any(err.lower() in resp.text.lower() for err in db_errors): return {"param": param_name, "vulnerable": True, "payload": payload} return {"param": param_name, "vulnerable": False}

逻辑说明:这个检测器先以正常参数值建立基线,然后逐个注入payload,通过响应差异判断注入点。只做响应长度对比会有很多误报,所以第二层加了数据库错误特征匹配,只有命中错误关键字时才确认漏洞。verify=False关闭SSL证书校验是因为很多测试站点使用自签名证书,验证会直接请求失败。

参数说明:PAYLOADS里每条payload针对不同的注入点类型,第一行单引号用于触发数据库语法错误,第3、4行是布尔盲注绕过用的;timeout=5控制单次请求的超时,防止目标响应慢导致任务长时间卡住。检测接口在真实使用时要限定请求频率,否则完全可能把目标站点打宕,这也是安全测试工具的伦理边界。

XSS检测的原理逻辑类似,只是payload换成<script>alert(document.cookie)</script>这类脚本标签,然后在响应体中检查未经过滤的payload回显。这里不额外贴代码,核心就是“提交payload → 检索响应体是否包含payload原文”。

3.4 误报处理与漏洞确证

自动扫描必然会遇到误报,尤其是基于响应差异的检测方式。一个有效的处理策略是将扫描结果标记为“疑似漏洞”,随后用独立的验证模块重新请求一次,只有验证请求也满足漏洞特征才把状态改为“已确认”。验证时可以换一组语义相同但写法不同的payload,比如URL编码后的%27%20OR%201%3D1--,如果两种不同编码都能触发同样的异常,就可以确信不是巧合。

4. 任务调度、异步执行与报告导出

4.1 Celery接入与任务状态流转

Web安全扫描是典型的耗时任务:一个子域枚举加端口扫描可能要跑几分钟,如果直接在Django视图里同步执行,浏览器请求会一直挂着,可能触发超时。这里采用Celery + Redis的异步任务方案,视图只需创建一条扫描任务记录,然后把真正的扫描工作交给Celery worker执行。

# tasks/tasks.py from celery import shared_task from django.utils import timezone from scanner.subdomain import run_subdomain_enum from scanner.portscan import scan_ports from scanner.sqli import detect_sqli from scanner.models import ScanTask, VulnResult @shared_task(bind=True, max_retries=3, default_retry_delay=30) def execute_scan_task(self, task_id: int): """执行扫描任务""" task_obj = ScanTask.objects.get(id=task_id) task_obj.status = 'running' task_obj.save(update_fields=['status']) try: target = task_obj.target if task_obj.task_type == 'subdomain': task_obj.logs = "开始子域枚举\n" task_obj.progress = 30 task_obj.save(update_fields=['logs', 'progress']) results = run_subdomain_enum(target.target_url, 'dict/subdomain.txt') task_obj.logs += f"发现 {len(results)} 个子域: {results}" task_obj.progress = 100 elif task_obj.task_type == 'portscan': task_obj.progress = 10 task_obj.save(update_fields=['progress']) ports = [21, 22, 23, 25, 53, 80, 110, 135, 139, 143, 443, 445, 993, 995, 1433, 1521, 3306, 3389, 5432, 6379, 8080, 8443] open_ports = scan_ports(target.target_url, ports) task_obj.logs = f"开放端口: {[p.port for p in open_ports]}" task_obj.progress = 100 task_obj.status = 'completed' task_obj.finished_at = timezone.now() task_obj.save() except Exception as exc: # 重试3次,每次间隔30秒 raise self.retry(exc=exc, countdown=30)

逻辑说明:bind=True让任务函数能访问它自己实例self,这样才能调用self.retry()实现失败重试。update_fields是Django ORM的一个优化点,只更新指定字段,能减少生成UPDATE语句的字段数量,对频繁状态变更很有效。

参数说明:max_retries=3意味着最多执行4次,第1次算原始执行,之后重试3次;default_retry_delay=30是重试间隔秒数。异常发生时任务不会直接标记为failed,而是先进入重试队列,重试次数用尽后Celery会把任务标记为失败,这时Django里的ScanTask.status才需要更新为failed。

任务状态流转的对照关系如下:

Celery任务状态Django ScanTask状态说明
PENDINGpending任务已创建入队
RECEIVED / STARTEDrunningworker开始执行
SUCCESScompleted正常完成
RETRYrunning失败重试中
FAILUREfailed重试次数耗尽

这个表在答辩时画成流程图也很有说服力,能直接体现你理解异步任务的生命周期。

4.2 扫描报告的PDF导出

报告模块是这类系统的门面,也是容易被课设项目忽略的部分。基础的实现是把漏洞信息渲染成HTML模板,再用第三方库转成PDF。比较省事的方案是weasyprint,它能把带CSS样式的HTML转成PDF,不需要额外安装LaTeX之类的重型依赖。

# reports/generator.py from django.template.loader import render_to_string from weasyprint import HTML from pathlib import Path def generate_pdf_report(task_id: int, output_path: str): """根据扫描任务生成PDF报告""" task = ScanTask.objects.select_related('target').get(id=task_id) vulns = task.vulns.all().order_by('-severity') html_content = render_to_string('reports/report_template.html', { 'task': task, 'vulns': vulns, }) # 渲染HTML为PDF HTML(string=html_content).write_pdf(output_path) return output_path

逻辑说明:select_related('target')通过SQL的JOIN一次性把外键关联的目标数据查出来,避免每访问一次task.target就多一次数据库查询,这在生成报告时要遍历很多漏洞记录的场景下能显著减少SQL执行次数。render_to_string把Django模板渲染成纯HTML字符串,交给weasyprint时注意模板里的静态资源路径要写成绝对路径,否则PDF里会漏掉图片和样式。

参数说明:output_path建议用settings.MEDIA_ROOT/reports/{task_id}_{date}.pdf的格式,这样在Django的MEDIA_URL下可以直接通过浏览器访问下载。报告模板里要展示漏洞等级分布,可以在视图层用ORM聚合:

from django.db.models import Count severity_stats = task.vulns.values('severity').annotate(count=Count('id'))

这段代码返回的是按severity分组的统计列表,例如[{'severity': 'high', 'count': 3}, {'severity': 'low', 'count': 1}],把它传进模板后可以用表格或柱状图展示,不需要额外的图表库。

4.3 前后端联动与轮询刷新

前端页面需要实时展示扫描进度,常见做法是前端每隔3~5秒轮询一次任务状态接口,而不是用WebSocket——WebSocket在小项目里引入会增加部署复杂度。轮询接口返回JSON,前端用setInterval刷新进度条:

# tasks/views.py from django.http import JsonResponse from scanner.models import ScanTask def task_progress_api(request, task_id: int): """任务进度查询API""" task = ScanTask.objects.get(id=task_id, owner=request.user) data = { 'status': task.status, 'progress': task.progress, 'logs': task.logs, } return JsonResponse(data)

接口只返回三个字段,前端拿到后更新页面上的进度条值和日志区文本。轮询间隔建议设置成3000毫秒,太短会给服务器造成不必要的压力,太长会导致进度条跳动感明显。这里没有用Django REST Framework,纯手写JsonResponse就能完成需求,少引一个依赖对课设项目来说更可控。

5. 运行部署、使用说明与高频排错

5.1 环境依赖清单

系统的运行环境依赖集中在requirements.txt里,核心依赖如下:

Django==4.2.7 celery==5.3.6 redis==5.0.3 django-celery-beat==2.6.0 requests==2.31.0 weasyprint==60.2 python-dotenv==1.0.0 gunicorn==21.2.0

版本说明:Django 4.2是当前LTS版本,安全更新周期覆盖到2026年,比5.x更稳当。Celery 5.3.x与Django 4.2的兼容性已经过广泛验证。weasyprint 60版本要求Python 3.9+,如果本机是Python 3.8,需要降到52之前的版本。

5.2 初始化与启动步骤

拿到项目源码后的启动路径如下,按顺序执行不要跳步:

# 1. 创建虚拟环境并安装依赖 python3 -m venv venv source venv/bin/activate # Windows: venv\Scripts\activate pip install -r requirements.txt # 2. 迁移数据库 python manage.py makemigrations users targets scanner tasks reports python manage.py migrate # 3. 创建超级管理员 python manage.py createsuperuser # 4. 启动Django服务与Celery python manage.py runserver 0.0.0.0:8000 # 新终端窗口,启动Redis(需提前安装Redis) redis-server # 新终端窗口,启动Celery worker celery -A config worker -l info

逻辑说明:makemigrations后面显式指定了app名称,这是因为不同app里的模型有外键依赖,分开迁移会避免生成损坏的迁移文件。Celery worker必须和Django进程使用同一个数据库和Redis实例,worker启动后会自动接收队列里的扫描任务。-l info表示日志级别为info,能看到任务接收和执行的实时状态。启动完成后,浏览器访问http://127.0.0.1:8000进入系统首页,先登录,然后进入目标管理页面添加一个目标,触发扫描任务后,在任务列表里能看到状态从“等待中”变成“扫描中”再到“完成”。

注意:使用系统扫描的任何目标都必须经过目标所有者书面授权,仅限本人拥有或授权测试的服务器,这不是免责声明,而是安全测试工作的基本职业规范。

5.3 高频问题与修复对照表

现象可能原因解决方式
访问页面报TemplateDoesNotExist前端模板文件路径未注册检查settings.pyDIRS是否包含模板根目录,必要时重启服务
Celery任务一直卡在pendingRedis未启动或worker未启动redis-cli ping确认Redis存活;celery -A config worker -l info看worker是否挂起
扫描任务报ConnectionRefusedError目标不可达或端口被防火墙拦截先在本机telnet 目标IP 端口验证连通性
PDF报告中文乱码weasyprint缺少中文字体apt install fonts-noto-cjk安装Noto CJK字体,然后重新生成
轮询接口返回403CSRF校验未通过在AJAX请求头加X-CSRFToken,值从cookie中读取

最隐蔽的一个坑是all.css等静态资源加载404,检查项目根目录下static/文件夹是否存在,以及settings.STATICFILES_DIRS路径是否指向正确。源码包里如果附带的是压缩后的CSS文件(可以看到xenon.css、tabler.css这类命名),要注意DEBUG=False时Django不会自动提供静态文件服务,必须执行python manage.py collectstatic收集到指定目录。这个问题在部署到宝塔或云服务器时特别常见,每次更新CSS后没有重新collect,浏览器缓存又加载旧文件,排查很久才发现是静态文件问题。

最后一招排错技巧:在settings.py里开一个LOG_LEVEL=DEBUG的日志配置,把SQL和请求日志打到logs/文件夹,当扫描结果数量不对时打开日志看ORM实际执行的SQL语句,比在代码里到处写print高效得多。

另外建议在交付项目前把python manage.py check --deploy跑一遍,它会自动检查settings里的安全隐患,比如DEBUG=TrueALLOWED_HOSTS为空、Cookie未设置Secure标记等。这个命令给出的警告项大多能在几分钟内修完,但答辩时讲到“我做了生产环境安全加固”,效果比多写几个功能模块还要加分。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询