ARTICLE DETAIL

资讯详情

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

Python + PyQt5 实现信息收集工具箱:子域名、端口扫描与指纹识别实战

Python + PyQt5 实现信息收集工具箱:子域名、端口扫描与指纹识别实战 简介这是一份基于Python的图形化信息收集与渗透测试工具源代码包主要面向安全入门者、渗透测试工程师与Python开发人员旨在解决从目标探测到服务识别的常见需求。工具将端口扫描、敏感文件探测、子域名发现、WHOIS查询、指纹识别、服务器信息收集等功能整合到PyQt5图形界面中使用者无需逐条输入命令即可完成侦查任务适合在授权测试、CTF比赛或日常学习中使用。包内共39个文件包含14个.py主程序、12个.pyc编译文件、6个.txt字典与说明文档、2个配置文件、1个UI界面文件及少量图标资源压缩包仅310KB形态小巧、目录模块划分清楚。源码中封装了socket/nmap端口扫描、目录暴力破解、CMS与服务器信息识别等模块同时附带字典文件与运行配置便于直接运行、二次开发或对照学习。该资源已有770人浏览学习对于希望掌握信息收集方法论、Python安全工具编写及PyQt5界面开发的人来说是一份参考价值较高的开源样例。1. 一套带图形界面的 Python 信息收集工具箱它能替你省掉哪些重复劳动做信息收集和渗透辅助测试的人都有过这种体验拿到一个授权目标先要在终端里来回切换七八个工具——子域名用一个脚本端口扫描切到 masscan指纹识别又要去翻在线平台最后结果散落在一堆文本文件里。这份源码把信息收集最常见的四件事——子域名枚举、端口扫描、HTTP 指纹识别、目录枚举——收进一个 PyQt5 图形界面工具里。填一个目标点开始扫描后台任务按阶段跑完结果汇总到表格能导出 CSV 和 HTML 报告。它解决的是安全测试前期最琐碎、最容易被重复劳动消耗的部分适合刚学 Python 网络编程、想看看完整 GUI 工具怎么串起来的人也适合需要在授权范围内快速出收集结果的场景。2. 整体架构与技术选型先想清楚模块边界再动手写代码这套源码拿到手之后第一件事不是急着跑起来而是先把目录结构看明白。信息收集工具最怕写成一个大文件所有函数堆在一起改一个端口扫描逻辑要拉着界面代码一起滚后期根本维护不动。这份源码把核心逻辑和界面层做了分离动手改之前先看懂它的边界。2.1 模块划分与目录结构recon_tool/ ├── main.py # 程序入口创建 QApplication拉起主窗口 ├── core/ # 业务逻辑不依赖任何 Qt 代码 │ ├── __init__.py │ ├── dns_utils.py # 子域名枚举 │ ├── port_scanner.py # 端口扫描 │ ├── http_probe.py # HTTP 探测与指纹识别 │ ├── dir_brute.py # 目录枚举 │ └── reporter.py # 结果汇总、CSV/HTML 导出 ├── ui/ │ ├── __init__.py │ ├── main_window.py # 主窗口布局与信号槽连接 │ └── worker.py # QThread 后台扫描线程 ├── dict/ │ ├── subnames.txt # 每行一个子域名前缀 │ └── dirs.txt # 每行一个目录/文件路径 ├── output/ # 扫描报告默认输出目录 └── requirements.txt # PyQt5、requests、dnspythoncore 和 ui 双层结构是这个工具最关键的设计。core 里全部是纯 Python 网络代码不 import 任何 Qt 组件这样扫描逻辑可以单独在命令行里测试不依赖界面ui 层只负责把 core 的返回结果显示出来。我习惯把“能跑通的逻辑”和“好看的窗口”彻底分开否则改一次界面布局就要重新测一遍扫描非常浪费时间。dict 目录里是两个纯文本字典每行一个词词条不要带点号和斜杠前缀字符由代码统一处理。subnames.txt 里放常见的 www、mail、api、admin、dev、test、oa 这类dirs.txt 里放 admin、login、api、swagger、backup、.git、.env 这类。字典是纯文本谁都能改不需要重新编译这也是工具落地后最好扩展的部分。2.2 为什么选 PyQt5 而不是 Tkinter 或 Web 界面信息收集工具需要同时展示结果表、日志区、进度状态界面的复杂程度决定了框架选择。Tkinter 上手成本确实最低但它的表格控件在持续刷新大量结果时明显发虚滚动也不顺而且 Python 的线程更新 Tkinter 控件时需要自己在回调里做线程切换写起来容易翻车。PyQt5 的 QThread 加信号槽机制把“子线程发结果 → 主线程刷新界面”这条链路设计得很顺QTableWidget 处理几千行结果也没有压力。方案上手成本表格与多区域展示后台线程打包体积结论Tkinter最低控件简陋结果多时排版吃力可以但回调要自己处理线程切换小只适合几十行的练手工具PyQt5中等QTableWidget / QTableView 成熟QThread 信号槽设计顺中等本场景最合适Flask Web中上浏览器展示灵活任务状态和跨线程通信全要自己维护要带浏览器本地工具用它偏重为什么不选 Web 方案信息收集工具是要在测试机上直接跑的起 Flask 服务还得自己开浏览器进程管理、端口占用、任务状态同步全变成额外负担。PyQt5 的控件库在这个场景里够用且不过剩所以我一般直接选它。2.3 线程模型QThread 加信号槽扫描和界面必须分家# ui/worker.py from PyQt5.QtCore import QThread, pyqtSignal class ScanWorker(QThread): progress pyqtSignal(int, str) # 当前完成百分比、阶段名称 result_ready pyqtSignal(dict) # 每发现一条结果发一次 all_done pyqtSignal(list) # 全部任务收尾带上完整结果 def __init__(self, target, config, parentNone): super().__init__(parent) self.target target self.config config self._is_cancelled False def cancel(self): self._is_cancelled True def run(self): results [] stages [子域名枚举, 端口扫描, 指纹识别, 目录枚举] for i, stage in enumerate(stages): if self._is_cancelled: break self.progress.emit(i * 25, f正在执行{stage}) # 这里调用 core 包里的对应函数把结果追加到 results # 每个阶段内部通过 result_ready 实时上报单条结果 self.all_done.emit(results)QThread 的 run() 在子线程里执行主线程的 QEventLoop 不会被阻塞。pyqtSignal 是线程安全的跨线程 emit 之后连接到的槽函数会在接收方所在线程执行所以界面更新都写在主窗口的槽函数里不要自己在 worker 里操作控件。ScanWorker 里放的是 cancel() 而不是 terminate()QThread 的 terminate 不可控可能在任何一条字节码处停掉线程正在跑的 socket 连接不会正常关闭下次扫描同一目标时可能出现 TIME_WAIT 堆积Windows 下尤其明显。用标志位在每轮循环开头判断虽然不能立刻停但最多延迟一个任务的时间安全得多。跑起来之前先安装依赖pip install -r requirements.txt里面主要有 PyQt5、requests、dnspython 三个包。命令行测试 core 模块可以直接python -c from core.port_scanner import scan_host; print(scan_host(127.0.0.1))不需要开界面这也是模块分离带来的好处。3. 信息收集模块实战子域名枚举、端口扫描与指纹识别的实现细节这一章是整套源码里最值得花时间读的部分。四个信息收集模块各自独立参数也各有讲究我会直接贴代码再讲逻辑方便你对照着改。3.1 子域名枚举字典加 DNS 解析线程数不是越大越好# core/dns_utils.py import socket from concurrent.futures import ThreadPoolExecutor def resolve_with_timeout(subdomain, timeout2.0): try: infos socket.getaddrinfo(subdomain, None, socket.AF_INET) ip infos[0][4][0] return subdomain, ip except socket.gaierror: return None def enum_subdomains(domain, wordlist_pathdict/subnames.txt, max_workers50): try: with open(wordlist_path, encodingutf-8) as fp: subs [line.strip() for line in fp if line.strip()] except FileNotFoundError: return [] resolved [] with ThreadPoolExecutor(max_workersmax_workers) as pool: futures [] for sub in subs: hostname f{sub}.{domain} futures.append(pool.submit(resolve_with_timeout, hostname)) for fut in futures: result fut.result() if result: resolved.append(result) return resolved这段逻辑不复杂对字典里每个前缀拼出完整域名调用 getaddrinfo 解析解析成功的记录域名和 IPgaierror 说明该子域名不存在直接跳过。这里没有用 gethostbyname因为它在多线程并发时对系统解析器的压力更大更容易触发超时getaddrinfo 是标准库层面更稳的选择。max_workers 默认 50常见做法是 3080 之间线程数开太大反而会让系统解析器并发受限出现大面积超时结果看着像目标没有子域名其实是自己把线程数堆崩了。timeout 参数控制单个域名解析等待时间外网目标设 2 秒是个保守值内网 DNS 环境可以降到 0.5 秒。3.2 端口扫描TCP connect 扫描加线程池超时与并发要一起调# core/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, 1433, 1521, 3306, 3389, 5432, 6379, 7001, 8000, 8080, 8443, 8888, 9200, 11211, 27017] def scan_port(host, port, timeout1.0): sock socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(timeout) code sock.connect_ex((host, port)) sock.close() return port, code 0 def scan_host(host, portsNone, timeout1.0, max_workers200): ports ports or COMMON_PORTS open_ports [] with ThreadPoolExecutor(max_workersmax_workers) as pool: futures {pool.submit(scan_port, host, p, timeout): p for p in ports} for fut in futures: port, is_open fut.result() if is_open: open_ports.append(port) return sorted(open_ports)connect_ex 返回 0 才代表端口开放返回 111 是连接被拒绝返回 socket.timeout 是目标没响应这两种都算关闭或被过滤。这里用的是 TCP connect 扫描不需要 root 权限Windows 和 Linux 都能跑代价是会在目标系统留下完整连接记录所以只适合授权测试。ThreadPoolExecutor 把 30 个常用端口的扫描压到秒级是因为 socket 连接期间 GIL 会被释放线程池做这种 I/O 密集型任务是有效果的。参数默认值建议timeout1.0 秒内网 0.5 秒外网 1.5 秒max_workers200100200 之间超过会吃满文件描述符portsCOMMON_PORTS可改成 range(1, 65536)但不建议在线程池里全扫Windows 下大量并发 socket 可能触发资源限制我一般控制在 200 以内。如果要全端口扫描更稳的做法是把它改成分批扫描每批 1000 个端口中间 sleep 一下释放句柄。3.3 HTTP 指纹识别别只信 Server 字段综合特征才靠得住# core/http_probe.py import requests def grab_banner(url, timeout5.0): headers {User-Agent: Mozilla/5.0 recon-tool/1.0} try: r requests.get(url, timeouttimeout, headersheaders, allow_redirectsTrue) if r.encoding is None or r.encoding.lower() iso-8859-1: r.encoding r.apparent_encoding page r.text[:200000] except requests.RequestException: return None return { url: url, status: r.status_code, server: r.headers.get(Server, ), powered: r.headers.get(X-Powered-By, ), title: extract_title(page), page: page, cookies: r.headers.get(Set-Cookie, ), } def detect_cms(info): signatures { WordPress: [wp-content, wp-includes], ThinkPHP: [thinkphp, /think/], 织梦CMS: [dedecms, powered by dedecms], Shiro: [rememberMedeleteMe], } page info.get(page, )[:200000] for cms, marks in signatures.items(): if any(m.lower() in page.lower() for m in marks): info.setdefault(signature, []).append(cms) return info指纹识别最容易翻车的地方是只读 Server 响应头。现在很多目标套了 CDN 或反向代理Server 字段显示的是 nginx 或 cloudflare真实中间件被藏在后面。所以这段代码同时抓了 X-Powered-By、页面标题、HTML 特征和 Set-Cookie 里的特征字符串。Shiro 的识别靠的是未登录时响应里会出现rememberMedeleteMe这段 Cookie 特征这个比看路径判断靠谱得多。requests 的 text 编码推断偶尔会失灵常见做法是显式判断编码后用 apparent_encoding 兜底避免页面乱码导致特征匹配失败。页面只截前 200KB是防止大页面拖慢匹配速度。3.4 目录枚举软 404 是最大的误报来源# core/dir_brute.py import requests def get_404_length(base_url, timeout3.0): probe base_url /nonexistent_path_9f8e7d6c try: r requests.get(probe, timeouttimeout, allow_redirectsFalse) return len(r.content), r.status_code except requests.RequestException: return 0, 404 def dir_brute(base_url, wordlist_pathdict/dirs.txt, timeout3.0, threads20, allow_status(200, 301, 302, 403)): base_len, _ get_404_length(base_url) with open(wordlist_path, encodingutf-8) as fp: words [line.strip() for line in fp if line.strip()] results [] def check(word): if not word.startswith(/): word / word try: r requests.get(base_url word, timeouttimeout, allow_redirectsFalse) except requests.RequestException: return None if r.status_code not in allow_status: return None # 长度和 404 基线接近的看成软 404 if abs(len(r.content) - base_len) 50: return None return word, r.status_code, len(r.content) with ThreadPoolExecutor(max_workersthreads) as pool: for item in pool.map(check, words): if item: results.append(item) return results这个模块的判断逻辑比端口扫描复杂很多框架对不存在的路径也返回 200页面内容是一个固定模板这种叫软 404。如果直接把所有 200 都当成可用路径结果表会被刷屏。常见做法是先请求一个随机路径拿到目标 404 页面的响应体长度作为基线再把扫描结果的页面长度和基线做差值比较差距小于 50 字节的视为同一个模板直接过滤。我这里用 get_404_length 先探基线再在 check 里按长度差过滤两个步骤合起来能挡掉大部分假阳性。allow_status 为什么包含 403有时候路径真实存在但被禁止访问403 本身就是有价值的信息值得记录到结果表里。4. 图形界面落地PyQt5 主窗体、任务队列与结果回显core 层跑通之后界面层就是把扫描线程和表格、日志、按钮接起来。窗口本身用 QWidget 手工布局就行不需要 Designer代码量不大反而更容易看懂每个控件的位置关系。4.1 主窗口布局输入区、按钮区、结果表、日志区# ui/main_window.py from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLineEdit, QPushButton, QTableWidget, QTableWidgetItem, QPlainTextEdit, QHeaderView) class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle(信息收集与渗透辅助测试工具) self.resize(960, 640) self.worker None self._build_ui() def _build_ui(self): central QWidget(self) root QVBoxLayout(central) bar QHBoxLayout() self.target_input QLineEdit() self.target_input.setPlaceholderText(输入目标域名或 IP例如 example.com) self.start_btn QPushButton(开始扫描) self.stop_btn QPushButton(停止) self.stop_btn.setEnabled(False) bar.addWidget(self.target_input, 3) bar.addWidget(self.start_btn, 1) bar.addWidget(self.stop_btn, 1) root.addLayout(bar) self.table QTableWidget(0, 5) self.table.setHorizontalHeaderLabels([类型, 目标, 结果, 状态码, 备注]) self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) root.addWidget(self.table, 4) self.log_view QPlainTextEdit() self.log_view.setReadOnly(True) root.addWidget(self.log_view, 1) self.setCentralWidget(central)布局用两个 layout 套起来水平栏放输入框和按钮垂直方向上面是结果表下面是日志区。addWidget 的第二个参数是拉伸因子输入框给 3、按钮给 1窗口拉宽时输入框占用更多空间日志区固定占据底部一部分。QHeaderView.Stretch 让表头自动撑满整列宽度省去手动调列宽。setPlaceholderText 是给用户看的输入提示不是实际内容。整个窗口只有两个事件需要处理开始、停止其余都是数据驱动刷新。4.2 信号槽连接扫描线程不要碰控件结果通过信号回传def start_scan(self): target self.target_input.text().strip() if not target: self.log_view.appendPlainText(目标不能为空) return config {timeout: 1.0, max_workers: 100} self.worker ScanWorker(target, config) self.worker.progress.connect(self.update_progress) self.worker.result_ready.connect(self.append_result) self.worker.all_done.connect(self.on_finished) self.start_btn.setEnabled(False) self.stop_btn.setEnabled(True) self.worker.start() def update_progress(self, percent, stage): self.log_view.appendPlainText(f[{percent}%] {stage}) def append_result(self, item: dict): row self.table.rowCount() self.table.insertRow(row) values [item.get(type, ), item.get(target, ), item.get(result, ), str(item.get(status, )), item.get(note, )] for col, val in enumerate(values): self.table.setItem(row, col, QTableWidgetItem(val)) def on_finished(self, results): self.log_view.appendPlainText(f扫描完成共 {len(results)} 条结果) self.start_btn.setEnabled(True) self.stop_btn.setEnabled(False)start_scan 里做了三件事读取输入框内容并 strip 掉空格、构造 ScanWorker 并设置扫描参数、连接信号。这里有个关键细节progress 信号携带两个参数 (int, str)连接的是 update_progress 槽而不是直接接到 appendPlainText因为槽函数的参数数量和类型必须和信号匹配直接连会导致类型不匹配的警告界面上看不出问题但日志不更新。append_result 在每次扫描线程发现一条结果时被调用插入新行逐列填充 QTableWidgetItem。on_finished 里把按钮状态恢复避免扫描完还停在不可点状态。停止按钮的逻辑就是调用 worker.cancel()前面已经说过用标志位而不是 terminate。4.3 结果导出CSV 编码和换行是两个隐形坑import csv def export_csv(self, pathoutput/report.csv): with open(path, w, encodingutf-8-sig, newline) as fp: writer csv.writer(fp) writer.writerow([类型, 目标, 结果, 状态码, 备注]) for row in range(self.table.rowCount()): writer.writerow([ self.table.item(row, c).text() if self.table.item(row, c) else for c in range(self.table.columnCount()) ]) self.log_view.appendPlainText(f报告已导出{path})csv 导出看起来简单实际有两个坑。第一文件编码必须是utf-8-sig普通 utf-8 无 BOM 写出来的文件用 Excel 打开直接乱码这是 Windows 环境最容易踩的。第二newline必须写否则在 Windows 下 csv 模块写每一行末尾会自动加一个空行导出的文件行间距放宽两倍。QTableWidgetItem 在某一格为空时是 None取值前要判断这就是代码里if self.table.item(row, c)的原因。导出按钮在界面上触发后日志区会打印导出路径output 目录不存在时记得先os.makedirs(output, exist_okTrue)。5. 常见问题与避坑五个典型的翻车现场与解决路径这套源码我前前后后跑了很多次翻车最集中的就这几个地方。每一条都是按“现象 → 原因 → 解决”的顺序写的对照着排查比从头看代码快。5.1 界面假死点击开始后窗口转圈任务结束才恢复现象点“开始扫描”后窗口变成白板拖不动也关不掉扫描结束瞬间恢复日志一次性全刷出来。原因扫描任务被直接写在按钮的回调函数里主线程被 socket 连接和 DNS 解析占住Qt 的事件循环收不到重绘和点击事件表现就是假死。解决所有耗时任务必须放进 QThread 的 run()通过信号把结果发回主线程。判断方法很简单在按钮回调里加一条print(start)如果点击后这个 print 后面跟着的程序迟迟不往下走说明主线程被阻塞了。把这个任务挪进 ScanWorker 的 run() 就能解决。5.2 子域名结果少得可怜不是目标没有子域名是解析线程太多了现象字典 500 条词跑完只有 20 条结果终端里大量超时同一批词换个时间跑结果数量还不一样。原因max_workers 开到 300系统解析器并发处理不过来大量 DNS 查询直接在底层超时。另外某些目标对高频 DNS 请求本身有速率限制。解决把 max_workers 降到 50timeout 从 1 秒调到 2 秒。如果还是少换 dnspython 的 resolver 做递归查询设置resolver.lifetime 3.0它比系统解析器能拿到更完整的结果。我一般会先用 50 线程跑一遍结果里挑存活的主域名再追加一批子域词而不是一次性把所有线程压上去。5.3 目录扫描刷出几十个 200全是软 404 在捣乱现象dirs.txt 里 200 条路径扫出来 150 条“200 OK”但打开看全是同一个“页面不存在”模板。原因框架对未定义路由统一返回 200响应体是固定模板长度一模一样。只按状态码判断不按响应内容判断就会全盘接收。解决在扫描前先请求一个随机路径作为 404 基线再用响应体长度差过滤。这块的完整代码在 3.4 节已经给过核心就是abs(len(r.content) - base_len) 50直接丢弃。注意有的站点模板里带时间戳长度会有轻微浮动阈值 50 是个合理的默认值遇到动态页面可以放宽到 100。5.4 CSV 导出后用 Excel 打开全乱码现象扫描结果用记事本打开正常用 Excel 打开都是乱码或者第一行正常后面错位。原因文件用普通 utf-8 编码写入Excel 默认按本地编码GBK猜读不到正确的字节序。解决导出时编码用utf-8-sig这个编码会在文件头写上 BOMExcel 识别到 BOM 后自动切到 UTF-8。同时写入newline参数避免 Windows 下每行多一个回车。这两处已经写进 4.3 的 export_csv 里了如果你自己重写导出逻辑务必保留这两项。5.5 PyInstaller 打包后双击没反应或报 Qt 平台插件错误现象在没装 Python 的机器上双击 exe闪退命令行运行报Could not find or load the Qt platform plugin windows。原因PyInstaller 默认不收集 PyQt5 的整个插件目录platforms 文件夹里的 qwindows.dll 没被带进包Qt 找不到平台插件就直接退出。解决打包命令加--collect-all PyQt5把插件、翻译文件和依赖一起收进去pyinstaller -w --collect-all PyQt5 -n recon_tool main.py-w表示不弹控制台窗口。打包后先用命令行recon_tool.exe跑一下看报错再双击测试。加了--collect-all包体积会增加但换来的是换台机器也能开这点体积可接受。6. 结果导出与验证把扫描结果变成可复现的取证报告工具能出结果不等于结果可信我每次改完代码都强制走一遍验证流程。验证目标不找外网直接在本机起服务可控性最强python -m http.server 8080 --bind 127.0.0.1在另一个终端同时起一个 FTP 或 SSH 服务让目标机有两个开放端口。然后在工具里输入127.0.0.1跑一遍全流程。预期结果端口扫描能识别 8080 和另一个端口指纹识别能认出 Python http.server 的特征目录枚举在本地静态服务上能扫到真实文件。这个基线能过再换到授权目标。报告生成我习惯用 HTML 而不是纯 CSV因为 HTML 能带上颜色标记和备注列直接丢给协作的人打开就能看。reporter.py 里的入口函数长这样def generate_html(results, pathoutput/report.html): rows .join( ftrtd{r.get(type)}/tdtd{r.get(target)}/td ftd{r.get(result)}/tdtd{r.get(status)}/td ftd{r.get(note)}/td/tr for r in results ) html f!DOCTYPE htmlhtmlheadmeta charsetutf-8 title信息收集报告/title/headbody h3信息收集结果/h3 table border1 cellspacing0 cellpadding6{rows}/table /body/html with open(path, w, encodingutf-8) as fp: fp.write(html)生成报告后我会拿 nmap 的结果对拍一次端口表。工具里扫到的开放端口应该和nmap -sT -p 1-10000 目标的结果保持一致不一致就往防火墙策略或超时参数上排查。指纹识别这一项拿一个已知跑 WordPress 的测试站验证识别不出来才需要怀疑是特征库没匹配上。还有一条验证习惯值得单独说每次改完扫描参数或过滤阈值先跑本地基线再跑授权目标最后核对导出的 HTML 报告。从那以后我每次改完逻辑都强制走一遍这个流程这套工具出过的问题百分之九十都发生在改动后没有回归测试的时候。希望帮到你。本文还有配套的精品资源点击获取
返回列表