ARTICLE DETAIL

资讯详情

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

用Django构建Web化网络安全扫描平台:端口扫描、子域名枚举与目录探测实践

用Django构建Web化网络安全扫描平台:端口扫描、子域名枚举与目录探测实践 简介面向Python/Django毕业设计学生的nweb渗透测试工具源码包完整提供基于Django构建Web安全检测应用的工程代码可学习扫描器、会话劫持、CSRF防护、弱口令测试、目录遍历、代码审计及OWASP Top Ten等常见检测模块并通过models、views、urls、forms、templates、static、middleware分层结构理解Django项目组织方式。包内共2000个文件、压缩包约557.95MB其中275个py源码与197个pyc字节码构成核心逻辑1662个svg图标配合css、js搭建前端界面html模板负责页面渲染sqlite3数据库及配置文件支撑运行整体工程完整、素材齐备。已有497人学习浏览特别适合毕业设计参考、Django源码精读或Web安全入门通过代码既能掌握权限校验、表单验证、URL路由、中间件等Django开发要点也能了解漏洞扫描原理、会话安全机制和OWASP风险项的检测思路同时明确渗透测试应遵循合法授权边界目录层次清晰从扫描任务启动、进度显示到结果入库均有完整实现。1. nweb 是什么一个把扫描器打包进 Web 平台的毕业设计解法毕设答辩现场最常见的翻车方式是把安全工具做成一个终端黑匣子评委只看到一串串滚动的字符看不到任务状态也看不出工程结构。用 Django 写的 nwebNetwork Web Scanner提供的是另一条路——端口扫描、子域名枚举、目录探测这些常规渗透测试能力被封装成一个个可调度的扫描任务前端负责发起和展示后端用 ORM 管理目标、任务和结果。这样它既具备真实的探测能力又是一个完整的产品原型。适合正在纠结“Django 项目做什么选题”的人也适合想借毕业设计切入安全研发方向、需要一个能放在简历上的完整作品的从业者。2. nweb 的 Django 骨架App 划分与数据模型怎么定2.1 为什么用 Django 而不是 Flask 写渗透测试工具很多人选 Flask理由是轻量。但做一个“渗透测试工具平台”真正的产品面是任务管理、目标管理、结果展示和用户权限扫描本身只是其中一个模块。Django 在这几点几乎是白送内置 Admin 后台可以直接管理目标列表内置 User 模型解决登录问题ORM 让结果落库和查询不需要拼 SQL。Flask 也能做但同样功能要多写一倍的胶水代码。毕设时间本来就紧没必要把时间花在这些重复劳动上。生产级的 Django 后端通常会拆出多个 App按企业级教程的做法甚至会把用户、权限、任务队列各拆成一个独立服务。nweb 这类项目不必那么重但也不能把所有逻辑塞进一个 App。我一般会拆成三个业务 Apptargets 管目标资产域名、IP、端口范围scan_task 管扫描任务和结果account 直接继承 Django 自带的 auth 体系暂时不做扩展。核心扫描代码不放 App 里而是放在项目根目录下的扫描引擎包里方便脱离 Django 做单元测试。2.2 初始化项目创建虚拟环境与 App创建项目时先建虚拟环境避免把依赖装进系统 Python后面装 requests、更新 Django 版本都不会污染到别的项目。下面这组命令在 Linux 和 Windows 上都通用Windows 上把source .venv/bin/activate换成.venv\Scripts\activate.bat即可。python -m venv .venv source .venv/bin/activate pip install django requests django-admin startproject nweb_project cd nweb_project python manage.py startapp targets python manage.py startapp scan_task python manage.py startapp accountdjango-admin 创建项目、manage.py 创建 App整套动作十分钟能完成。新手最容易漏的一步是创建 App 后忘了去 settings.py 的 INSTALLED_APPS 里登记结果跑 migrate 时报 “No such table”后面一排查才发现 App 根本没被 Django 加载。做完 startapp 顺手把三个 App 名字加进 INSTALLED_APPS是省时间的好习惯。2.3 模型设计Target、ScanTask、ScanResult 三个表怎么拆数据模型是整个平台的地基。设计核心是把“扫描任务”和“扫描结果”分开任务表记录“什么时候对哪个目标做了什么”结果表记录“扫描出来的内容是什么”。下面是我会写进 models.py 的核心部分。# scan_task/models.py from django.db import models from django.contrib.auth.models import User class Target(models.Model): name models.CharField(max_length128) # 目标名称如“内网测试机” host models.CharField(max_length255) # IP 或域名 port_range models.CharField(max_length64, # 如 1-1024 default1-1024) created_by models.ForeignKey(User, on_deletemodels.CASCADE) created_at models.DateTimeField(auto_now_addTrue) def __str__(self): return f{self.name}({self.host}) class ScanTask(models.Model): class Status(models.TextChoices): PENDING PENDING, 等待中 RUNNING RUNNING, 执行中 DONE DONE, 已完成 FAILED FAILED, 失败 CANCELLED CANCELLED, 已取消 target models.ForeignKey(Target, on_deletemodels.CASCADE) scan_type models.CharField(max_length32) # port / subdomain / directory status models.CharField(max_length16, choicesStatus.choices, defaultStatus.PENDING) progress models.IntegerField(default0) # 0-100前端轮询用 current_item models.CharField(max_length255, blankTrue) result models.JSONField(defaultlist, blankTrue) created_by models.ForeignKey(User, on_deletemodels.CASCADE) created_at models.DateTimeField(auto_now_addTrue) class ScanResult(models.Model): task models.ForeignKey(ScanTask, on_deletemodels.CASCADE, related_nameitems) item_type models.CharField(max_length32) # open_port / domain / directory detail models.JSONField(defaultdict) # 不同扫描类型字段差异大用 JSON 存 created_at models.DateTimeField(auto_now_addTrue)参数说明Status用 TextChoices 而不是写成字符串常量Django 后台下拉框会自动显示中文代码里也不容易手滑写错值。progress字段放任务表而不是结果表因为前端轮询进度只需查任务一张表join 结果表会慢一个数量级。result用 JSONField 存最终汇总结果明细行再落 ScanResult牺牲一点查询能力换展示时的便利。为什么不做传统的关系表比如“开放端口表”“发现域名表”因为扫描器输出的字段差异太大端口记录有 service、banner目录记录有 status_code、title子域名记录有 cname、ip。强行统一成关系表每一种扫描都要新建一张表。JSONField 在 Django 3.1 之后对 SQLite、MySQL 都支持得很好是这类“半结构化结果”的最优解。2.4 Admin 注册演示时最实用的一个动作把模型注册进 Django Admin演示时可以直接在后台添加目标、查看任务记录不用等前端页面全部写完。# scan_task/admin.py from django.contrib import admin from .models import Target, ScanTask, ScanResult admin.register(Target) class TargetAdmin(admin.ModelAdmin): list_display (name, host, port_range, created_at) admin.register(ScanTask) class ScanTaskAdmin(admin.ModelAdmin): list_display (id, target, scan_type, status, progress, created_at) list_filter (status, scan_type) admin.register(ScanResult) class ScanResultAdmin(admin.ModelAdmin): list_display (task, item_type, created_at)这段代码解决的是“数据要有地方看”的问题。创建超级用户后登录/admin目标列表、任务状态、扫描结果一目了然。答辩演示时这比在终端里敲 SQL 查库体面得多。list_filter 加在 ScanTask 上演示时按 scan_type 过滤出端口扫描任务几秒钟就能找到目标数据。3. 扫描功能落地端口扫描、子域名枚举与目录探测的实现3.1 路由与视图从点击“开始扫描”到看到结果前端发起一个 POST 请求到/api/tasks/视图接收 target_id 和 scan_type校验目标归属后创建 ScanTask 记录并返回 task_id前端拿 task_id 轮询进度。下面是最简路由配置。# nweb_project/urls.py from django.contrib import admin from django.urls import path, include urlpatterns [ path(admin/, admin.site.urls), path(api/, include(scan_task.urls)), ]# scan_task/urls.py from django.urls import path from . import views urlpatterns [ path(tasks/, views.create_task), path(tasks/int:pk/, views.task_detail), ]请求链路很直接urls 把/api/tasks/转发给 create_task 视图视图负责校验、建任务、触发扫描线程最后返回 JSON。前端只要拿到带 task_id 的响应就完成第一步后续进度展示全部走 task_detail 接口。这种拆分方式让“发起”和“查询”两个动作分别扛独立接口后续加权限控制也容易。3.2 端口扫描器连接超时与并发数的选择端口扫描本质是 TCP 连接探测目标端口处于监听状态时握手会成功。常见做法是用 socket 逐个 connect但串行扫 1 到 1024 号端口非常慢必须用线程池并发。# engine/port_scanner.py import socket from concurrent.futures import ThreadPoolExecutor COMMON_PORTS [21, 22, 23, 25, 53, 80, 110, 111, 135, 139, 143, 443, 445, 993, 995, 1723, 3306, 3389, 5900, 8080] def _probe(host, port, timeout1.0): sock socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(timeout) result False try: if sock.connect_ex((host, port)) 0: result True except Exception: result False finally: sock.close() return port, result def scan_port(host, portsNone, timeout1.0, max_workers50): if ports is None: ports COMMON_PORTS with ThreadPoolExecutor(max_workersmax_workers) as pool: futures [pool.submit(_probe, host, p, timeout) for p in ports] opened [] for future in futures: port, ok future.result() if ok: opened.append(port) return sorted(opened)参数说明timeout1.0在本地或内网扫描合适如果扫公网目标1 秒可能不够稳建议调到 3 秒但要清楚整体耗时会线性上涨。max_workers50是比较保守的并发数太大容易在目标防火墙上触发封禁也可能消耗本机大量临时端口。connect_ex在连接失败时不会抛异常而是返回错误码异常处理只留给超时等少见情况代码进度更干净。3.3 子域名枚举字典大小和泛解析处理子域名枚举的常见做法是拿字典里的前缀拼上主域名做 DNS 解析能解析就认为存在。这个逻辑本身简单但不处理“泛解析”会扫出一堆假域名这也是这个模块最典型的坑。# engine/subdomain_enum.py import socket from concurrent.futures import ThreadPoolExecutor, as_completed def detect_wildcard(domain): 检测主域名是否配置了泛解析 fake fnonexist-{abs(hash(domain)) % 100000}.{domain} try: ip socket.gethostbyname(fake) return ip except socket.gaierror: return None def _check_sub(pre, domain, wildcard_ip): sub f{pre}.{domain} try: ip socket.gethostbyname(sub) except socket.gaierror: return None if wildcard_ip and ip wildcard_ip: return None # 命中泛解析丢弃 return {sub: sub, ip: ip} def enum_subdomain(domain, wordlist_file, max_workers30): wildcard_ip detect_wildcard(domain) with open(wordlist_file, r, encodingutf-8) as f: words [line.strip() for line in f if line.strip()] results [] with ThreadPoolExecutor(max_workersmax_workers) as pool: futures [pool.submit(_check_sub, w, domain, wildcard_ip) for w in words] for future in as_completed(futures): item future.result() if item: results.append(item) return resultsdetect_wildcard 用一个几乎不可能真实存在的前缀去解析主域名如果解析到了 IP说明域名开了泛解析* 记录指向某台服务器后续枚举中凡解析到该 IP 的子域全部视为噪声丢弃。字典文件用 admin、api、mail、dev、test 这类常见前缀几百行就够本科毕设用了。字典越大越慢也越容易被目标 DNS 服务器的限流策略盯上所以宁可选小字典加并发控制不要一股脑塞几万行。3.4 目录扫描状态码、重定向与 User-Agent目录扫描是通过 HTTP 请求探测目标站点上真实存在的路径。常见做法是先看 404 响应特征再逐个请求字典里的路径按状态码分类结果。# engine/dir_scan.py import requests def scan_directory(base_url, wordlist_file, timeout5.0): ua {User-Agent: Mozilla/5.0 (compatible; nweb/1.0)} with open(wordlist_file, r, encodingutf-8) as f: paths [line.strip() for line in f if line.strip()] results [] for path in paths: url base_url.rstrip(/) path try: resp requests.get(url, headersua, timeouttimeout, allow_redirectsFalse) except requests.RequestException: continue entry {path: path, status: resp.status_code, size: len(resp.content)} if resp.status_code in (301, 302): entry[location] resp.headers.get(Location, ) results.append(entry) return results关键参数allow_redirectsFalse必须加否则 302 会带着扫描器自动跳转到登录页结果里全是登录页的 200 状态码真实路径反而被淹没。timeout设 5 秒以上有些框架接口处理慢超时设短会误报“目标不可达”。真实场景里还要增加“404 模板过滤”如果目标站点的 404 页面固定返回同一段内容就把这种响应识别为“不存在”否则扫出来的结果基本都是误报。3.5 结果回显视图层怎么把扫描结果交给页面扫描完成后视图从任务表取出 result JSON 直接返回前端渲染。这里数据模型和视图做了明确分工视图只负责取数和序列化展示逻辑全部交给前端按 item_type 分发。# scan_task/views.py from django.http import JsonResponse from .models import ScanTask def task_detail(request, pk): task ScanTask.objects.select_related(target).get(pkpk) data { id: task.id, target: task.target.host, scan_type: task.scan_type, status: task.status, progress: task.progress, current_item: task.current_item, result: task.result, } return JsonResponse(data)这段代码通过 select_related 一次性把目标信息带出来避免前端拿到 task_id 后再请求一次目标接口。JsonResponse 会把 JSONField 内的列表原样输出前端拿到 result 数组后直接遍历渲染成表格。扫描结果结构不统一这件事后端不掺和前端按 item_type 决定渲染成端口表格还是子域名列表边界清楚。4. 异步任务与结果入库让长时间扫描不再卡死页面4.1 同步等待为什么是最容易犯的错如果直接在 create_task 视图里同步执行扫描这个请求会占用视图线程数秒甚至数分钟。Django 开发服务器默认线程池资源有限一个慢请求占住线程其他所有登录、查询请求都得排队。到演示现场点一次扫描整个平台“没反应”就是这个原因。正确思路是视图只负责把任务写入数据库并立即返回 task_id扫描逻辑放后台线程执行前端每秒请求一次任务详情接口把 progress 渲染到进度条。这是 Web 工具类项目最常见的异步方案不引入额外组件也能把逻辑讲通答辩时也说得清楚。4.2 用内置线程托管扫描执行不引入 Celery 时我一般直接用 Python 的 threading 模块一个扫描任务一个线程。核心是把“任务执行流程”和“任务状态更新”解耦。# scan_task/services.py import threading from .models import ScanTask from engine.port_scanner import scan_port from engine.subdomain_enum import enum_subdomain from engine.dir_scan import scan_directory SCANNERS { port: scan_port, subdomain: enum_subdomain, directory: scan_directory, } def run_scan_task(task_id): task ScanTask.objects.get(pktask_id) task.status ScanTask.Status.RUNNING task.save(update_fields[status]) target task.target try: if task.scan_type port: task.result scan_port(target.host) elif task.scan_type subdomain: task.result enum_subdomain(target.host, wordlist/sub.txt) elif task.scan_type directory: task.result scan_directory(fhttp://{target.host}, wordlist/dir.txt) task.progress 100 task.status ScanTask.Status.DONE task.save(update_fields[result, progress, status]) except Exception as exc: task.status ScanTask.Status.FAILED task.result [{error: str(exc)}] task.save(update_fields[status, result])参数说明SCANNERS 字典是分发表新增扫描类型时只改这里视图层不需要知道有哪些扫描器。update_fields 减少数据库写操作进度到 100 时只更新四个字段避免全表字段重写。异常处理是抓大放小扫描线程里任何异常都不应该导致 Django 进程崩溃把错误写进 result 比打印堆栈更利于前端展示。4.3 任务创建视图与进度轮询创建任务的视图做三件事校验目标归属、创建 ScanTask 记录、启动后台线程。线程启动后立即返回响应时间控制在 100 毫秒内。# scan_task/views.py import threading from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from django.shortcuts import get_object_or_404 from .models import Target, ScanTask from .services import run_scan_task csrf_exempt require_POST def create_task(request): target_id request.POST.get(target_id) scan_type request.POST.get(scan_type) target get_object_or_404(Target, pktarget_id, created_byrequest.user) task ScanTask.objects.create( targettarget, scan_typescan_type, created_byrequest.user, ) t threading.Thread(targetrun_scan_task, args(task.id,), daemonTrue) t.start() return JsonResponse({task_id: task.id})参数说明csrf_exempt 在纯 API 场景省去 CSRF token 处理但如果之后要改成 Django 模板渲染页面需要去掉这个装饰器并加上表单校验。这里用 request.user 过滤目标归属目标不是当前用户创建时返回 404避免越权访问任务数据。线程的 daemonTrue 在本机演示时很关键开发服务器 CtrlC 退出时守护线程随主进程一起结束不会出现“进程退了扫描还在后台跑”的僵尸线程。真正生产环境不要用裸线程那时任务应该交给消息队列管理生命周期。4.4 任务取消删除对象其实有讲究答辩可能被问到“任务能不能取消”。常见做法是软删除而不是真删除把任务状态改成 CANCELLED后台扫描函数每轮循环检查一次状态再决定是否继续。这既保留任务记录又不会让扫描线程无限跑下去。def should_stop(task_id): return not ScanTask.objects.filter( pktask_id, status__in[ScanTask.Status.CANCELLED, ScanTask.Status.DONE], ).exists()在扫描循环里每处理完一个子域名或目录就调用一次 should_stop返回 True 就 break。取消接口执行ScanTask.objects.filter(pkpk).update(statusCANCELLED)。注意这里用 QuerySet.update 而不是先 get 再 saveupdate 直接落库没有“先读后写”的竞态窗口。这是 Django 执行查询更新对象的一个实用细节批量更新永远优先于单对象改属性再 save。4.5 要不要上 Celery规模与答辩取舍方案额外依赖适合场景坑内置线程无单机演示、任务量小无法跨进程调度进程重启任务断掉Celery RedisRedis、celery、worker 进程任务多、要排队、老师问调度本地环境配置成本高进程管理容易翻车rq Redisrq、Redis比 Celery 简单生态小功能少对一个毕业设计来说内置线程方案完全够用。如果导师明确要求“任务队列”或“分布式扫描”再上 Celery 不迟。答辩讲方案取舍时可以说“考虑到系统演示环境是单机引入内置线程调度而非消息队列在可维护性和复杂度之间取了平衡”这句话本身是加分项说明你想过而不是不会用。5. nweb 排坑手记从能启动到能演示的五个问题5.1 点“开始扫描”后浏览器一直转圈直到超时现象前端发起扫描请求后整个页面不再响应浏览器最终提示连接超时。原因扫描逻辑直接写在视图函数里同步执行线程被长时间占用。Django 开发服务器默认线程池很薄一个扫描任务就把整个服务拖住了后续所有请求排队等待。解决把视图拆成“建任务 启动后台线程 立刻返回”见第四章的 create_task。判断是否踩坑的方法在视图第一行加print(in)、最后一行加print(out)如果 in 到 out 耗时超过 2 秒说明同步执行还没拆干净。这个排查法适用于所有 Django 视图卡死类问题。5.2 SQLite 报 “database is locked” 错误现象任务跑着跑着控制台冒出OperationalError: database is locked任务状态卡在 PENDING。原因默认配置下 SQLite 同一时刻只允许一个进程写库。扫描线程在频繁更新进度视图线程也在写任务记录两个写入操作撞在一起就锁了。解决先打开 WAL 模式再减小写入频率进度不是每扫一个端口就写一次而是攒到 10% 增量再写。DATABASES 配置里加OPTIONS同时确保扫描循环里的保存操作尽量集中。DATABASES { default: { ENGINE: django.db.backends.sqlite3, NAME: BASE_DIR / db.sqlite3, OPTIONS: { timeout: 20, init_command: PRAGMA journal_modeWAL;, }, } }timeout 参数让数据库等待锁释放的时间延长到 20 秒WAL 模式允许读写并发。两个措施一起上之后这类报错基本消失。5.3 子域名扫描推到一半像死机现象子域名任务耗时超过十分钟进度一直停在 12%连 DNS 查询都变慢了。原因字典太大加上线程数量一多本机发出的 DNS 请求被系统解析器或路由器限流。更糟的是如果目标域名配了泛解析每个前缀都解析成功结果里存了一堆假数据看起来像卡死其实是逻辑不对。解决先用 detect_wildcard 检测泛解析并过滤再把并发数降到 20 以下字典控制在 1000 行以内。扫描前先估算平均一个 DNS 查询 50 毫秒并发 201000 个词大概几分钟内跑完。如果实际耗时远超过估算优先怀疑限流其次怀疑泛解析。5.4 DEBUGFalse 之后样式全丢现象为了演示把 DEBUG 设为 False重启后登录页变成纯 HTMLCSS 全没了。原因Django 的 runserver 只在 DEBUGTrue 时托管静态文件。DEBUGFalse 后静态文件路径没人管页面引用 /static/ 下的文件全部 404。解决本地演示不要动 DEBUG。如果有人非要在答辩时关掉 DEBUG 装“生产环境”需要先跑 collectstatic 把静态文件收集到指定目录再用独立静态文件服务提供。对 nweb 这个项目保持 DEBUGTrue 是省时间的正确选择答辩考核的是系统设计和思维不是 DEBUG 开关。5.5 开发服务器一重启扫描结果全丢了现象演示前不小心改了一个文件Django 自动重载后台任务列表空了之前扫描出来的端口和目录全部消失。原因开发服务器会监听源码变化自动重启内存里的任务状态随之清空。如果扫描逻辑操作的是 Python 变量而不是数据库重启后一切归零。解决扫描结果一律写入 ScanTask.result 或 ScanResult前端从接口读不读进程内变量。源码修改和任务状态分离的意思是CtrlS 之前想清楚这次保存会不会触发 autoreload如果正在跑任务宁可等任务结束再改代码。这个坑在答辩前夜最容易踩撞上之后没有后悔药。6. 从毕业设计到真工具本地靶场验证与插件化改造先解决“怎么证明它真的有用”的问题。不要拿公网域名做测试那既涉及授权问题又容易被反而不稳定。常见的做法是在本机起一个靶场DVWA 和 OWASP Juice Shop 都行把 Target 的 host 填成 127.0.0.1端口扫描选 8080目录扫描跑完能看到 /login.php、/admin 这类真实路径整个流程就闭环了。验证时重点看三处扫描结果是否和靶场实际服务一致、进度条是否平滑上涨、扫描过程中页面其他功能是否还能正常用。再解决“将来怎么扩展”的问题。我一般会把扫描器做成插件化结构每个扫描器是一个独立的类继承同一个抽象基类运行时动态加载。# engine/base_scanner.py class BaseScanner: name base def __init__(self, target, task_id): self.target target self.task_id task_id def scan(self): raise NotImplementedError # engine/port_scanner.py from .base_scanner import BaseScanner class PortScanner(BaseScanner): name port def scan(self): return scan_port(self.target.host)插件化之后新增一个“CORS 检测”只需要另起一个文件写实现在 SCANNERS 字典里注册一行前端表单里加一个选项其余一概不动。这才是把这个项目从“跑通”做到“能维护”的关键一步。我当年在答辩前一周把任务调度从内置线程换成 Celery结果被 Redis 环境问题折腾了三天最后换回内置线程才压哨跑通。后来想明白了毕设不是追新框架是把一个方案讲到闭环。先把最小的方案跑起来再按需加复杂度这条路走得最稳。希望帮到你。本文还有配套的精品资源点击获取
返回列表