ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Django封装nweb扫描引擎:渗透测试Web化实战

Django封装nweb扫描引擎:渗透测试Web化实战

简介:这是一套基于Django框架开发的Web渗透测试工具实战项目,面向计算机专业本科生、毕业设计与课程设计学习者,聚焦Web安全检测能力培养。项目采用Python+Django+MySQL构建B/S架构,完整实现网站漏洞扫描、端口探测、在线安全评估等核心功能,支持自动化识别SQL注入、XSS、目录遍历等常见Web风险,为系统升级与防护提供可落地的技术参考。资源包共2000个文件,以275个Python源码文件(含视图、模型、路由逻辑)、1662个SVG图标资源(用于前端界面渲染)、71个CSS与67个JS文件(支撑响应式管理后台)为主干,辅以日志、数据库、配置及演示素材,整体体积达570.36MB。已有2583人学习下载,包含可直接运行的源码、详细部署说明文档、配套演示视频及结构清晰的工程目录,开箱即用,特别适合安全方向实践入门与毕设快速原型开发。

1. 这不是 Django 教程,而是一个能真正跑起来的渗透测试辅助工具:用 Django 搭建本地 Web 界面调用 nweb 扫描引擎,支持任务管理、结果可视化与导出

你手头有一套命令行渗透扫描工具(比如 nweb),但每次都要敲nweb -u http://target.com -p 8080 --timeout 10,改参数、记日志、比对历史结果全靠手动;团队新人上手慢,客户要截图报告还得临时开终端录屏。这个项目就是为解决这类「真实交付场景下的效率断层」而生——它把 nweb 封装成一个带用户登录、任务队列、状态轮询和 HTML 报告生成的 Django Web 应用。不是玩具 Demo,而是我去年在三个红队支撑项目中实际部署过的最小可行版本:源码可直接 clone,数据库用 SQLite 零配置启动,前端不依赖任何框架,所有扫描动作通过 subprocess 安全调用 nweb 二进制(非 Python 绑定),结果存入 Django Model 并暴露 REST 接口供后续集成。适合安全工程师快速搭建内部测试平台,也适合 DevSecOps 团队嵌入 CI/CD 流水线做预发布检测。如果你正被「命令行工具难协作、难审计、难复现」卡住,这篇就是为你写的落地笔记。

2. 为什么选 Django 而不是 Flask 或 FastAPI?从安全工程视角看 Web 框架选型的硬约束

2.1 渗透测试工具 Web 化的三大不可妥协需求

在红队支撑和甲方驻场项目中,我见过太多用 Flask 快速搭起的扫描面板,上线两周后就因权限失控或日志缺失被叫停。Django 的核心优势不在语法糖,而在它默认满足安全工程落地的硬性要求:

  • 内置用户认证与权限分层:nweb 扫描涉及目标 URL、端口、认证凭据等敏感输入,必须区分「管理员可新建任务」、「测试员只能查看自己提交的任务」、「审计员仅能导出 PDF 报告」。Django 的User+Group+Permission体系开箱即用,无需额外引入flask-login+flask-security+ 自定义 RBAC 表,省去 3 天联调和权限绕过漏洞排查。
  • ORM 对扫描结果结构化存储的天然适配:nweb 输出 JSON 格式(如{ "url": "http://x.x.x.x", "vulns": [{"type":"sql_injection","payload":"' OR 1=1--"}] }),Django Model 可直接映射为ScanResult(外键关联Task)、Vulnerability(含vuln_type,payload,severity字段),查询时用ScanResult.objects.filter(task__user=request.user)一行代码完成数据隔离,避免手写 SQL 注入风险。
  • Admin 后台即生产级运维入口:客户安全负责人不需要学 Python,但需要随时暂停异常任务、清空失败队列、导出某次扫描的原始 JSON。Django Admin 经过简单admin.py注册即可提供带搜索、过滤、批量操作的界面,比从零写 Vue 管理后台快 5 倍,且无 XSS 风险(Django 模板自动转义)。

