python

dirsearch 二开实战:集成 EHole 指纹识别与 Swagger 未授权检测

2026-07-06 #渗透测试#python#二次开发

dirsearch 是 Web 目录扫描的瑞士军刀。但它只管扫,扫到的东西是什么——不知道。这次二开把 EHole 指纹识别、Swagger 未授权检测、Spring Boot Actuator 泄漏检测等七个模块焊进去,让它不光能”找到”,还能”认出”。


为什么二开而不是重写

dirsearch 有几个很难超越的优势:

  • 成熟的重定向处理:301/302 跟随、递归检测
  • 403 绕过:8 种绕过技术(X-Forwarded-For、大小写、路径爆破等)
  • 完善的字典系统:按框架分类的专用字典(Spring Boot、若依、Laravel 等)
  • 代理和延迟控制:防止被 WAF 封

在这些基础设施上做增量,比从零重写划算得多。dirsearchplus 的做法是在 dirsearch 的扫描结果上叠加识别层。


架构:扫描 + 识别 双层结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
dirsearch 扫描引擎 (lib/)

├─► 目录扫描(原始 dirsearch 核心)
├─► 403 绕过测试


增强识别层(本次二开加入)

├─► JS 信息收集 → 从 JS 文件中提取 API/路径/密钥
├─► EHole 指纹识别 → 识别 CMS/框架类型
├─► Packer-Fuzzer → 前端打包器源码泄漏检测
├─► Swagger 未授权检测 → /swagger-ui.html 等
├─► Spring Boot Actuator → /actuator/env 等
├─► SubFinder 子域名爆破
├─► HPP/HFP 参数污染检测
└─► SSRF 深度探测

模块一:JS 信息收集

扫描过程中发现 .js 文件不是终点,里面可能藏了 API 路由、内部路径、甚至硬编码密钥:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import re

JS_PATTERNS = {
'api_endpoints': r'["\'](/api/v\d+/[\w/]+)["\']',
'internal_paths': r'["\'](/(?:admin|manage|dashboard|internal)/[\w/]+)["\']',
'secrets': r'(?:api[_-]?key|secret|token|password)\s*[:=]\s*["\']([^"\']+)["\']',
'aws_keys': r'AKIA[0-9A-Z]{16}',
'jwt_tokens': r'eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]+',
}

def analyze_js_file(js_content):
"""从 JS 文件中提取敏感信息"""
findings = {}

for name, pattern in JS_PATTERNS.items():
matches = re.findall(pattern, js_content, re.IGNORECASE)
if matches:
findings[name] = list(set(matches)) # 去重

return findings

实战中,这个模块经常能从打包后的 app.xxx.js 里直接捞出后台 API 路由表。


模块二:EHole 指纹识别

发现 Web 服务后,识别它跑的是什么系统——若依?Spring Boot?ThinkPHP?每种框架都有”指纹”:

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
EHole_FINGERPRINTS = {
'Ruoyi': {
'paths': ['/ruoyi', '/profile/avatar', '/dev-api/'],
'headers': {
'X-Powered-By': r'RuoYi',
},
'body': [
r'ruoyi\.js',
r'若依',
r'/ruoyi/login',
],
},
'SpringBoot': {
'paths': ['/actuator', '/error'],
'body': [
r'Whitelabel Error Page',
r'{"timestamp":".*","status":\d+',
],
},
'Nacos': {
'paths': ['/nacos/'],
'body': [
r'<title>Nacos</title>',
r'console-ui/public/js/',
],
},
'Swagger': {
'paths': ['/swagger-ui.html', '/swagger-resources', '/v2/api-docs',
'/v3/api-docs', '/doc.html'],
'body': [
r'swagger-ui',
r'Swagger UI',
],
},
'Druid': {
'paths': ['/druid/index.html', '/druid/login.html'],
'body': [r'Druid Stat Index'],
},
}

def identify_fingerprint(target_url, session):
"""扫描目标并识别指纹"""
results = []

