hunt-source-leak

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

HUNT-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:
  • .js.map
    source maps
    — reconstruct full TypeScript/ES6 source code → find hardcoded API keys, internal endpoints, auth logic bypasses
  • swagger.json
    /
    openapi.json
    — complete REST API specification with all endpoints, parameters, auth schemes, and internal route names
  • .env
    /
    .env.production
    — APP_KEY, DB_PASSWORD, API_KEY, SECRET_KEY in plaintext
  • .git/
    exposure
    —
    git clone
    the entire source history → all past hardcoded secrets
  • asset-manifest.json
    /
    _next/static/
    — all JS bundle paths → systematic source map discovery
  • build-info
    /
    info.json
    — git commit hash, build timestamp, dependency versions → CVE targeting

暴露 TypeScript 源码的 source map = 可以查看所有 API 路由、认证逻辑和密钥。Swagger/OpenAPI JSON = 完整的 API 攻击面地图。
最高价值的发现:
  • .js.map
    source maps
    — 重建完整的 TypeScript/ES6 源代码 → 查找硬编码 API 密钥、内部端点和认证逻辑绕过
  • swagger.json
    /
    openapi.json
    — 包含所有端点、参数、认证方案和内部路由名称的完整 REST API 规范
  • .env
    /
    .env.production
    — 明文形式的 APP_KEY、DB_PASSWORD、API_KEY、SECRET_KEY
  • .git/
    暴露
    —
    git clone
    整个源代码历史 → 找出所有过去硬编码的密钥
  • asset-manifest.json
    /
    _next/static/
    — 所有 JS bundle 路径 → 系统化发现 source map
  • build-info
    /
    info.json
    — git commit hash、构建时间戳、依赖版本 → 针对性排查 CVE

Phase 1 — Quick Wins (Run First)

阶段 1 — 快速突破(优先运行)

bash
undefined
bash
undefined

These 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

---
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

---

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. A
.map
URL 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.
bash
# 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
main.<hashA>.js.map
. On re-verification that URL returned a small HTML soft-404 and the finding was nearly closed as fixed. The bundle had rotated to
main.<hashB>.js
— and the map was still published at
main.<hashB>.js.map
, same size. Nothing had been remediated.
Tell the client this explicitly in the report: redeploying does not fix source map exposure. Only
GENERATE_SOURCEMAP=false
(or stripping
.map
at deploy) plus a CDN purge closes it. A team that redeploys and re-checks the old link will wrongly declare victory.
Same rule applies to any content-hashed artifact: chunk files, CSS maps,
asset-manifest.json
, and staging equivalents.
bash
undefined
始终在测试前解析当前构建 hash,并在重新验证前再次解析。 Bundle 文件名采用内容哈希,因此每次部署都会轮换。昨天记录的
.map
URL 今天可能返回 404,而 source map 仍可能以新名称公开。旧 URL 返回 404 并不代表已修复——这意味着构建已发生轮换。
bash
# 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 位于
main.<hashA>.js.map
。重新验证时,该 URL 返回了一个小型 HTML soft-404,该问题差点被当作已修复而关闭。但 bundle 已轮换为
main.<hashB>.js
——而 map 仍发布在
main.<hashB>.js.map
,大小相同。实际上并未采取任何修复措施。
在报告中明确告知客户:重新部署无法修复 source map 暴露问题。 只有
GENERATE_SOURCEMAP=false
(或在部署时移除
.map
)并清除 CDN 缓存才能解决。团队如果重新部署后只复查旧链接,就会错误地宣布问题已解决。
同一规则也适用于任何采用内容哈希的产物:chunk 文件、CSS maps、
asset-manifest.json
及其 staging 等价资源。
bash
undefined

Step 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
undefined
bash
undefined

Common 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
undefined
bash
undefined

Check 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
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

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
undefined
bash
undefined

Build 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
undefined
bash
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
undefined
bash
undefined

Download 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
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

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
curl -s "https://$TARGET/static/js/main.*.js" |
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

---
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

---

Chain Table

关联表

Source leak findingChain toImpact
Source map with API keyUse key directly → API accessHigh/Critical
Source map with auth logicFind auth bypass routeCritical
Swagger → internal endpointsTest undocumented admin routesHigh
.git exposedFull source history → all past secretsCritical
build-info with git hashCVE targeting exact versionHigh
.env with DB_PASSWORDDirect database accessCritical

源代码泄露发现后续利用路径影响
Source map 中含 API key直接使用密钥 → API 访问高/严重
Source map 中含 auth logic查找 auth bypass 路由严重
Swagger → 内部端点测试未公开的管理员路由高
.git 暴露完整源代码历史 → 所有历史密钥严重
build-info 中含 git hash针对准确版本排查 CVE高
.env 中含 DB_PASSWORD直接访问数据库严重

Tools

工具

bash
undefined
bash
undefined

git-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:信息级