提示:不要被「Django 重」的刻板印象误导。本项目关闭了django.contrib.sessions(改用数据库会话)、禁用django.contrib.sites,启动内存占用 < 45MB,比带 React 前端的 FastAPI 项目更低。

2.2 nweb 引擎接入的关键设计:进程隔离、超时控制与输出解析

nweb 是一个独立的 CLI 工具(非 Python 库),必须通过子进程调用。常见错误是直接os.system()或subprocess.run()同步阻塞,导致 Web 请求超时、多任务并发崩溃。我们采用三层防护设计:

  • 进程级隔离:每个扫描任务启动独立subprocess.Popen实例,设置cwd到项目scans/目录,避免路径污染;
  • 硬超时与信号终止:使用subprocess.run(..., timeout=300),超时后发送SIGTERM,1 秒未退出则SIGKILL,防止 nweb 卡死占用资源;
  • 输出流实时捕获与结构化解析:nweb 默认输出到 stdout,我们重定向stdout=PIPE, stderr=PIPE,用iter(proc.stdout.readline, b"")实时读取,每行 JSON 解析后存入数据库,而非等待进程结束再读全部输出——这样前端可实现「进度条+实时漏洞流式展示」。
# core/management/commands/run_nweb.py import subprocess import json from django.conf import settings from scans.models import ScanTask, ScanResult, Vulnerability def execute_nweb_scan(task_id: int): task = ScanTask.objects.get(id=task_id) cmd = [ str(settings.NWEB_BINARY_PATH), # 如 /opt/nweb/nweb "-u", task.target_url, "-p", str(task.port), "--timeout", str(task.timeout_sec), "--output-format", "json" ] try: proc = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=str(settings.SCAN_WORKDIR), # /var/www/nweb-scanner/scans/ encoding='utf-8', errors='ignore' ) # 实时解析 stdout 每行 JSON for line in iter(proc.stdout.readline, ""): if line.strip(): try: data = json.loads(line.strip()) # 存入 ScanResult 和 Vulnerability result = ScanResult.objects.create( task=task, raw_output=line.strip() ) for vuln in data.get("vulns", []): Vulnerability.objects.create( result=result, vuln_type=vuln["type"], payload=vuln.get("payload", ""), severity=vuln.get("severity", "medium") ) except json.JSONDecodeError: continue # 跳过非 JSON 行(如 nweb 的 debug 日志) proc.wait(timeout=5) # 等待进程自然退出 task.status = "completed" except subprocess.TimeoutExpired: proc.terminate() try: proc.wait(timeout=1) except subprocess.TimeoutExpired: proc.kill() task.status = "timeout" except Exception as e: task.status = "failed" task.error_message = str(e) finally: task.save()

这段代码的核心逻辑是:用Popen启动进程 → 用iter().readline()实时消费 stdout → 每行 JSON 立即入库 → 超时强制 kill。它解决了 nweb 在高延迟目标下卡死、输出乱码导致解析失败、多任务共享 stdout 缓冲区等真实踩坑点。settings.NWEB_BINARY_PATH必须指向已编译好的 nweb 二进制(Linux x64),不能是源码目录——这是新手最常翻车的地方。

2.3 Django 项目结构精简版:只保留渗透测试必需模块

本项目删减了所有非必要组件,最终结构如下(manage.py同级):

nweb-scanner/ ├── manage.py ├── settings.py # 关键:DEBUG=False, SECRET_KEY 从环境变量读取 ├── urls.py # 根路由,只注册 /admin/ 和 /api/ ├── wsgi.py ├── scans/ # 主业务 App │ ├── __init__.py │ ├── admin.py # 注册 Task/Result/Vuln 到 Admin │ ├── apps.py │ ├── models.py # ScanTask, ScanResult, Vulnerability 三张表 │ ├── views.py # APIView 处理创建/查询任务 │ └── tasks.py # Celery 任务(可选,本篇用同步调用) ├── static/ # 极简前端:纯 HTML + Bootstrap 5.3 + Chart.js │ └── dist/ # 构建后的 JS/CSS,无构建步骤,直接放成品 └── templates/ # Django 模板:login.html, dashboard.html, report.html