for name, fingerprint in EHole_FINGERPRINTS.items():
matched = False
evidence = []

# 1. 检查路径
for path in fingerprint.get('paths', []):
resp = session.get(f"{target_url}{path}", timeout=5)
if resp.status_code == 200:
matched = True
evidence.append(f"路径命中: {path}")

# 2. 检查响应头
for header, pattern in fingerprint.get('headers', {}).items():
value = resp.headers.get(header, '')
if re.search(pattern, value):
matched = True
evidence.append(f"Header命中: {header}={value}")

# 3. 检查页面内容
for pattern in fingerprint.get('body', []):
if re.search(pattern, resp.text, re.IGNORECASE):
matched = True
evidence.append(f"Body命中: {pattern[:50]}")

if matched:
results.append({
'name': name,
'confidence': 'high' if len(evidence) >= 2 else 'medium',
'evidence': evidence,
})

return results

实战价值:识别出若依框架后,自动加载若依专用字典(ruoyi-endpoints.txt),针对已知的若依漏洞端点做定向爆破。


模块三:Swagger 未授权检测

Swagger 文档泄露是近几年的高频漏洞。常见路径包括:

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
41
SWAGGER_PATHS = [
'/swagger-ui.html',
'/swagger-ui/index.html',
'/swagger-resources',
'/v2/api-docs',
'/v3/api-docs',
'/doc.html', # Knife4j(国产 Swagger UI)
'/api-docs',
'/swagger.json',
'/swagger.yaml',
'/api/swagger-ui.html',
]

def check_swagger(target_url, session):
"""多路径探测 Swagger 未授权访问"""
for path in SWAGGER_PATHS:
try:
resp = session.get(f"{target_url}{path}", timeout=5,
allow_redirects=True)
if resp.status_code == 200:
content_type = resp.headers.get('Content-Type', '')

# 检测 Swagger 特征
swagger_indicators = [
'swagger-ui',
'"swagger":',
'"openapi":',
'Swagger UI',
'Knife4j',
]
for indicator in swagger_indicators:
if indicator in resp.text[:2000]:
return {
'url': f"{target_url}{path}",
'type': 'Swagger UI' if 'swagger-ui' in resp.text else 'OpenAPI JSON',
'accessible': True,
}
except:
continue

return None

模块四:Spring Boot Actuator 敏感端点

Spring Boot 的 actuator 是信息泄露重灾区:

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
ACTUATOR_ENDPOINTS = {
'/actuator/env': '环境变量(含数据库密码)',
'/actuator/configprops': '配置属性',
'/actuator/mappings': '所有 API 路由映射',
'/actuator/beans': 'Spring Bean 列表',
'/actuator/heapdump': '堆转储(可提取明文密码)',
'/actuator/threaddump': '线程转储',
'/actuator/logfile': '日志文件',
'/actuator/gateway/routes': 'Spring Cloud Gateway 路由',
'/env': '环境变量(旧版路径)',
'/mappings': '路由映射(旧版路径)',
}

def check_actuator(target_url, session):
"""探测 Spring Boot Actuator 端点"""
findings = []

for endpoint, description in ACTUATOR_ENDPOINTS.items():
try:
resp = session.get(f"{target_url}{endpoint}", timeout=5)
if resp.status_code == 200:
# 确认不是误报(真正的 actuator 返回 JSON)
if resp.headers.get('Content-Type', '').startswith('application/json'):
findings.append({
'endpoint': endpoint,
'description': description,
'severity': 'high' if endpoint.endswith(('env', 'heapdump')) else 'medium',
})
except:
continue

return findings

模块五:Packer-Fuzzer 前端源码泄漏检测

