W4terCTF 2026 出题记录 — Misc 心理念写
milkory

是的,我又来 W4terCTF 出题了!今年除了 Pwn 题以外,我还和 APJifengc 准备了一道 Minecraft 相关的 Misc 题「心理念写」。

题目内容

Misc — 心理念写(Hard / 6 Solves / 823 pts)

我这是在做 MisC!才、才不是在玩什么 Minecraft 呢……

题目分发了一个压缩包 thoughtography.zip,包含一个简单的 Minecraft Paper 服务器 Docker 镜像,其中需要注意的是 plugins/Thoughtography-2026.jar。这个插件没有提供源码,但是只有一个类,用市面上的反编译插件可以轻松读懂内容。

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
package team.w4terdr0p.thoughtography;

import org.bukkit.*;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.bukkit.event.server.ServerLoadEvent;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;

import java.util.Arrays;
import java.util.Objects;

public class Main extends JavaPlugin implements Listener {
private String flag = "flag{##ERROR##}";
private int bp = 50;

@Override public void onEnable() {
flag = getConfig().getString("flag");
Bukkit.getPluginManager().registerEvents(this, this);
}

@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void win(PlayerTeleportEvent event) {
if (event.getCause() == PlayerTeleportEvent.TeleportCause.UNKNOWN &&
event.getFrom().toVector().distanceSquared(event.getTo().toVector()) > 100.0) {
event.getPlayer().sendMessage("Congrats! Your flag: " + flag);
getLogger().warning(event.getPlayer().getName() + " wins flag ;)");
}
}

@EventHandler
public void handle(ServerLoadEvent event) {
var world = Objects.requireNonNull(Bukkit.getWorld("world"));
world.setGameRule(GameRule.DO_DAYLIGHT_CYCLE, false);
world.setGameRule(GameRule.DO_WEATHER_CYCLE, false);
world.setGameRule(GameRule.COMMAND_BLOCKS_ENABLED, false);
world.setGameRule(GameRule.SPECTATORS_GENERATE_CHUNKS, false);
world.setGameRule(GameRule.SPAWN_RADIUS, 0);
world.setTime(6000);
world.setSpawnLocation(0, 1, 0);
for (int i = -3; i <= 3; ++i) {
for (int j = -3; j <= 3; ++j) {
world.getBlockAt(i, 0, j).setType(Material.OBSIDIAN);
}
}
}

private static final String[] BLACKLIST = {"gamemode", "op"};

@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (bp >= 50 && sender instanceof Player player) {
var actual = String.format("minecraft:execute as %s at @s run %s", player.getName(), String.join(" ", args));
if (Arrays.stream(BLACKLIST).anyMatch(actual::contains)) {
sender.sendMessage("You feel bad to perform that.");
} else {
bp -= 50;
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), actual);
sender.sendMessage("Thoughtography is being performed.");
}
} else {
sender.sendMessage("You don't have enough brainpower.");
}
return true;
}
}

分析代码可以知道:

  • 插件在启动时从配置中读取 flag
  • 在玩家触发 PlayerTeleportEvent 事件,传送原因为 UNKNOWN,且传送距离大于 10 格时可以得到 Flag。
  • 提供了一次任意命令执行机会,只禁用了 gamemodeop 两个命令。
  • 世界初始化禁用了昼夜和天气循环,禁用了命令方块,生成了初始平台,基本没有其他限制。

另外,还有一些 server.properties 中值得注意的设定。

1
2
3
4
difficulty=easy
enable-command-block=false
generator-settings={"layers"\:[],"biome"\:"minecraft\:the_void","structures"\:false}
max-players=1

image

出生点

代码审计

题目的目标十分明确。UNKNOWN 实际上是默认的传送原因,找到 Paper 中没有正确处理的传送链,只要找到一条符合传送要求的链,再编写实现该传送的命令即可得到 Flag。

在出题过程中,我们找到了三条可能触发对应传送要求的链,我们给它们分别取一个名字。

  • 耕地链. 玩家在耕地上跳跃时,有机会将耕地转化为泥土。由于泥土的方块高度比耕地高一些,服务器会把玩家向上传送一小段距离,这个传送的原因为 UNKNOWN
  • 骑乘链. 玩家在骑乘在一个实体上。当那个实体传送时,玩家会被带着传送,对玩家的这次传送原因为 UNKNOWN
  • 末地链. 玩家通过末地传送门从末地回到主世界时,如果没有触发终末之诗,这次传送的原因为 UNKNOWN

其中,耕地链由于传送距离较短,无法满足传送距离大于 10 的要求,而剩下两条链通过合适的命令都可以实现。

挑战

由于 Minecraft 服务器消耗的资源较多,本题最终采用了预约挑战的形式,选手可以通过预约客户端在网站上预约。

image

预约客户端

在放出的 78 个可预约时间段中,共有 23 个被预约,其中 6 支队伍成功攻破本题!真的非常感谢大家。

骑乘链

在解出的队伍中,有 3 个队伍使用了骑乘链。

我们出题时使用的命令如下。核心思路是生成一个船和末影珍珠,并把末影珍珠的 Owner 设置为船。然后在末影珍珠掉下来之前,乘船逃跑,实现 10 格以上的传送。