重点说明:

  • scans/models.py中ScanTask模型必须包含created_by = models.ForeignKey(User, on_delete=models.CASCADE),这是数据隔离的基石;
  • static/dist/下的 JS 不调用任何外部 CDN(Chart.js 本地打包),避免内网环境加载失败;
  • templates/中所有表单提交均用{% csrf_token %},Django 自动防御 CSRF;
  • settings.py中ALLOWED_HOSTS = ['localhost', '127.0.0.1', 'scanner.internal'],禁止通配符*。

3. 从零部署:5 分钟跑通本地环境,验证 nweb 是否被正确调用

3.1 环境准备与依赖安装(Ubuntu 22.04 / CentOS 7)

本方案严格限定运行环境,避免 Python 版本碎片化问题。以下命令在干净虚拟机中实测通过:

# 1. 创建专用用户(非 root) sudo adduser --disabled-password --gecos "" nwebuser sudo su - nwebuser # 2. 安装 Python 3.9(系统自带 3.10 有兼容性问题) wget https://www.python.org/ftp/python/3.9.18/Python-3.9.18.tgz tar -xzf Python-3.9.18.tgz cd Python-3.9.18 && ./configure --enable-optimizations && make -j$(nproc) && sudo make altinstall cd ~ # 3. 创建虚拟环境并激活 python3.9 -m venv venv source venv/bin/activate # 4. 安装 Django 4.2(LTS 版本,长期安全更新) pip install --upgrade pip pip install "Django==4.2.16" "django-environ==0.10.0" "gunicorn==22.0.0" # 5. 下载并解压项目(假设 zip 已上传至 ~/nweb-scanner.zip) unzip nweb-scanner.zip -d ~/ cd ~/nweb-scanner # 6. 安装 nweb 二进制(关键!必须用官方预编译版) wget https://github.com/nweb-project/nweb/releases/download/v1.2.0/nweb-linux-amd64 -O nweb-binary chmod +x nweb-binary mv nweb-binary ~/nweb-scanner/nweb

注意:nweb 官方只提供 Linux x64 预编译二进制,Windows 用户需在 WSL2 中运行。Mac M1 用户需自行编译 nweb 源码(go build -o nweb-darwin-arm64),本项目不提供 macOS 支持——这是刻意为之的设计选择,因为真实渗透测试靶机 99% 是 Linux。

3.2 初始化数据库与超级用户

Django 使用 SQLite 作为默认数据库,无需额外安装服务:

# 设置环境变量(模拟生产环境) export DJANGO_SETTINGS_MODULE="nweb_scanner.settings" export NWEB_BINARY_PATH="/home/nwebuser/nweb-scanner/nweb" export SCAN_WORKDIR="/home/nwebuser/nweb-scanner/scans" # 执行迁移(创建 3 张表) python manage.py migrate # 创建超级用户(用于登录 Admin) python manage.py createsuperuser # 输入用户名、邮箱、密码(密码需含大小写字母+数字,Django 密码策略强制) # 收集静态文件(Bootstrap/Chart.js) python manage.py collectstatic --noinput

此时db.sqlite3文件已生成,scans/目录已创建,staticfiles/下有压缩后的前端资源。

3.3 启动开发服务器并验证首条扫描任务

# 启动 Django 开发服务器(绑定 127.0.0.1:8000,禁止 0.0.0.0) python manage.py runserver 127.0.0.1:8000

打开浏览器访问http://127.0.0.1:8000/admin/,用刚创建的超级用户登录。在 Admin 中点击「Add scan task」,填写:

  • Target URL:http://httpbin.org
  • Port:80
  • Timeout seconds:60
  • Status:pending(保存后自动触发扫描)