现代前端项目用 Webpack/Vite 打包后,.map 文件(source map)和未清理的源码目录常常暴露在 Web 目录下:

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
PACKER_PATHS = [
# Source Map 文件
'static/js/app.js.map',
'static/js/chunk-vendors.js.map',
'js/app.%.js.map', # % 替代 hash
# 源码目录
'.git/HEAD',
'.svn/entries',
'.DS_Store',
# 配置文件
'vue.config.js',
'webpack.config.js',
'vite.config.js',
# CI/CD 文件
'.gitlab-ci.yml',
'Jenkinsfile',
'.travis.yml',
]

def check_packer_leaks(target_url, session):
"""检测前端打包器导致的源码/配置泄漏"""
findings = []

for path in PACKER_PATHS:
# 处理通配符
test_urls = expand_path(target_url, path)
for test_url in test_urls:
try:
resp = session.head(test_url, timeout=5)
if resp.status_code == 200:
size = int(resp.headers.get('Content-Length', 0))
findings.append({
'url': test_url,
'size': size,
'severity': 'high' if 'git' in test_url else 'medium',
})
except:
continue

return findings

模块六:SubFinder 子域名爆破

集成 SubFinder 的思路做 DNS 爆破——比纯字典枚举多了一层接口发现:

1
2
3
4
5
def subfinder_enum(domain):
"""调用 SubFinder 做子域名发现"""
cmd = ['subfinder', '-d', domain, '-silent', '-o', '-']
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
return [line.strip() for line in result.stdout.split('\n') if line.strip()]

SubFinder 的优势在于它不只做字典爆破,还会调用多个被动 API(VirusTotal、SecurityTrails、Shodan 等)聚合数据。


模块七:SSRF 深度探测

传统 SSRF 检测只测 http://127.0.0.1,但这个模块做得更深:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
SSRF_PAYLOADS = [
# 内网探测
'http://127.0.0.1:80/',
'http://localhost:80/',
'http://0.0.0.0:80/',
'http://[::1]:80/',
# 云元数据(AWS/阿里云/腾讯云)
'http://169.254.169.254/latest/meta-data/',
'http://100.100.100.200/latest/meta-data/', # 阿里云
'http://metadata.tencentyun.com/latest/meta-data/', # 腾讯云
# DNS Rebinding
'http://1.1.1.1@127.0.0.1/',
# 协议探测
'file:///etc/passwd',
'gopher://127.0.0.1:6379/_*1%0d%0a$8%0d%0aflushall%0d%0a', # Redis
'dict://127.0.0.1:6379/info', # Redis
]

实战案例:若依框架完整攻击链

dirsearchplus 扫描一个若依站点时,会自动触发连锁检测:

1
2
3
4
5
6
1. 目录扫描 → 发现 /dev-api/、/profile/avatar
2. EHole 指纹 → 识别为 RuoYi
3. 加载 ruoyi-endpoints.txt 专用字典
4. 探测 /prod-api/swagger-ui.html → Swagger 未授权
5. 探测 /dev-api/actuator/env → 数据库密码泄露
6. JS 信息收集 → 从 app.js 中提取完整 API 路由表

一条链走到底,不需要手动切换工具。


开发中踩的坑

1. Selenium + ChromeDriver 内存泄漏

某些检测(JS 渲染页面)依赖 Selenium。每次用完后必须显式 driver.quit(),否则 ChromeDriver 进程会堆积:

1
2
3
4
5
6
7
8
9
def render_page(url):
driver = None
try:
driver = webdriver.Chrome()
driver.get(url)
return driver.page_source
finally:
if driver:
driver.quit() # 关键!

2. 字典文件编码

Spring Boot 端点字典包含 /actuator/ 路径,但如果字典文件编码不对,URL 拼接会出问题。统一用 UTF-8:

1
2
with open(dict_file, 'r', encoding='utf-8') as f:
paths = [line.strip() for line in f if line.strip()]

总结

dirsearchplus 的核心价值不是”又一个目录扫描器”,而是把发现 → 识别 → 验证三条线串成自动化的攻击链。扫到 Spring Boot 自动查 actuator,识别出若依自动换专用字典,发现 Swagger 自动解析 API 结构。

评论
分享