本文仅供合法授权的安全研究与学习使用,请勿用于非法用途。读者应确保行为符合当地法律法规。
前言 在 Web 渗透测试中,敏感目录扫描是信息收集阶段的核心环节。一个看似普通的网站背后,往往隐藏着后台管理目录(/admin)、备份文件(/backup.zip)、配置文件(/.git/config)、测试接口(/test、/debug)等敏感路径——这些路径一旦被未授权访问,就可能直接导致数据泄露甚至服务器被控制。
敏感目录扫描的原理并不复杂:基于字典文件,对目标 URL 逐个拼接路径并发起 HTTP 请求,根据响应状态码判断路径是否存在。但要在实战中高效、稳定地完成大规模扫描,就需要解决三个工程问题:字典选择、并发控制、反封禁对抗 。
本文基于真实的 Python 实现源码,剖析两套敏感目录扫描工具的实现——一套基于 threading + Queue 的传统多线程方案,一套基于 ThreadPoolExecutor 的现代线程池方案,并讲解代理、UA 轮换等实战对抗技巧。
技术背景 HTTP 状态码判断逻辑 扫描器主要依据状态码判断路径存在性:
状态码
含义
扫描器处理
200
路径存在且可访问
确认存在,重点标记
301/302
重定向
可能存在,需跟进确认
403
禁止访问
路径存在但无权限,仍是有效信息
404
不存在
跳过
500
服务器错误
路径可能存在但触发异常
实战中还需注意”万能响应”问题——有些站点对任意路径都返回 200(SPA 前端路由),需要结合响应体长度、标题等特征做二次判断。
并发模型对比
模型
实现
特点
threading.Thread + Queue
继承Thread类,从队列消费任务
传统写法,控制精细
ThreadPoolExecutor
concurrent.futures 线程池
现代写法,API 简洁,内置 Future
反封禁对抗 大规模扫描容易触发 WAF/IPS 封禁,常用对抗手段:
User-Agent 轮换 :模拟不同浏览器,避免单一 UA 被识别
代理 IP 轮换 :通过 SOCKS5/HTTP 代理分散请求来源
请求速率控制 :合理设置超时和并发数,避免过于激进
实现思路 整体架构如下:
1 2 3 4 5 6 7 8 9 字典文件读取 → 任务队列(Queue) ↓ 多线程消费/线程池提交 ↓ 构造完整URL并发起请求 ↓ 根据状态码判断路径是否存在 ↓ 输出存在的敏感路径
源码包含三个层次的实现:
B站视频版 dirPathScan.py :threading.Thread + Queue 基础版,含 UA 轮换
自定义改造 scan_directories.py :带 argparse 命令行参数的工程化封装
目录扫描 dirBurp.py :ThreadPoolExecutor 线程池版,支持 SOCKS5 代理
核心代码解析 1. UA 轮换池 两个版本都采用了 UA 轮换策略,这里以基础版为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import randomuser_agents = [ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' , 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36' , 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36' , 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Safari/537.36' , 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36' , 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36' , 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36' ] headers = { 'User-Agent' : random.choice(user_agents), }
random.choice(user_agents) 在模块加载时随机选定一个 UA。注意这里有个小瑕疵——UA 是模块级随机一次性的,所有线程共用同一个 UA。更完善的实现应在每次请求时动态随机:
1 2 def get_headers (): return {'User-Agent' : random.choice(user_agents)}
2. 多线程扫描核心类 基础版通过继承 threading.Thread 实现多线程消费:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import threadingfrom multiprocessing import Queueimport requestsfrom requests import RequestExceptionclass DirPathScan (threading.Thread): """ 敏感路径扫描类,继承自 threading.Thread 类 """ def __init__ (self, queue ): """ 初始化敏感路径扫描类 :param queue: 任务队列,用于存储待扫描的URL """ super ().__init__() threading.Thread.__init__(self ) self .queue = queue def run (self ): """ 线程运行方法,从队列中获取URL并进行扫描 """ while not self .queue.empty(): url = self .queue.get() try : res = requests.get(url=url, headers=headers, timeout=2 ) if res.status_code == 200 : print (f"{url} 存在敏感路径 状态码:{res.status_code} " ) except RequestException as e: print (f"请求 {url} 时发生错误: {e} " )
关键点解析:
Queue 线程安全 :queue.get() 内部有锁保护,多线程并发取任务不会冲突。注意这里用的是 multiprocessing.Queue(兼容性好),实战中 queue.Queue(标准库)也完全可以
while not self.queue.empty() :线程持续从队列消费任务直到清空,这是”工作窃取”模式的雏形——快的线程会多处理任务,实现负载均衡
timeout=2 :超时设置很关键,扫描场景下宁可漏报也不要卡死
状态码注释 :源码中 403、404 的判断被注释掉了,实战中 403(禁止访问)往往意味着路径存在但无权限,是高价值信息,建议放开
3. 启动函数:字典加载与线程调度 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 def start (url, count, filePath ): """ 敏感路径扫描的启动函数 :param url: 需要扫描的基础URL :param count: 线程数量 :param filePath: 字典文件路径 """ queue = Queue() with open (filePath, "r" ) as file: for i in file: queue.put(url + i.rstrip('\n' )) threads = [] threading_count = int (count) for i in range (threading_count): threads.append(DirPathScan(queue)) for t in threads: t.start() for t in threads: t.join()
流程清晰:读取字典文件逐行放入队列 → 创建 N 个扫描线程 → 全部启动 → 等待全部完成。rstrip('\n') 去掉换行符,避免拼接出 http://target.com/admin\n 这样的错误 URL。
4. 命令行参数封装 自定义改造版增加了 argparse 支持,让工具更易用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import argparseimport dirPathScandef start (url, count, filePath ): queue = Queue() with open (filePath, "r" ) as file: for i in file: queue.put(url + i.rstrip('\n' )) threads = [] threading_count = int (count) for i in range (threading_count): threads.append(dirPathScan.DirPathScan(queue)) for t in threads: t.start() for t in threads: t.join() parser = argparse.ArgumentParser(description='敏感目录扫描工具' ) parser.add_argument('--url' , type =str , required=True , help ='目标URL' ) parser.add_argument('--filePath' , type =str , required=True , help ='文件路径' ) parser.add_argument('--count' , type =int , required=True , help ='计数值' ) args = parser.parse_args() url = args.url filePath = args.filePath count = args.count start(url=url, count=count, filePath=filePath)
三个必填参数:--url 目标地址、--filePath 字典路径、--count 线程数。这样工具就可以直接命令行调用,不用改代码。
5. 线程池版 + SOCKS5 代理 dirBurp.py 采用了更现代的 ThreadPoolExecutor 写法,并加入了代理支持:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 from concurrent.futures import ThreadPoolExecutor, as_completedimport requestsproxies = { 'http' : 'socks5://127.0.0.1:33333' , 'https' : 'socks5://127.0.0.1:33333' } base_url = "http://192.168.1.11:9999" def load_directories (file_path ): with open (file_path, 'r' , encoding='utf-8' ) as f: return [line.strip() for line in f if line.strip()] def request_directory (directory ): url = f"{base_url} /{directory} " try : response = requests.get(url, proxies=proxies, timeout=10 ) if response.status_code in [200 ]: print (f"[+] 找到: {url} - 状态码: {response.status_code} " ) except requests.exceptions.RequestException as e: pass def directory_brute_force (base_url, directories, max_workers=10 ): with ThreadPoolExecutor(max_workers=max_workers) as executor: future_to_directory = {executor.submit(request_directory, directory): directory for directory in directories} for future in as_completed(future_to_directory): directory = future_to_directory[future] try : future.result() except Exception as e: print (f"[-] 处理 {directory} 时发生异常: {e} " )
相比 threading.Thread 方案,ThreadPoolExecutor 有几个优势:
API 更简洁 :submit + as_completed 替代手动管理线程生命周期
Future 机制 :每个任务返回 Future 对象,可以获取返回值和异常
上下文管理 :with 语句自动等待所有任务完成,无需手动 join
SOCKS5 代理的加入让这个版本更适合实战——当目标有 IP 频率限制时,通过代理池轮换 IP 可以持续扫描。注意使用 SOCKS 代理需要安装 pip install requests[socks]。
主程序入口:
1 2 3 4 5 6 7 8 if __name__ == "__main__" : dir_file = "dir.txt" directories = load_directories(dir_file) if not directories: print ("[-] 目录列表为空或文件未找到。" ) else : print (f"[*] 已加载 {len (directories)} 个目录进行爆破。" ) directory_brute_force(base_url, directories)
使用方法与运行效果 基础版(无参数) 1 2 3 4 5 if __name__ == '__main__' : url = 'https://www.example.com' filePath = 'PHP.txt' count = 32 start(url=url, count=count, filePath=filePath)
命令行版 1 python scan_directories.py --url https://www.example.com --filePath PHP.txt --count 32
代理版
运行效果示例 1 2 3 4 5 6 [*] 已加载 3528 个目录进行爆破。 [+] 找到: http://192.168.1.11:9999/admin - 状态码: 200 [+] 找到: http://192.168.1.11:9999/login.php - 状态码: 200 [+] 找到: http://192.168.1.11:9999/.git/config - 状态码: 200 [+] 找到: http://192.168.1.11:9999/backup.zip - 状态码: 200 [+] 找到: http://192.168.1.11:9999/phpinfo.php - 状态码: 200
发现 .git/config 意味着可进一步用 GitHack 工具恢复源码;backup.zip 可能包含整站源码和数据库配置;phpinfo.php 泄露服务器完整环境信息——这些都是高危发现。
防御对策 蓝队可从以下维度防御敏感目录扫描:
删除/迁移敏感路径 :上线前检查并删除 /admin、/test、/debug、/.git、/backup 等敏感路径,后台管理目录应改为非常规路径并限制 IP 访问。
关闭目录列表 :Web 服务器关闭 autoindex(Nginx)/ Options +Indexes(Apache),避免目录结构泄露。
部署 WAF :配置 WAF 规则识别扫描行为(短时间大量 404 请求),自动封禁来源 IP。
统一错误响应 :对不存在的路径统一返回自定义 404 页面(而非服务器默认 404),避免通过响应差异判断路径存在性。
速率限制 :对单 IP 的请求频率做限制(如 Nginx limit_req),增加扫描成本。
敏感文件监控 :部署文件完整性监控,及时发现 .git、.svn、.env 等敏感文件的意外暴露。
总结 敏感目录扫描是 Web 渗透的”敲门砖”——简单但不可或缺。本文剖析的两套实现代表了两种典型的 Python 并发编程范式:threading + Queue 适合需要精细控制的场景,ThreadPoolExecutor 适合快速开发。
从源码中可以提炼几个实战要点:一是字典质量决定扫描效果,PHP、Java、Python 不同技术栈要用不同字典;二是并发数并非越高越好,过高反而触发封禁导致漏报,一般 20-50 线程为宜;三是代理和 UA 轮换是应对封禁的基本功,实战中必不可少。
一个优秀的扫描器还需要在此基础上增加:响应体长度去重(对付万能 200)、递归扫描(发现目录后继续扫描子目录)、结果自动验证(对 200 结果截图或检查内容)等能力。这些扩展留待读者在实战中逐步完善。