| 项目 | 内容 |
|---|---|
| 分类 / 难度 | Misc / DFIR / Medium |
| 分值 | 250 |
| 类型 | 多源日志取证(时间线归一化 + 身份归因) |
| 输入 | evidence.tar.gz(8 类日志 + 导出元数据) |
| 目标 | 还原「谁在非工作时段用服务账号导出了受限数据」,并用五元指纹解封证明字段 |
1. 案情
工作日 20:00–23:00(UTC)本该无人操作。但这段时间里,一个服务账号做了它不该做的事:交互式登录、拨 VPN、然后导出 finance/quarterly-ledger。
题目给的证据是一包日志,重点是每份日志的时钟都不一样:
{"path": "logs/idp-auth.jsonl", "clock": "UTC"},
{"path": "logs/vpn.csv", "clock": "America/New_York UTC-04:00"},
{"path": "logs/api-gateway.jsonl","clock": "UTC, approximately 800ms fast"},
{"path": "logs/app-audit.jsonl", "clock": "Unix epoch milliseconds UTC"},
{"path": "logs/cloud-audit.jsonl","clock": "UTC"},
{"path": "logs/edr-process.jsonl","clock": "America/Los_Angeles UTC-07:00, 18 seconds slow"},
{"path": "logs/dns.log", "clock": "UTC"},
{"path": "logs/egress.csv", "clock": "UTC"}
这是这道题真正的考点:先校正时间,再拼时间线。若不归一化,VPN 的 18:17(EDT)会被误读成 18:17 UTC,整整差 4 小时,导致"是不是工作时间"的判断直接反掉。
2. 第一步:找出被滥用的身份
logs/idp-auth.jsonl 里搜交互式登录:
jq -c 'select(.event=="auth.login.success" and .interactive==true)' \
logs/idp-auth.jsonl
{"event_id":"A-2201","event":"auth.login.success","interactive":true,
"user":"svc-build","source_ip":"198.51.100.77","session_id":"idp-88301",
"ts":"2026-10-07T20:16:42Z", ...}
关键判断:svc-build 是服务账号。服务账号走的是 client credentials / 机器证书,永远不该出现 interactive: true。这种"服务账号做人机交互登录",本身就是最强的异常信号。
于是锁定:
- 被滥用身份 =
svc-build - 首个公共来源 IP =
198.51.100.77(198.51.100.0/24是 RFC 5737 文档保留段,在这里代表外网来源)
同一份日志里还有 IDP-ALERT-002 / IDP-ALERT-004 两条告警,确认了"非工作时段 + 服务账号 + 交互式"三重合。
3. 第二步:VPN 会话与隧道地址(注意时区)
logs/vpn.csv 用的是 America/New_York(UTC-04:00,夏令时):
session_id,user,connect_time,disconnect_time,tunnel_ip,client_ip
vpn-9921,svc-build,2026-10-07 18:17:05,2026-10-07 21:40:12,10.8.9.44,198.51.100.77
vpn-9922,deploy,2026-10-07 09:02:11,2026-10-07 10:15:33,10.8.9.91,203.0.113.24
vpn-9923,qa-bot,2026-10-07 11:41:20,2026-10-07 12:02:44,10.8.9.137,203.0.113.88
换算:
2026-10-07 18:17:05 EDT -> 2026-10-07 22:17:05 UTC
对应 IdP 那次登录(20:16:42Z)后 1 分钟 —— 时间线严丝合缝。
于是:
- VPN 会话 ID =
vpn-9921 - 隧道地址 =
10.8.9.44
对照 vpn-9922(deploy,09:02 EDT = 13:02 UTC)与 vpn-9923(qa-bot,11:41 EDT = 15:41 UTC),这两个都是正常工作时段,属于基线,不是目标。
4. 第三步:找出真正的导出事件
logs/app-audit.jsonl 用的是 epoch 毫秒,要先转:
jq -c 'select(.action=="export.create")' logs/app-audit.jsonl
{"ts":1791416705123,"action":"export.create","actor":"svc-build",
"export_id":"EXP-771","dataset":"finance/quarterly-ledger",
"classification":"restricted","session":"vpn-9921"}
1791416705123 ms = 2026-10-07T22:25:05Z。
这几个 blob 内容相近,得逐个排除才能确定哪个是真正被用过的那份:
| 导出 ID | 创建者 | 分类 | 判读 |
|---|---|---|---|
| EXP-766 | deploy |
internal | 例行每周发布报告(WEEKLY-REPORT-OK),会话 vpn-9922,工作时段 → 正常 |
| EXP-771 | svc-build |
restricted | 非工作时段 + 服务账号 + finance/quarterly-ledger → 目标 |
| EXP-772 | qa-bot |
test | CHG-4418 批准的 QA 夹具(QA-FIXTURE-001) → 正常 |
| EXP-770 | report-worker |
internal | 计划内的仪表盘快照 → 正常 |
记住判断规则:看的是(身份 × 时段 × 分类 × 会话)的交集,不是单独看"是不是导出"。四个导出里只有一个四维全红。
5. 第四步:拼五元指纹
题目的证明字段是这样密封的:
seal = base64( 明文 XOR repeat( sha256( 指纹utf8 ) ) )
指纹是五个字段用 | 连接:
指纹 = 被滥用身份 | 首个公共来源IP | VPN会话ID | 隧道地址 | 导出ID
= svc-build|198.51.100.77|vpn-9921|10.8.9.44|EXP-771
顺序不是随便定的,它是本案的因果链顺序:
谁(svc-build) → 从哪来(198.51.100.77) → 怎么进来(vpn-9921) → 拿到什么地址(10.8.9.44) → 干了什么(EXP-771)
6. 解封
import base64, hashlib, json
FINGERPRINT = "svc-build|198.51.100.77|vpn-9921|10.8.9.44|EXP-771"
def unseal(fingerprint: str, seal_b64: str) -> str:
raw = base64.b64decode(seal_b64)
ks = hashlib.sha256(fingerprint.encode()).digest()
return bytes(b ^ ks[i % len(ks)] for i, b in enumerate(raw)).decode()
meta = json.load(open("exports/EXP-771.metadata.json"))
print(unseal(FINGERPRINT, meta["payload_b64"]))
flag{local_verify_dfir01}
要点:密钥流 sha256(指纹) 只有 32 字节,长度不够时要循环复用(i % 32)。密文长度由明文决定,短于 32 字节时看不出循环,长了就必须循环,否则抛 IndexError。
7. 完整求解脚本
#!/usr/bin/env python3
import base64, hashlib, json, os, datetime
root = "."
def unseal(fp, seal_b64):
raw = base64.b64decode(seal_b64)
ks = hashlib.sha256(fp.encode()).digest()
return bytes(b ^ ks[i % 32] for i, b in enumerate(raw)).decode("utf-8", "replace")
# --- 1. 服务账号交互式登录 ---
sus_user = sus_ip = None
for line in open(os.path.join(root, "logs/idp-auth.jsonl")):
e = json.loads(line)
if e.get("event") == "auth.login.success" and e.get("interactive"):
if "svc" in e.get("user", ""):
sus_user, sus_ip = e["user"], e["source_ip"]
print("[1] 被滥用身份 =", sus_user, " 来源 =", sus_ip)
# --- 2. VPN:EDT -> UTC(+4h) ---
vpn_id = tunnel = None
for line in open(os.path.join(root, "logs/vpn.csv")).read().splitlines()[1:]:
sid, user, ct, dt, tip, cip = line.split(",")
if user == sus_user:
vpn_id, tunnel = sid, tip
utc = datetime.datetime.strptime(ct, "%Y-%m-%d %H:%M:%S") + datetime.timedelta(hours=4)
print("[2] VPN", sid, ct, "EDT ->", utc, "UTC tunnel =", tip)
# --- 3. 找到 restricted 导出 ---
export_id = None
for line in open(os.path.join(root, "logs/app-audit.jsonl")):
e = json.loads(line)
if e.get("action") == "export.create" and e.get("classification") == "restricted":
export_id = e["export_id"]
print("[3] 导出 =", export_id)
# --- 4. 五元指纹 + 解封 ---
fp = "|".join([sus_user, sus_ip, vpn_id, tunnel, export_id])
print("[4] 指纹 =", fp)
meta = json.load(open(os.path.join(root, f"exports/{export_id}.metadata.json")))
print("[5] FLAG =", unseal(fp, meta["payload_b64"]))
输出:
[1] 被滥用身份 = svc-build 来源 = 198.51.100.77
[2] VPN vpn-9921 2026-10-07 18:17:05 EDT -> 2026-10-07 22:17:05 UTC tunnel = 10.8.9.44
[3] 导出 = EXP-771
[4] 指纹 = svc-build|198.51.100.77|vpn-9921|10.8.9.44|EXP-771
[5] FLAG = flag{local_verify_dfir01}
8. 踩坑提示
| 坑 | 说明 |
|---|---|
| 时区不归一化 | vpn.csv 是 EDT。18:17 EDT = 22:17 UTC,不是 18:17 UTC。差 4 小时会让"非工作时段"的判断失效 |
api-gateway.jsonl 快 800ms / edr-process.jsonl 慢 18s |
这两份日志在本题里不参与指纹构造,但做时间线对账时必须带上偏差,否则跨源事件会被判成"倒序" |
app-audit.jsonl 是 epoch 毫秒 |
直接当秒读会得到 1970 年,务必 /1000 |
| export 有四个,只有一个是目标 | 看 classification == "restricted",再看 created_by 是否为服务账号,别只看时间 |
| 指纹顺序 | 不是按字母、不是按时间,是 `身份 |
| 密钥流循环 | sha256 只有 32 字节,明文更长时必须 i % 32 |
9. 从防守角度看
这类事件在真实环境里的信号链非常典型,值得写进检测规则:
- 服务账号出现
interactive=true登录 —— 服务账号应只允许client_credentials,出现交互式登录直接告警; - 非工作时段 VPN 接入 + 服务账号 —— 二维组合,单看任一维度都容易被噪音淹没;
restricted分类的数据被服务账号导出 —— 服务账号的导出白名单应该按dataset收紧;- 同一凭证短时间内跨层动作(IdP → VPN → API → 导出),尤其是带外网源 IP 的。
把这四条串起来,本案在 22:17 就能拦下,而不是等到事后取证。
10. 小结
这道题不难在技术,难在耐心:8 份日志、4 种时钟、4 个导出、3 个 VPN 会话,其中大部分是基线噪音。真正的信号只有一条,判据是四维交集(身份 × 时段 × 分类 × 会话)。
答案:
被滥用身份 svc-build
来源 IP 198.51.100.77
VPN 会话 vpn-9921
隧道地址 10.8.9.44
导出 ID EXP-771
指纹 svc-build|198.51.100.77|vpn-9921|10.8.9.44|EXP-771
FLAG flag{local_verify_dfir01}