1
/use execute summon acacia_boat positioned ~ ~99 ~ summon ender_pearl run data modify entity @s Owner set from entity @e[type=acacia_boat,limit=1] UUID

image

骑乘链

一血队伍采用的是类似的命令,但是多生成了一个潜影贝用来接末影珍珠,这样就不用乘船逃跑了。

1
/use execute summon oak_boat run execute positioned ~ ~15 ~ summon shulker run execute positioned ~ ~200 ~ summon ender_pearl run data modify entity @s Owner set from entity @e[type=oak_boat,sort=nearest,limit=1] UUID

二血队伍没有使用 execute,只使用了一条 summon,配合 Passengers 也实现了类似的利用思路。

1
/use summon minecraft:area_effect_cloud ~ ~2.5 ~ {Duration:0,Age:0,WaitTime:0,Passengers:[{id:"minecraft:oak_boat",NoGravity:1b,UUID:[I;9,9,9,9]},{id:"minecraft:ender_pearl",Motion:[0.0d,3d,0.0d],Owner:[I;9,9,9,9]}]}

六血队伍使用的命令也类似,在此不再赘述。

末地链

相较于骑乘链,末地链的利用则更为有趣一些。根据前面的分析,只要从末地返回主世界两次,就可以实现要求。而要从末地无限制返回主世界,最容易想到的方法就是打龙了!

我们预期的一个简单解法如下。通过三个带有末地传送门的矿车,在主世界打掉第一个,把剩下两个放进末地。由于玩家传送到末地出生点平台时,平台上的方块会被清空,包括我们放的末地传送门。所以我们需要先在末地放第二个传送门,穿过回到主世界,然后回到末地放下第三个传送门,再次穿过完成利用。

1
/use execute summon armor_stand summon armor_stand summon armor_stand at @e[type=armor_stand] run summon minecart ~1 ~1 ~1 {Passengers:[{id:"falling_block",BlockState:{Name:"end_portal"},Time:-20000}]}

三血队伍采用了类似的思路,不过少了一个末地传送门。要实现利用则需要 roll 一个较好的末地出生点,开船把第二个传送门送离出生点,这样就不会被清空。

1
/use summon minecraft:oak_boat 0 9 1 {Passengers:[{id:"oak_boat",Passengers:[{id:"falling_block",BlockState:{Name:"end_portal"}}]},{id:"falling_block",BlockState:{Name:"end_portal"}}]}

image

运气不错

四血队伍选择打龙(需要 roll 一个出生点非封闭的种子),通过箱子矿车给自己准备了十分强大的弓箭,以及末影珍珠用于移动,成功拿下末影龙,并走两次末地主岛传送门得到 Flag。这个队伍原本还打算用末影水晶把龙蛋炸下来,不过貌似是因为冒险模式没能成功放下末影水晶。

1
/use summon chest_minecart ~ ~1 ~ {Items:[{Slot:0,id:bow,components:{"enchantments":{power:255}}},{Slot:1,id:ender_pearl,count:16},{Slot:2,id:arrow,count:64},{Slot:3,id:end_crystal,count:64}],Passengers:[{id:falling_block,BlockState:{Name:end_portal}}]}

image

打龙高手

五血队伍也选择了打龙,不过他们的命令……

1
/use setblock ~ ~ ~ minecraft:end_portal

没错,这一队选择了开挂打龙。

image

背背背背起了行囊 离开家的那一刻

大哥不喜欢末影珍珠,直接飞怎么了?大哥不喜欢开挂刷神器,直接上手打怎么了?裁判进服时被大哥 KA 狂殴,忘关就是开了?实际上开挂也在我们的预期里,不过实际看到还是十分难绷。

另外,我们还考虑了一些可能的方案,不过比较好笑。比如,生成三个末影人,然后……慢慢等他们放下手中的末地传送门。需要 roll 一个末地平台全封闭的种子,防止末影龙在等待过程中来打扰。(不过这个方案并没有成功实践过,等得太累了。)

1
/use execute summon enderman summon enderman summon enderman as @e[type=enderman] run data merge entity @s {carriedBlockState:{Name:"end_portal"}}

后记

由于题目太有节目效果(特别是末地链),我原本还打算剪一个视频发布,但是因为太蠢了没有装 Replay Mod,很多通过时刻也没有开 OBS 录制!为数不多录制下来的片段实际看的时候发现画质十分差,检查 OBS 设置发现我还用着国赛画质呢??那还说啥了,不录了!

image

byd 国赛导致的电竞帧率

和我出的其他大多数题目类似,「心理念写」也是一个超能力,作用是预知未来,并以摄像机等媒介成像。本题使用一条命令后,还需要做许多别的工作,对应了 Flag 中的 snap the unknown future in one shot,也就是用一条命令为之后要做的操作铺垫,令人忍俊不禁。(大概如此吧,我扯不动了。)

我自认为本题既有传统的审计,又有新颖的体验,应该能让大家玩的开心吧!不过就在这五个月间,听说有另一场新手赛出了许多 Minecraft OSINT,最后呈现到本场比赛的效果是许多人看到 Minecraft 就吓哭了。看到有些队伍赛后后悔没看过题目内容,还是有些遗憾的。

不论如何,明年如果有机会我还会出有趣的 Misc 题的,下次再见!