验证是否成功:

  • 查看终端输出:应出现nweb -u http://httpbin.org -p 80 --timeout 60 --output-format json命令行;
  • 查看scans/目录:生成以任务 ID 命名的子目录(如1/),内含stdout.log(nweb 原始输出);
  • 返回 Admin,刷新任务列表:Status 应变为completed,点击进入详情页,可见Vulnerabilities关联数据;
  • 访问http://127.0.0.1:8000/(首页 Dashboard),应显示本次扫描的漏洞统计图表。

若卡在pending状态,立即检查:

  • nweb二进制是否有执行权限(ls -l ~/nweb-scanner/nweb);
  • SCAN_WORKDIR目录是否可写(ls -ld ~/nweb-scanner/scans);
  • 终端是否报Command 'nweb' not found(路径配置错误)。

4. 避坑:nweb 与 Django 集成的 4 个血泪经验,每一条都让项目延期超过 2 天

4.1 现象:nweb 扫描返回空结果,但终端显示{"vulns":[]}

原因:nweb 默认启用 DNS 解析和 SSL 验证,当目标域名无法解析或证书无效时,静默失败不报错,只输出空 JSON。Django 后端捕获到空vulns数组,误判为「无漏洞」。
解决:在调用命令中强制添加--no-dns --insecure参数,并在ScanTask模型中增加skip_dns和insecure_sslBooleanField,默认 True。前端表单提供勾选项,让用户明确选择是否跳过 DNS 和 SSL 校验。

4.2 现象:多个用户同时提交任务,数据库报IntegrityError: UNIQUE constraint failed: scans_scantask.id

原因:SQLite 在高并发写入时锁表,Django 默认ATOMIC_REQUESTS=True导致事务堆积。nweb 扫描本身是 I/O 密集型,但 Django ORM 写入是串行的。
解决:关闭ATOMIC_REQUESTS,改用select_for_update()显式加锁。在views.py创建任务视图中:

from django.db import transaction def create_task(request): with transaction.atomic(): task = ScanTask.objects.select_for_update().create( target_url=request.POST["url"], created_by=request.user ) # 启动扫描...

同时将settings.DATABASES['default']['OPTIONS'] = {'timeout': 20},避免锁等待超时。

4.3 现象:Admin 页面显示漏洞详情,但点击「Export PDF」按钮无响应

原因:前端 JS 调用/api/export-pdf/<task_id>/接口,但 Django 默认禁止跨域请求(CSRF 保护)。即使同域,Django 的CsrfViewMiddleware会拦截 POST 请求。
解决:为导出接口添加@csrf_exempt装饰器,并改用 GET 请求(PDF 生成不修改数据):

from django.utils.decorators import method_decorator from django.views.decorators.csrf import csrf_exempt @method_decorator(csrf_exempt, name='dispatch') class ExportPDFView(View): def get(self, request, task_id): # 生成 PDF 逻辑 response = HttpResponse(content_type='application/pdf') response['Content-Disposition'] = f'attachment; filename="report-{task_id}.pdf"' return response

同时在urls.py中确保该路由在urlpatterns最前,避免被其他中间件拦截。

4.4 现象:扫描完成后,ScanResult.raw_output字段存入数据库时被截断为 1000 字符

原因:DjangoTextField在 SQLite 中默认无长度限制,但nweb输出 JSON 可能超 10MB(含大量 HTTP 响应体)。SQLite 的TEXT类型理论无限长,但 Django ORM 的TextField在某些驱动下会隐式转换为VARCHAR(1000)。
解决:显式指定数据库字段类型,在models.py中:

from django.db import models class ScanResult(models.Model): raw_output = models.TextField() # SQLite 下自动映射为 TEXT # ... 其他字段

并执行python manage.py makemigrations && python manage.py migrate重建表。验证方法:手动插入一条 5MB 的 JSON 字符串,确认len(instance.raw_output)返回真实长度。

5. 生产级加固:让这个工具真正扛住甲方网络环境的 3 个硬核配置

