- 343
- IGN
- videogamesm12
Alternative title: "The exploit that proved plugins are ineffective as anti-exploit measures"
What was it?
Invalid color was a family of exploits which took advantage of an oversight in how Spigot handled custom colors in items like potions, leather armor, banners, and maps. To presumably allow for changes made by plugins using the Bukkit API to take effect, Spigot injects an extra bit of code into parts of the game to synchronize the data between the core Minecraft item and the Bukkit API itself, which requires certain item data such as potion data to be encoded into something the Bukkit API can understand. MD_5 made a mistake when adding support for custom colored objects like potions where if the value given was outside what is normally possible to generate an RGB value, the server throws an exception which results in it failing to encode packets, so as a failsafe it kicks whoever the packet was for. The bug was introduced in Spigot back in November 2016 but took a little less than 2 years to be exploited. It was patched in December 2019 by MD_5. Paper backported the fix to previous versions of the server software in early January 2020.The exploit first surfaced on the server in August 2018, but it wouldn't be utilized until the next year when Panther began using it heavily as a chunkban exploit while attacking the server. This started a game of cat and mouse as a patch was quickly conceived by means of VulnerabilityPatcher, but the problem is that using plugins to detect exploits is not an infallible way of preventing exploitation. As such, Panther found different ways of smuggling the exploit past the filters including jukeboxes. He'd place an item containing the exploit on the ground and then break the block, dropping the item and kicking everyone who was near it at the time. It was a considerable nuisance at the time. If memory serves, we eventually decided to fork Paper and fix the exploit there, but this has since been lost to the sands of time.
This technical explanation was written with a very basic knowledge of how Spigot worked under the hood back then. Some information may not be entirely accurate as I'm making educated guesses because some parts of the code were really difficult to grasp as for what the fuck they actually did or why it does the things it does. There are still some things I don't fully understand (like why it tries to read items in a packet it's trying to encode), so there may be some inaccuracies.
How exactly did it work?
The Spigot server stack is split up into two components: Bukkit and CraftBukkit. Bukkit is an API used by plugins to interact with a Minecraft server without having to worry about breaking changes or directly relying on raw Minecraft code. It cannot be used standalone, as it requires a server mod that implements the API, which is where CraftBukkit kicks in. CraftBukkit is a modification of the vanilla server software which implements the Bukkit API, and is effectively the backbone for most servers today. Most plugin developers do not interact with CraftBukkit's code directly as there is usually no reason to unless they need to interact with raw Minecraft code. Instead, they interact with the Bukkit API which acts as an intermediary of sorts.In Minecraft, RGB colors for things like potion colors work by storing RGB values in a single three-byte number and then reading it byte by byte to extract the colors individually. Combined color numbers can be as high as 16,777,215 (equivalent to #FFFFFF in hex), but only as low as 0 (equivalent to #000000 in hex). The game doesn't seem to care that much about any numbers that are outside these boundaries (at least, nothing interesting happened when I tried setting it to anything crazy), but Bukkit's implementation is a completely different story.
Java:
private static final int BIT_MASK = 0xff;
/**
* Creates a new Color object from a red, green, and blue
*
* @param red integer from 0-255
* @param green integer from 0-255
* @param blue integer from 0-255
* @return a new Color object for the red, green, blue
* @throws IllegalArgumentException if any value is strictly {@literal >255 or <0}
*/
@NotNull
public static Color fromRGB(int red, int green, int blue) throws IllegalArgumentException {
return new Color(red, green, blue);
}
/**
* Creates a new color object from an integer that contains the red,
* green, and blue bytes in the lowest order 24 bits.
*
* @param rgb the integer storing the red, green, and blue values
* @return a new color object for specified values
* @throws IllegalArgumentException if any data is in the highest order 8
* bits
*/
@NotNull
public static Color fromRGB(int rgb) throws IllegalArgumentException {
Validate.isTrue((rgb >> 24) == 0, "Extrenuous data in: ", rgb);
return fromRGB(rgb >> 16 & BIT_MASK, rgb >> 8 & BIT_MASK, rgb >> 0 & BIT_MASK);
}
If you're having trouble reading this, don't worry, you're not alone. I spent a good chunk of time trying to read this before it finally clicked. Instead of using constructors, colors in Bukkit are created by using special functions offered by the API which create instances for you. When you call the
fromRGB
function that accepts a single number, it first checks to if any data is leftover when it tries to shift the number past three bytes. If not, it extracts each color byte individually and passes them as arguments to the fromRGB
function accepting three separate numbers for red, green, and blue respectively, which calls the relevant constructor. If there is extra data, it bombs out with an IllegalArgumentException complaining about it. There are additional checks in place to make sure that each color number can't somehow exceed 255, but that's not relevant to the exploit, so we're going to move on.Bukkit's API stores item data in an object called ItemMeta. The base ItemMeta stores basic information that anything can have like display names, lore, and attributes. For data specific to certain items, the API offers variants which extend ItemMeta's functionality. When CraftBukkit implements these, they read the data from the item's NBT and set the values accordingly. If they encounter data that has a Bukkit API object for it which (like RGB colors), CraftBukkit will use those and create them using the NBT data, and this is where things can get messy. Let's take a look at how it loads potion metadata.
Java:
class CraftMetaPotion extends CraftMetaItem implements PotionMeta {
static final ItemMetaKey AMPLIFIER = new ItemMetaKey("Amplifier", "amplifier");
static final ItemMetaKey AMBIENT = new ItemMetaKey("Ambient", "ambient");
static final ItemMetaKey DURATION = new ItemMetaKey("Duration", "duration");
static final ItemMetaKey SHOW_PARTICLES = new ItemMetaKey("ShowParticles", "has-particles");
static final ItemMetaKey SHOW_ICON = new ItemMetaKey("ShowIcon", "has-icon");
static final ItemMetaKey POTION_EFFECTS = new ItemMetaKey("CustomPotionEffects", "custom-effects");
static final ItemMetaKey POTION_COLOR = new ItemMetaKey("CustomPotionColor", "custom-color");
static final ItemMetaKey ID = new ItemMetaKey("Id", "potion-id");
static final ItemMetaKey DEFAULT_POTION = new ItemMetaKey("Potion", "potion-type");
// Having an initial "state" in ItemMeta seems bit dirty but the UNCRAFTABLE potion type
// is treated as the empty form of the meta because it represents an empty potion with no effect
private PotionData type = new PotionData(PotionType.UNCRAFTABLE, false, false);
private List<PotionEffect> customEffects;
private Color color;
/* -- */
CraftMetaPotion(NBTTagCompound tag) {
super(tag);
if (tag.hasKey(DEFAULT_POTION.NBT)) {
type = CraftPotionUtil.toBukkit(tag.getString(DEFAULT_POTION.NBT));
}
if (tag.hasKey(POTION_COLOR.NBT)) {
color = Color.fromRGB(tag.getInt(POTION_COLOR.NBT));
}
if (tag.hasKey(POTION_EFFECTS.NBT)) {
NBTTagList list = tag.getList(POTION_EFFECTS.NBT, CraftMagicNumbers.NBT.TAG_COMPOUND);
int length = list.size();
customEffects = new ArrayList<PotionEffect>(length);
for (int i = 0; i < length; i++) {
NBTTagCompound effect = list.getCompound(i);
PotionEffectType type = PotionEffectType.getById(effect.getByte(ID.NBT));
// SPIGOT-4047: Vanilla just disregards these
if (type == null) {
continue;
}
int amp = effect.getByte(AMPLIFIER.NBT);
int duration = effect.getInt(DURATION.NBT);
boolean ambient = effect.getBoolean(AMBIENT.NBT);
boolean particles = tag.hasKeyOfType(SHOW_PARTICLES.NBT, CraftMagicNumbers.NBT.TAG_BYTE) ? effect.getBoolean(SHOW_PARTICLES.NBT) : true;
boolean icon = tag.hasKeyOfType(SHOW_ICON.NBT, CraftMagicNumbers.NBT.TAG_BYTE) ? effect.getBoolean(SHOW_ICON.NBT) : particles;
customEffects.add(new PotionEffect(type, duration, amp, ambient, particles, icon));
}
}
}
}
The first thing it does is call the constructor it inherited from CraftMetaItem, which loads data common to all items first (e.g. name, lore, unbreakability, enchantments, etc). Then, it determines whether the potion was derived from a built-in potion, such as one you could get in the vanilla creative inventory screen. Next, it tries to read the potion color as an integer if such an entry is present and creates a Bukkit color from the resulting number. But wait, we've established earlier that attempting to create a Bukkit color using the
fromRGB
function has a chance of throwing an error if the value isn't right. Since the server creates ItemMeta instances for the item it's trying to encode when sending items to players' clients, what happens when the server tries to create metadata for an item with a negative or otherwise outrageous number set in its NBT?Well, CraftBukkit goes through the entire process of creating regular metadata before reaching the block of code related to the potion color. It detects that an entry is present in the NBT for the custom item data and proceeds to call the
fromRGB
function using the NBT value it was given. The function finds extra data where it shouldn't be and-IllegalArgumentException, followed by the wacky number. This doesn't get caught anywhere, so if it happens while the server is trying to encode the data to send it to players, it fails to encode corrctly and as a failsafe, bombs out and kicks the player from the server with the error message. The server also dumps the error into the logs with a full stacktrace, presumably for easier debugging. Hence, the exploit.
Code:
[21:59:53 WARN]: java.lang.IllegalArgumentException: Extrenuous data in: -1
[21:59:53 WARN]: at org.apache.commons.lang.Validate.isTrue(Validate.java:93)
[21:59:53 WARN]: at org.bukkit.Color.fromRGB(Color.java:147)
[21:59:53 WARN]: at org.bukkit.craftbukkit.v1_14_R1.inventory.CraftMetaPotion.<init>(CraftMetaPotion.java:61)
[21:59:53 WARN]: at org.bukkit.craftbukkit.v1_14_R1.inventory.CraftItemStack.getItemMeta(CraftItemStack.java:309)
[21:59:53 WARN]: at net.minecraft.server.v1_14_R1.PacketDataSerializer.a(PacketDataSerializer.java:255)
[21:59:53 WARN]: at net.minecraft.server.v1_14_R1.PacketPlayOutWindowItems.b(PacketPlayOutWindowItems.java:55)
[21:59:53 WARN]: at net.minecraft.server.v1_14_R1.PacketEncoder.encode(PacketEncoder.java:42)
[21:59:53 WARN]: at net.minecraft.server.v1_14_R1.PacketEncoder.encode(PacketEncoder.java:12)
[21:59:53 WARN]: at io.netty.handler.codec.MessageToByteEncoder.write(MessageToByteEncoder.java:107)
[21:59:53 WARN]: at io.netty.channel.AbstractChannelHandlerContext.invokeWrite0(AbstractChannelHandlerContext.java:738)
[21:59:53 WARN]: at io.netty.channel.AbstractChannelHandlerContext.invokeWriteAndFlush(AbstractChannelHandlerContext.java:801)
[21:59:53 WARN]: at io.netty.channel.AbstractChannelHandlerContext.write(AbstractChannelHandlerContext.java:814)
[21:59:53 WARN]: at io.netty.channel.AbstractChannelHandlerContext.writeAndFlush(AbstractChannelHandlerContext.java:794)
[21:59:53 WARN]: at io.netty.channel.AbstractChannelHandlerContext.writeAndFlush(AbstractChannelHandlerContext.java:831)
[21:59:53 WARN]: at io.netty.channel.DefaultChannelPipeline.writeAndFlush(DefaultChannelPipeline.java:1071)
[21:59:53 WARN]: at io.netty.channel.AbstractChannel.writeAndFlush(AbstractChannel.java:300)
[21:59:53 WARN]: at net.minecraft.server.v1_14_R1.NetworkManager.lambda$b$4(NetworkManager.java:205)
[21:59:53 WARN]: at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java:163)
[21:59:53 WARN]: at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:404)
[21:59:53 WARN]: at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:465)
[21:59:53 WARN]: at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:884)
[21:59:53 WARN]: at java.lang.Thread.run(Thread.java:750)
This issue isn't just limited to potion colors, as the same problem is also present in the code for banners, filled maps, and (apparently) fire charges.
How do you resolve this?
Since this was due to a server-side bug and not a flaw with Minecraft itself, it was patched in December 2019 and packported in January 2020. The solution is to just update Paper or Spigot for the version you're using. Problem solved.How did we handle it?
It's time for another history lesson, because this exploit was a pain in the ass to deal with back in the day. The exploit started a cat-and-mouse game between Panther and the staff team of the server at the time, as every time we patched the exploit he would just find a new way to workaround the patch or straight up bypass it altogether and use that instead. A lot of features in the TotalFreedomMod such as AutoTP and AutoClear were introduced as a direct result of exploits like this.With every patch came a bypass for said patch, and eventually Panther began smuggling it in via schematics with innocent sounding names. Whenever one would get deleted, he'd just recreate several more as backups. Presumably to make it easier to upload any exploit he wanted, he had planned to gain administrative access undercover using an alternate account with the name Softieeeee. However, his move to have multiple backup schematics and to recreate the ones that got deleted ultimately screwed him over because he was sloppy with VPNs and alternate accounts.
Upon hearing about the existence of smuggled in schematics, I spent an evening trying to track down every smuggled schematic and trace them back to the user and IP address who saved them. This task was made easier by the fact that 1) I had FTP access at the time so I could easily download every schematic and log from the server on a whim and 2) duplicate schematics had duplicate file hashes, meaning it was just a matter of finding schematic files with the same hashes. While digging through the logs, I was able to link two accounts Panther had used to recreate the schematics and also deploy the exploits to an account named Softieeeee by means of both the IP addresses and name histories of the accounts. I forget who I told exactly, but one to two weeks later Robin told Ivan about the link in the Smartn't group chat. A day later, information about the link went public after Panther's admin application under Softieeeee was denied.
Upon finding out that we were onto him, Panther gave credit where it was due and admitted to being Softieeeee. Eventually, he started using different exploits instead, but my memory of what happened beyond this point becomes incredibly fuzzy. If memory serves, the exploit continued to be used for quite some time afterwards until eventually we forked Paper to fix the exploit. We called it Gayper, because at the time everything was called gay for some reason.
Conclusion
Thanks for reading. This was a particularly complex exploit to document. Even though the core flaw was pretty simple to explain (an uncaught exception causing problems), the history surrounding it and the overall impact it had on the server led to much greater ripple effect throughout the server's history. This exploit is a key example of why plugins suck ass at patching exploits, because they don't patch them, they mitigate them. The root cause of the problem, which took several hours of development time to come up with mitigation tactics for, remained unfixed. So when new bypasses came around, the exploit would still work. This is why when Scissors hit the scene, exploit patches became a lot better and harder to circumvent. Years-old exploits like death potions were literally rendered obsolete overnight.I must ask again: what exploit should I document next? Let me know.