| name | performing-api-inventory-and-discovery |
| description | 执行 API 资产清点和发现,以识别组织环境中的所有 API 端点,包括已记录的、 未记录的、影子 API、僵尸 API 和已废弃的 API。测试人员通过被动流量分析、 主动扫描、DNS 枚举、JavaScript 分析和云资源清点来构建全面的 API 目录。 映射至 OWASP API9:2023 不当资产管理。当请求涉及 API 发现、影子 API 检测、 API 清单审计或攻击面测绘时触发。
|
| domain | cybersecurity |
| subdomain | api-security |
| tags | ["api-security","owasp","api-discovery","shadow-api","inventory","attack-surface"] |
| version | 1.0.0 |
| author | mahipal |
| license | Apache-2.0 |
执行 API 资产清点与发现
适用场景
- 在安全评估前测绘组织的完整 API 攻击面
- 识别由开发团队未经安全审查部署的影子 API(Shadow API)
- 发现仍可访问但无人维护的已废弃或僵尸 API 版本
- 发现通过移动应用、SPA 或微服务暴露的未记录 API 端点
- 为合规需求(PCI-DSS、SOC2、GDPR)建立 API 清单
不适用于 未经书面授权的场景。API 发现涉及扫描网络基础设施和分析流量。
前置条件
- 明确授权文件,指定目标域名和网络范围
- 被动流量捕获能力(网络分路器、代理或云流量镜像)
- 主动扫描工具:Amass、subfinder、httpx 和 nuclei
- JavaScript 分析工具:LinkFinder、JS-Miner 或自定义解析器
- 访问云控制台(AWS、Azure、GCP)进行 API 网关清点
- Burp Suite Professional 用于被动 API 端点发现
工作流程
步骤 1:通过流量分析进行被动 API 发现
import re
import json
from collections import defaultdict
def analyze_har_for_apis(har_file_path):
"""从 HTTP 归档(HAR)文件中提取 API 端点。"""
with open(har_file_path) as f:
har = json.load(f)
api_endpoints = defaultdict(lambda: {
"methods": set(), "content_types": set(),
"auth_types": set(), "count": 0
})
for entry in har["log"]["entries"]:
url = entry["request"]["url"]
method = entry["request"]["method"]
api_patterns = [
r'/api/', r'/v\d+/', r'/graphql', r'/rest/',
r'/ws/', r'/rpc/', r'/grpc', r'/json',
]
if any(re.search(p, url) for p in api_patterns):
normalized = re.sub(r'\?.*$', '', url)
normalized = re.sub(r'/\d+(/|$)', '/{id}\\1', normalized)
normalized = re.sub(
,
, normalized)
ep = api_endpoints[normalized]
ep[].add(method)
ep[] +=
header entry[][]:
name = header[].lower()
name == :
header[].lower():
ep[].add()
header[].lower():
ep[].add()
name == :
ep[].add()
content_type = (
(h[] h entry[][]
h[].lower() == ), )
content_type:
ep[].add(content_type.split()[])
()
url, info (api_endpoints.items()):
methods = .join((info[]))
auth = .join(info[])
()
()
api_endpoints
步骤 2:主动 API 端点发现
amass enum -d example.com -o amass_results.txt
subfinder -d example.com -o subfinder_results.txt
grep -iE '(api|rest|graphql|ws|gateway|backend|internal|staging|dev|v1|v2)' \
amass_results.txt subfinder_results.txt | sort -u > api_subdomains.txt
cat api_subdomains.txt | httpx -status-code -content-length -title \
-tech-detect -o live_apis.txt
cat api_subdomains.txt | while read domain; do
for path in /api /api/v1 /api/v2 /graphql /swagger.json /openapi.json \
/api-docs /docs /health /status /metrics /actuator; do
curl -s -o /dev/null -w "%{http_code} %{url_effective}\n" \
"https://${domain}${path}" 2>/dev/null | grep -v "^404"
done
done
import requests
import concurrent.futures
def discover_api_endpoints(base_domains):
"""主动探测已发现域名上的 API 端点。"""
API_PATHS = [
"/api", "/api/v1", "/api/v2", "/api/v3",
"/graphql", "/gql", "/query",
"/rest", "/json", "/rpc",
"/swagger.json", "/swagger/v1/swagger.json",
"/openapi.json", "/openapi.yaml", "/api-docs",
"/docs", "/redoc", "/explorer",
"/.well-known/openid-configuration",
"/health", "/healthz", "/ready",
"/status", "/info", "/version",
"/metrics", "/prometheus",
"/actuator", "/actuator/health", "/actuator/info",
"/admin", "/admin/api", "/internal",
"/debug", "/debug/vars", "/debug/pprof",
"/ws", "/websocket", "/socket.io",
"/grpc", "/twirp",
]
discovered = []
def ():
scheme [, ]:
url =
:
resp = requests.get(url, timeout=, allow_redirects=,
verify=)
resp.status_code (, , ):
{
: url,
: resp.status_code,
: resp.headers.get(, ),
: resp.headers.get(, ),
: (resp.content),
}
requests.exceptions.RequestException:
concurrent.futures.ThreadPoolExecutor(max_workers=) executor:
futures = {}
domain base_domains:
path API_PATHS:
future = executor.submit(check_endpoint, domain, path)
futures[future] = (domain, path)
future concurrent.futures.as_completed(futures):
result = future.result()
result:
discovered.append(result)
()
discovered
步骤 3:从 JavaScript 源码分析 API 端点
import re
import requests
def extract_apis_from_javascript(js_urls):
"""从 JavaScript 源文件中提取 API 端点。"""
api_pattern = re.compile(
r'''(?:['"`])((?:/api/|/v[0-9]+/|/graphql|/rest/)[^'"`\s<>{}]+)(?:['"`])''',
re.IGNORECASE
)
url_pattern = re.compile(
r'''(?:['"`])(https?://[a-zA-Z0-9._-]+(?:\.[a-zA-Z]{2,})+(?:/[^'"`\s<>{}]*)?)(?:['"`])'''
)
fetch_pattern = re.compile(
r'''(?:fetch|axios|ajax|XMLHttpRequest|\.get|\.post|\.put|\.delete|\.patch)\s*\(\s*(?:['"`])([^'"`]+)'''
)
all_endpoints = set()
for js_url in js_urls:
try:
resp = requests.get(js_url, timeout=10)
content = resp.text
for match in api_pattern.findall(content):
all_endpoints.add(("relative", match))
for match in url_pattern.findall(content):
if any(kw in match.lower() for kw in ["/api", "/v1", "/v2", "graphql"]):
all_endpoints.add(("absolute", match))
for match in fetch_pattern.findall(content):
all_endpoints.add((, ))
requests.exceptions.RequestException:
()
source, endpoint (all_endpoints):
()
all_endpoints
():
resp = requests.get(, timeout=)
js_files = re.findall(, resp.text)
full_urls = []
js js_files:
js.startswith():
full_urls.append(js)
js.startswith():
full_urls.append()
js.startswith():
full_urls.append()
full_urls
步骤 4:云 API 网关清点
import boto3
def inventory_aws_apis():
"""清点 AWS API Gateway 中的所有 API。"""
apigw = boto3.client('apigateway')
apigwv2 = boto3.client('apigatewayv2')
apis = []
rest_apis = apigw.get_rest_apis()
for api in rest_apis['items']:
resources = apigw.get_resources(restApiId=api['id'])
stages = apigw.get_stages(restApiId=api['id'])
for stage in stages['item']:
for resource in resources['items']:
for method in resource.get('resourceMethods', {}).keys():
apis.append({
"type": "REST",
"name": api['name'],
"stage": stage['stageName'],
"path": resource['path'],
"method": method,
"url": f"https://{api['id']}.execute-api.{boto3.session.Session().region_name}.amazonaws.com/{stage['stageName']}{resource['path']}",
"created": str(api.get('createdDate', '')),
})
http_apis = apigwv2.get_apis()
api http_apis[]:
routes = apigwv2.get_routes(ApiId=api[])
stages = apigwv2.get_stages(ApiId=api[])
route routes[]:
apis.append({
: ,
: api[],
: route[],
: api[],
: api[],
})
()
api apis:
()
apis
步骤 5:API 版本与影子 API 检测
def detect_shadow_and_zombie_apis(discovered_endpoints, documented_endpoints):
"""将发现的 API 与已记录的清单进行比对。"""
def normalize(ep):
ep = re.sub(r'/v\d+/', '/vX/', ep)
ep = re.sub(r'/\d+', '/{id}', ep)
return ep.lower().rstrip('/')
documented_normalized = {normalize(ep) for ep in documented_endpoints}
shadow_apis = []
zombie_apis = []
for ep in discovered_endpoints:
normalized = normalize(ep["url"])
if normalized not in documented_normalized:
if re.search(r'/v[0-9]+/', ep["url"]):
zombie_apis.append(ep)
else:
shadow_apis.append(ep)
print(f"\n影子 API(未记录):{len(shadow_apis)} 个")
for api in shadow_apis:
print(f" [影子] {api['url']} -> {api['status']}")
print(f"\n僵尸 API(已废弃版本):{len(zombie_apis)} 个")
for api in zombie_apis:
()
api zombie_apis:
resp = requests.get(api[], timeout=)
resp.status_code (, ):
()
shadow_apis, zombie_apis
核心概念
| 术语 | 定义 |
|---|
| 影子 API(Shadow API) | 由开发团队部署,未经过正式 API 管理流程或安全审查的 API |
| 僵尸 API(Zombie API) | 已废弃或旧版本的 API,仍处于运行状态但不再维护或监控 |
| API 清单(API Inventory) | 组织中所有 API 的全面目录,包括端点 URL、所有者、版本、认证方式和数据分类 |
| 资产管理不当(Improper Inventory Management) | OWASP API9:2023 - 未能维护准确的 API 清单,导致 API 端点未受监控和保护 |
| 攻击面(Attack Surface) | 攻击者可能与之交互的所有 API 端点、方法和参数的总集合 |
| API 蔓延(API Sprawl) | 组织内 API 的无节制扩散,通常由微服务采用而缺乏集中治理所导致 |
工具与系统
- Amass:通过 DNS 枚举、网页爬取和 API 发现进行攻击面测绘的 OWASP 工具
- httpx:用于验证已发现域名并识别活跃 API 端点的快速 HTTP 探测工具
- nuclei:基于模板的扫描器,用于检测暴露的 API 文档、调试端点和配置错误的服务
- Swagger UI Detector:用于在组织内查找暴露的 Swagger/OpenAPI 文档端点的工具
- Akto:通过流量分析发现 API 并维护自动化清单的 API 安全平台
常见场景
场景:企业 API 攻击面评估
场景背景:一家大型企业有 200 多个开发团队使用微服务架构。安全团队怀疑有许多未记录的 API 暴露在互联网上,需要为安全审计进行全面的 API 清点。
方法:
- DNS 枚举发现 340 个子域名,其中 45 个包含 API 相关关键词(api、rest、gateway、backend)
- 使用 API 路径词典主动探测所有子域名,发现 127 个活跃 API 端点
- 主 Web 应用的 JavaScript 分析揭示 34 个 API 端点,其中 8 个指向未记录的内部服务
- AWS API Gateway 清点显示跨 12 个账户有 67 个 REST API 和 23 个 HTTP API
- 与官方 API 目录交叉对比:31 个影子 API(未记录),14 个僵尸 API(已废弃版本)
- 3 个僵尸 API 无认证,通过本应下线的端点暴露客户数据
- 2 个影子 API 无需授权即可向互联网暴露内部管理功能
常见陷阱:
- 只检查已记录的 API 端点,遗漏在 API 网关之外部署的影子 API
- 未扫描前端应用程序硬编码 API 端点 URL 的 JavaScript 包
- 遗漏非标准端口或子路径后的 API
- 未检查多个 API 版本,其中旧版本可能缺乏安全控制
- 假设所有 API 都通过 API 网关,而实际上部分 API 可能直接暴露
输出格式
## API 清单与发现报告
**组织**:示例公司
**评估日期**:2024-12-15
**扫描域名**:340 个
### 摘要
| 类别 | 数量 |
|----------|-------|
| 发现的总 API 数 | 127 |
| 已记录的 API | 82 |
| 影子 API(未记录) | 31 |
| 僵尸 API(已废弃) | 14 |
| 无认证的 API | 8 |
| 暴露敏感数据的 API | 5 |
### 关键发现
1. **僵尸 API**:api-v1.example.com/api/v1/users - 2022 年已废弃,
仍可访问,无需认证,返回完整用户数据
2. **影子 API**:internal-tools.example.com/api/admin - 管理功能
无需授权暴露至互联网
3. **暴露的文档**:12 个 Swagger UI 实例可公开访问,
泄露了完整的 API 模式和端点详情