简介这套基于Python-Django的多功能Web安全渗透测试工具源码包面向安全测试人员、Python开发者和网络安全相关专业学习者解决Web应用安全评估与漏洞发现的实际需求。系统集成漏洞检测、目录识别、端口扫描、指纹识别、域名探测、旁站探测、信息泄露检测等模块通过资产与信息收集分析安全隐患输出漏洞危害等级并附修复建议。资源共2000个文件压缩包约19.61MB其中275个Python文件为系统核心代码1325个SVG配合CSS、JS构成前端界面另包含日志、HTML模板、SQLite数据库、Markdown说明及Word使用文档等便于快速理解模块划分和二次开发。附带使用说明与项目代码可结合实践学习完整渗透测试流程适合作为毕业设计、课程项目或安全工具开发进阶参考。已有486人学习/下载。1. 为什么一款Web渗透测试工具会选Django做骨架把Web安全渗透测试工具做成Django项目最早看起来是个偏“重”的选择——明明用脚本加几个requests循环就能扫完的目标何必套一层框架但实际跑过几轮批量扫描后会发现渗透测试真正费时间的不是发HTTP请求而是怎么管理“测什么、谁授权、扫出什么、怎么复核、怎么出报告”这一整条链路。Django在这类工具里承担的正是管理面和数据面的底座自带Admin后台可以直接把扫到的东西变成可审核的工单ORM把目标、端口、漏洞、任务状态这些关系型数据落到数据库里再配合异步线程或Celery承接扫描调度。所以与其说这个标题在讲“用Django写扫描器”不如说在讲“用Django把扫描器的结果管理起来并让检测能力以插件形式长出来”。对测试工程师来说它解决的是批量目标巡检、漏洞持续追踪、报告可导出的问题对Django开发者来说这套代码则是理解框架跟安全能力如何结合的比较完整的参考样本。2. 搭建独立扫描工程项目结构、数据模型与异步任务这类工具最常见的落地方式不是把扫描逻辑塞进views.py而是拆成多个应用每个应用管一条业务线。项目一开始拆得清楚后面检测插件越多的时候越不会出现互相import的泥潭。2.1 初始化Django项目时按功能模块拆分应用django-admin startproject pentest_platform cd pentest_platform python manage.py startapp targets python manage.py startapp scanners python manage.py startapp reports python manage.py startapp users四个应用的分工用一张表就能看明白应用名职责核心模型targets目标资产管理域名、IP、授权状态、授权书Targetscanners扫描任务调度、检测插件、漏洞结果ScanTask, Vulnerabilityreports报告生成、导出、聚合查询Reportusers用户、角色、操作日志、API凭据UserProfile应用拆完后要在settings.py的INSTALLED_APPS里按顺序注册同时把语言和时区调整成国内使用习惯INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, targets, scanners, reports, users, ] LANGUAGE_CODE zh-hans TIME_ZONE Asia/Shanghai USE_I18N True USE_TZ TrueUSE_TZ保持True但把TIME_ZONE设置成Asia/Shanghai这样Admin后台和导出的报告里时间戳不会差8个小时。很多新手在这里只改LANGUAGE_CODE不动TIME_ZONE结果漏洞发现时间显示的是UTC排查问题时会多绕一圈。2.2 设计扫描目标、任务、漏洞三条核心表2.2.1 targets应用里的Target模型from django.db import models class Target(models.Model): AUTH_STATUS ( (pending, 待授权), (authorized, 已授权), (rejected, 已拒绝), (expired, 已过期), ) domain models.CharField(域名, max_length255, blankTrue) ip models.GenericIPAddressField(IP地址, nullTrue, blankTrue) owner models.CharField(负责人, max_length64, blankTrue) auth_status models.CharField(授权状态, max_length20, choicesAUTH_STATUS, defaultpending) auth_file models.FileField(授权书, upload_toauth/, blankTrue, nullTrue) created_at models.DateTimeField(创建时间, auto_now_addTrue) def __str__(self): return self.domain or self.ip or str(self.id) class Meta: db_table tb_target ordering [-created_at]domain和ip都允许为空但业务上至少要填一个所以__str__里做了兜底。auth_status用字符串choices而不是布尔值因为授权状态不只是“是否授权”还涉及待审核、过期字符串枚举在Admin的list_filter筛选和报表导出里都比布尔值友好得多。auth_file存放的是授权书扫描件这是企业内部安全测试工具必须保留的证据链扫描脚本里也应该校验这个字段是否已上传。2.2.2 scanners里的任务状态机与漏洞表class ScanTask(models.Model): STATUS ( (pending, 排队中), (running, 扫描中), (completed, 完成), (failed, 失败), (cancelled, 取消), ) target models.ForeignKey(Target, on_deletemodels.CASCADE, related_nametasks) scanner_type models.CharField(扫描器类型, max_length50, defaultweb_basic) status models.CharField(状态, max_length20, choicesSTATUS, defaultpending) progress models.IntegerField(进度百分比, default0) start_time models.DateTimeField(nullTrue, blankTrue) end_time models.DateTimeField(nullTrue, blankTrue) created_at models.DateTimeField(auto_now_addTrue) class Vulnerability(models.Model): SEVERITY ( (critical, 严重), (high, 高危), (medium, 中危), (low, 低危), (info, 信息), ) task models.ForeignKey(ScanTask, on_deletemodels.CASCADE, related_namevulns) plugin_name models.CharField(插件名, max_length100) vuln_name models.CharField(漏洞名称, max_length200) severity models.CharField(风险等级, max_length20, choicesSEVERITY, defaultmedium) url models.CharField(检测URL, max_length500) payload models.TextField(使用的Payload, blankTrue) evidence models.TextField(验证数据, blankTrue) detail models.TextField(漏洞描述, blankTrue) retest_status models.CharField(复测状态, max_length20, defaultunretested)这里的关键设计是Vulnerability挂在ScanTask下而不是直接挂在Target下。因为同一目标要跑多轮扫描每轮结果必须独立归档如果挂Target上一轮扫出的历史漏洞很容易跟本轮混在一起。retest_status字段记录unretested、confirmed、fixed比发现漏洞后直接删记录更符合安全测试的实际流程也方便月底统计复测完成率。2.3 异步执行选型不引入Celery的最轻方案扫描任务最怕的就是HTTP请求把视图进程卡死用户在前端点“开始扫描”后页面转圈几十秒没响应。常见做法有两种性能要求一般的工具直接用多线程加数据库状态轮询规模再大才引Celery。我的判断是目标量在几十个以内、单机部署时先用Django的threading模块加工作线程就够了。Celery会多出Redis、Worker、Beat三层运维成本对这个体量的工具来说收益有限。下面是调度层最简实现import threading from django.utils import timezone from scanners.detectors.web_basic import WebBasicScanner def run_scan_task(task_id): task ScanTask.objects.get(pktask_id) task.status running task.start_time timezone.now() task.save(update_fields[status, start_time]) scanner WebBasicScanner(task.target) try: scanner.scan(callbacktask.update_progress) task.status completed task.end_time timezone.now() except Exception as exc: task.status failed task.error_msg str(exc)[:500] finally: task.save(update_fields[status, end_time, error_msg]) def start_scan(task_id): threading.Thread(targetrun_scan_task, args(task_id,), daemonTrue).start()task.update_progress作为回调扫描器内部每完成一个检测项就把progress写进数据库前端轮询任务接口就能拿到进度条。启动线程后立即返回HTTP响应给前端任务状态在数据库里流转这套模型在单机场景下非常稳。3. 从视图层到漏洞检测插件的完整实现路径框架和数据模型定了之后真正决定工具好不好用的是视图层怎么把扫描能力暴露出去以及检测插件怎么写才能持续叠加。3.1 URL路由与视图层from django.urls import path from . import views urlpatterns [ path(api/targets/, views.target_list, nametarget-list), path(api/targets/int:pk/, views.target_detail, nametarget-detail), path(api/targets/int:pk/scan/, views.start_scan, nametarget-scan), path(api/tasks/, views.task_list, nametask-list), ]写API风格的接口是因为前端要用异步请求轮询进度纯Django模板渲染在进度条这类交互上会比较吃力。如果项目页面少也可以后端直接模板渲染把Admin作为数据操作入口。start_scan视图有一个必须做的校验目标处于已授权状态才允许建任务否则存在越权扫描隐患。from django.http import JsonResponse from django.views.decorators.http import require_POST from scanners.models import ScanTask require_POST def start_scan(request, pk): target Target.objects.filter(pkpk).first() if target is None: return JsonResponse({error: target not found}, status404) if target.auth_status ! authorized: return JsonResponse({error: target not authorized}, status403) task ScanTask.objects.create( targettarget, scanner_typerequest.POST.get(scanner_type, web_basic) ) start_scan(task.id) return JsonResponse({task_id: task.id, status: task.status})require_POST限制请求方法避免GET请求触发扫描授权校验放在创建任务之前比在扫描器内部校验更早暴露问题。3.2 HTTP请求封装与指纹识别扫描器发HTTP请求时要注意统一封装不能在各个插件里各自调用requests.get否则超时设置、代理、SSL处置会各写一套排查问题时很头痛。import requests class HttpClient: def __init__(self, timeout10, verify_sslFalse): self.session requests.Session() self.session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Accept: */*, }) self.timeout timeout self.verify_ssl verify_ssl def get(self, url, **kwargs): return self.session.get( url, timeoutkwargs.get(timeout, self.timeout), verifyself.verify_ssl )verify_ssl默认False是因为内网测试环境大量自签名证书但引用这个类的插件要把verifyFalse的行为写清楚否则公网环境下中间人攻击风险不可控。ssl验证是安全测试工具自身的安全边界建议在配置项里显式暴露。指纹识别是信息收集模块的基础能力简单实现就能用def fingerprint(response): headers response.headers server headers.get(Server, ) powered_by headers.get(X-Powered-By, ) cookies response.headers.get(Set-Cookie, ) hints { apache: apache in server.lower(), nginx: nginx in server.lower(), php: php in powered_by.lower() or PHPSESSID in cookies, java: java in powered_by.lower(), python: python in powered_by.lower() or wsgi in server.lower(), } return [k for k, v in hints.items() if v]这个字典匹配逻辑虽然简单但后续要做“按指纹加载对应检测插件”时它就是这个路由表。3.3 漏洞检测插件的编写规范建议给检测插件定义统一的基类接口统一后新增检测项不用动调度层代码。基类设计如下class BasePlugin: name base risk info def __init__(self, target, http_client): self.target target self.http http_client def check(self): raise NotImplementedError def get_vuln(self, vuln_name, severity, url, payload, evidence): return { plugin_name: self.name, vuln_name: vuln_name, severity: severity, url: url, payload: payload, evidence: evidence, }一个检测插件的实现class SQLIDetector(BasePlugin): name sqli_detector risk high def check(self): issues [] for path in [/, /index.php, /search]: resp self.http.get(self.target.build_url(path)) if resp.status_code 500 and sql in resp.text.lower(): issues.append(self.get_vuln( vuln_name疑似SQL注入, severityhigh, urlresp.url, payloadpath, evidenceresp.text[:200] )) return issues这里有一个设计点插件返回的是字典列表而不是直接向Vulnerability表写记录。这样插件保持纯函数的形态方便单测调度层统一负责落库同时用get_vuln返回的plugin_name反查插件定位漏洞来源。基类方法返回类型说明check()list[dict]插件入口返回漏洞字典列表get_vuln()dict拼装标准漏洞字段统一格式target.build_url()str拼接目标URL自动处理反斜杠与端口3.4 扫描报告导出报告导出用Django management command比较合适扫描完成后敲一条命令就能生成归档结果# scanners/management/commands/export_report.py import csv from django.core.management.base import BaseCommand from scanners.models import Vulnerability class Command(BaseCommand): help 导出扫描报告为CSV def add_arguments(self, parser): parser.add_argument(--task-id, typeint, requiredTrue) parser.add_argument(--output, typestr, defaultreport.csv) def handle(self, *args, **options): qs Vulnerability.objects.filter(task_idoptions[task_id]) with open(options[output], w, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow([漏洞名称, 风险等级, URL, 插件, 复测状态]) for v in qs: writer.writerow([ v.vuln_name, v.get_severity_display(), v.url, v.plugin_name, v.retest_status ])文件编码必须写成utf-8-sig否则用Excel直接打开CSV时中文字段全是乱码这个坑很容易在中途不知不觉踩到却很难一下定位。4. 部署启动时的关键配置与参数调整工具写完要跑起来部署配置直接决定它能不能稳定跑完一轮完整扫描。4.1 数据库与跨域配置# settings.py ALLOWED_HOSTS [*] # 内网部署可先用通配公网务必收紧 DATABASES { default: { ENGINE: django.db.backends.postgresql, NAME: pentest, USER: pentest_user, PASSWORD: os.environ.get(DB_PASSWORD), HOST: 127.0.0.1, PORT: 5432, } } CORS_ALLOW_ALL_ORIGINS False CORS_ALLOWED_ORIGINS [ http://127.0.0.1:8080, http://192.168.1.10:8080, ]SQLite适合单机快速验证多台机器同时写库会出现锁等待所以正式环境要换PostgreSQL。数据库口令不写进源码从环境变量读取防止源码被传到不相关的地方时泄露测试内网凭证。CORS要显式列出前端来源不推荐开着ALLOW_ALL真跑跨域配置放开太宽会把工具本身变成攻击入口。4.2 初始化数据与启动命令python manage.py makemigrations targets scanners reports python manage.py migrate python manage.py createsuperuser python manage.py runserver 0.0.0.0:8080makemigrations后面接app名是为了避免文件顺序错乱一次性把所有模型的迁移文件对齐createsuperuser创建的账号用来登录Admin后台runserver的0.0.0.0表示局域网内可访问但要靠防火墙或Nginx限制来源IP不能直接暴露公网。如果想省掉手工插数据的步骤可以自己写一个seed_demo的management command往Target表插入几条测试目标并设置authorized状态开发阶段会顺手很多。4.3 常见报错排查CSRF、CORS与超时现象原因处理方式POST请求返回403 CSRF验证失败Django默认CSRF中间件开启前端表单没有带token模板中加{% csrf_token %}API请求在Header里带X-CSRFToken前端请求被浏览器拦截后端没开跨域或来源不在白名单用django-cors-headers把来源域名加进CORS_ALLOWED_ORIGINS扫描时requests报Timeout目标网络延迟高或存在防火墙过滤HttpClient超时用递增重试第一次10秒第二次20秒第三次30秒CSRF是刚用Django写接口的新手最容易卡的关口。如果工具只在受控内网使用可以给扫描API视图加csrf_exempt但公网环境必须保留CSRF校验。这个取舍要在项目文档里写明不能默认取消。4.4 生产部署时选Gunicorn还是Waitress生产环境不要用runserver跑并发和安全都不达标。Linux服务器上常见做法是Gunicorn加Nginx反代Windows环境则经常用waitress配合Nginx。启动命令区别如下# Linux gunicorn pentest_platform.wsgi:application -w 4 -b 127.0.0.1:8000 # Windows waitress-serve --listen127.0.0.1:8000 pentest_platform.wsgi:applicationGunicorn的-w参数指定worker数一般按CPU核心数乘2。Nginx配一份反代配置文件把80端口流量转发到8000再配合gzip压缩网页资源。这套组合在安全测试工具这种低频但请求单个耗时长的场景下完全够用。5. 用Django Admin做检测结果复核和风险评级的进阶用法Admin是Django工具里经常被忽略的组件。渗透测试工具的用户是安全测试人员而不是前端开发Admin的列表页自动提供筛选、搜索和分页正好覆盖“已授权目标有哪些”“高危漏洞扫出几条”“复测状态如何”这些高频查询需求。定制漏洞管理页的核心在于list_display和list_filter的配合from django.contrib import admin from scanners.models import Vulnerability admin.register(Vulnerability) class VulnerabilityAdmin(admin.ModelAdmin): list_display (vuln_name, severity, target_host, plugin_name, retest_status) list_filter (severity, retest_status, plugin_name) search_fields (vuln_name, url) actions [mark_as_fixed] def target_host(self, obj): return obj.task.target.domain or obj.task.target.ip target_host.short_description 目标 admin.action(description标记为已修复) def mark_as_fixed(self, request, queryset): queryset.update(retest_statusfixed)severity中文字段用get_severity_display取而不是直接取原始字符串这样才能显示“高危”而不是“high”。list_filter里放组合筛选复测状态和插件名放进去后复核人员可以快速挑出某个插件扫出但还没复测的记录。风险评级可以做成一个计算函数不存字段而是根据漏洞数量和等级动态得出def calculate_risk_score(task_id): agg Vulnerability.objects.filter(task_idtask_id).values(severity) \ .annotate(countCount(id)) score 0 level_map {critical: 10, high: 7, medium: 4, low: 1} for item in agg: score level_map.get(item[severity], 0) * item[count] return min(score, 100)这个分数不需要落库在报告页面或报表里实时计算就行避免了任务状态变化后分数过期的问题。批量复测状态更新是另一个实用技巧选中多条漏洞记录直接执行动作把状态统一改成confirmed或fixed省去逐条编辑的重复劳动。配合admin action整个复测流程在Admin里就能完成不需要额外开发前端页面。前端渲染漏洞列表时记得用select_related(task__target)把关联对象一次取出来否则Django ORM会在循环里对每条漏洞都跑一次目标查询200条漏洞就是201条SQL页面会明显变慢。def task_vulns(request, task_id): vulns Vulnerability.objects.filter(task_idtask_id) \ .select_related(task__target) return render(request, scanners/vuln_list.html, {vulns: vulns})加上select_related后关联的目标信息在一条查询里用JOIN取全循环渲染target_host时不再产生N1查询。这样一个页面接口就同时覆盖了查询、筛选、批量操作和性能优化几个要点。本文还有配套的精品资源点击获取