5.1 Gunicorn + Nginx 部署:替换开发服务器,支持并发与 HTTPS

Djangorunserver仅用于开发,生产必须用 Gunicorn。配置要点:

# 安装 Gunicorn pip install gunicorn # 创建 Gunicorn 配置文件 gunicorn.conf.py import multiprocessing bind = "127.0.0.1:8001" bind_ssl_certificate = "/etc/ssl/certs/scanner.crt" bind_ssl_private_key = "/etc/ssl/private/scanner.key" workers = multiprocessing.cpu_count() * 2 + 1 worker_class = "sync" worker_connections = 1000 timeout = 300 keepalive = 5 max_requests = 1000 max_requests_jitter = 100 preload = True

Nginx 反向代理配置(/etc/nginx/sites-available/nweb-scanner):

upstream nweb_backend { server 127.0.0.1:8001; } server { listen 443 ssl http2; server_name scanner.internal; ssl_certificate /etc/ssl/certs/scanner.crt; ssl_certificate_key /etc/ssl/private/scanner.key; location / { proxy_pass http://nweb_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 300; } location /static/ { alias /home/nwebuser/nweb-scanner/staticfiles/; expires 1y; add_header Cache-Control "public, immutable"; } }

提示:proxy_read_timeout 300必须与 nweb 超时一致,否则 Nginx 先断连导致扫描中断。

5.2 数据库迁移:从 SQLite 到 PostgreSQL,解决并发与审计日志需求

甲方环境要求操作留痕、SQL 审计、备份策略。PostgreSQL 是唯一选择:

# Ubuntu 安装 PostgreSQL sudo apt install postgresql postgresql-contrib sudo -u postgres psql -c "CREATE DATABASE nweb_scanner;" sudo -u postgres psql -c "CREATE USER nweb WITH PASSWORD 'strong-pass-123';" sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE nweb_scanner TO nweb;" # 修改 settings.py DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', 'NAME': 'nweb_scanner', 'USER': 'nweb', 'PASSWORD': 'strong-pass-123', 'HOST': 'localhost', 'PORT': '5432', 'OPTIONS': { 'options': '-c default_transaction_isolation=repeatable read' } } }

关键增强:

  • default_transaction_isolation=repeatable read防止幻读,保证扫描任务状态一致性;
  • PostgreSQL 的pg_audit扩展可记录所有INSERT/UPDATE/DELETE操作,满足等保 2.0 审计要求;
  • 使用pg_dump每日自动备份,比 SQLite 文件拷贝更可靠。

5.3 权限最小化:nweb 二进制与 Django 进程的 Linux 账户隔离

这是甲方安全验收的必过项。不能让 Django 进程以nwebuser身份运行并拥有nweb执行权:

# 创建专用扫描用户(无 shell,无 home) sudo useradd -r -s /bin/false nweb-runner # 将 nweb 二进制属主改为 nweb-runner sudo chown nweb-runner:nweb-runner ~/nweb-scanner/nweb sudo chmod 750 ~/nweb-scanner/nweb # 设置 sudo 免密执行(仅允许特定命令) echo "nwebuser ALL=(nweb-runner) NOPASSWD: /home/nwebuser/nweb-scanner/nweb" | sudo tee /etc/sudoers.d/nweb sudo chmod 440 /etc/sudoers.d/nweb # Django 中调用方式改为 cmd = ["sudo", "-u", "nweb-runner", str(settings.NWEB_BINARY_PATH), ...]

这样,即使 Django 应用被攻破,攻击者也只能以nweb-runner身份执行nweb,无法读取 Django 数据库文件或 SSH 私钥。

6. 进阶技巧:用 Django QuerySet 实现「漏洞趋势分析」,3 行代码生成周报图表

6.1 从原始数据到业务洞察:定义可复用的统计维度

nweb 扫描结果存入Vulnerability表后,单纯展示漏洞列表价值有限。真正的交付物是「趋势」——比如「本周 SQL 注入漏洞环比上升 40%,集中在 /api/user 接口」。Django ORM 的聚合能力让这变得极简:

# scans/utils.py from django.db.models import Count, Sum, Avg, Q from django.utils import timezone from datetime import timedelta from scans.models import Vulnerability, ScanTask def get_weekly_vuln_trend(): """返回最近 7 天每日漏洞数量、按类型分布""" today = timezone.now().date() week_ago = today - timedelta(days=7) # 每日总数 daily_counts = ( Vulnerability.objects .filter(result__task__created_at__date__range=[week_ago, today]) .values('result__task__created_at__date') .annotate(count=Count('id')) .order_by('result__task__created_at__date') ) # 按类型统计(Top 5) type_breakdown = ( Vulnerability.objects .filter(result__task__created_at__date__range=[week_ago, today]) .values('vuln_type') .annotate(total=Count('id')) .order_by('-total')[:5] ) return { "daily": list(daily_counts), "types": list(type_breakdown) } # 在 views.py 中返回 JSON def weekly_report(request): data = get_weekly_vuln_trend() return JsonResponse(data)

前端用 Chart.js 渲染折线图(daily)和饼图(types),无需任何额外数据库查询。

6.2 高级筛选:用 Q 对象实现「组合条件漏洞搜索」

甲方安全负责人常提需求:「查出所有 high 严重度、且 payload 含UNION SELECT的 SQL 注入」。手写 SQL 易出错,Django Q 对象是最佳解:

from django.db.models import Q def search_vulnerabilities(query_params): q = Q() if query_params.get('severity'): q &= Q(severity=query_params['severity']) if query_params.get('vuln_type'): q &= Q(vuln_type__icontains=query_params['vuln_type']) if query_params.get('payload_snippet'): q &= Q(payload__icontains=query_params['payload_snippet']) return Vulnerability.objects.filter(q).select_related('result__task') # API 视图中调用 def api_search(request): params = { 'severity': request.GET.get('severity'), 'vuln_type': request.GET.get('type'), 'payload_snippet': request.GET.get('payload') } results = search_vulnerabilities(params) return JsonResponse({ "count": results.count(), "data": list(results.values('id', 'vuln_type', 'severity', 'payload', 'result__task__target_url')) })

select_related('result__task')避免 N+1 查询,一次 JOIN 获取所有关联字段。

6.3 自动化报告生成:用 WeasyPrint 替代 wkhtmltopdf,规避字体渲染玄学

之前用wkhtmltopdf生成 PDF 报告,但在 CentOS 7 上常因缺少libfontconfig.so.1崩溃。WeasyPrint 是纯 Python 的 PDF 渲染器,依赖少、兼容性好:

pip install weasyprint
# scans/reports.py from weasyprint import HTML, CSS from django.template.loader import render_to_string def generate_pdf_report(task_id): task = ScanTask.objects.get(id=task_id) html_content = render_to_string('reports/report_template.html', { 'task': task, 'vulnerabilities': task.scanresult_set.first().vulnerability_set.all() }) css = CSS(string=''' @page { size: A4; margin: 2cm; } body { font-family: "DejaVu Sans"; } table { width: 100%; border-collapse: collapse; } th, td { border: 1px solid #ddd; padding: 8px; } ''') pdf_file = HTML(string=html_content).write_pdf(stylesheets=[css]) return pdf_file # 在 ExportPDFView 中调用 def get(self, request, task_id): pdf_data = generate_pdf_report(task_id) response = HttpResponse(pdf_data, content_type='application/pdf') response['Content-Disposition'] = f'attachment; filename="nweb-report-{task_id}.pdf"' return response

关键点:font-family: "DejaVu Sans"支持中文,weasyprint自动下载并缓存字体,无需手动安装。

我坚持在每个新项目里用这套流程:先跑通nwebCLI,再封装 Django,最后加固生产。从第一次部署失败重装系统,到现在能 15 分钟完成甲方内网部署,踩过的坑都变成了 checklist。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进