Skip to content

Commit

Permalink
Log4j Disable Literal Pattern Converter
Browse files Browse the repository at this point in the history
  • Loading branch information
cliveverghese committed Dec 23, 2021
1 parent 999092f commit 4d43a6c
Show file tree
Hide file tree
Showing 8 changed files with 320 additions and 3 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ Release Date: TBD
### [#39](https://github.com/corretto/hotpatch-for-apache-log4j2/pull/39) Support for multiple patches
The tool now supports applying multiple patches to the same VM in a single attach. Existing patch has been renamed to `Log4j2_JndiNoLookup` and it is applied by default.

### [#46](https://github.com/corretto/hotpatch-for-apache-log4j2/pull/46) Add patch for CVE-2021-45105
Patch for [CVE-2021-45105](https://nvd.nist.gov/vuln/detail/CVE-2021-45105) is added to the tool. This patches the
LiteralPatternConverter to disable lookups in format messages.

### Ongoing Changes ###
See [all changes](https://github.com/corretto/hotpatch-for-apache-log4j2/compare/1.3.0...main) since the previous version.

Expand Down
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
# Log4jHotPatch

This is a tool which injects a Java agent into a running JVM process. The agent will attempt to patch the `lookup()` method of all loaded `org.apache.logging.log4j.core.lookup.JndiLookup` instances to unconditionally return the string "Patched JndiLookup::lookup()". It is designed to address the [CVE-2021-44228](https://nvd.nist.gov/vuln/detail/CVE-2021-44228/) remote code execution vulnerability in Log4j without restarting the Java process. This tool will also address [CVE-2021-45046](https://nvd.nist.gov/vuln/detail/CVE-2021-45046/).
This is a tool which injects a Java agent into a running JVM process. The agent will attempt to patch the `lookup()`
method of all loaded `org.apache.logging.log4j.core.lookup.JndiLookup` instances to unconditionally return the string
"Patched JndiLookup::lookup()". It is designed to address the
[CVE-2021-44228](https://nvd.nist.gov/vuln/detail/CVE-2021-44228/) remote code execution vulnerability in Log4j without
restarting the Java process. This tool will also address
[CVE-2021-45046](https://nvd.nist.gov/vuln/detail/CVE-2021-45046/).

To Patch for [CVE-2021-45105](https://nvd.nist.gov/vuln/detail/CVE-2021-45105), you can run the tool with the following
parameters `patcherClassName=com.amazon.corretto.hotpatch.patch.impl.set.Log4j2PatchSetWithDisableLookups`. This will
patch the LiteralPatternConverter to disable the lookups in log patterns.

This has been currently only tested with JDK 8, 11, 15 and 17 on Linux!

Expand Down
44 changes: 43 additions & 1 deletion build-tools/bin/run_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,19 @@ function start_static_target() {
popd > /dev/null
}

function start_dos_test() {
if [[ -f /tmp/vuln2.log ]]; then
rm /tmp/vuln2.log
fi
local jdk_dir=$1
local agent_jar=$2

pushd "${ROOT_DIR}/test" > /dev/null
${jdk_dir}/bin/java -cp log4j-core-2.12.1.jar:log4j-api-2.12.1.jar:. -Dlog4j2.configurationFile=${ROOT_DIR}/src/test/resources/log4j-vuln2.properties -javaagent:${agent_jar}=patcherClassName=com.amazon.corretto.hotpatch.patch.impl.set.Log4j2PatchSetWithDisableLookups Vuln2 > /tmp/vuln2.log &

popd > /dev/null
}

function static_agent_configure_verbose() {
if [ "$3" = "unset" ]; then
PROP_VALUE=""
Expand Down Expand Up @@ -107,6 +120,25 @@ function verify_idempotent_agent() {
fi
}

function verify_dos_test() {
if grep -q '${ctx:myvar} - Any string' /tmp/vuln2.log
then
echo "Test passed. JVM did not run into StackOverflow"
else
echo "Test failed. JVM failed."
cat /tmp/vuln2.log
exit 1
fi
if grep -q '${ctx:myvar} - Patched JndiLookup::lookup()' /tmp/vuln2.log
then
echo "Test passed. Patched JndiLookup::lookup"
else
echo "Test failed. Did not patch JndiLookup"
cat /tmp/vuln2.log
exit 1
fi
}

if [[ $# -lt 2 ]]; then
usage
exit 1
Expand Down Expand Up @@ -165,7 +197,7 @@ if [[ "${JVM_MV}" == "17" ]]; then
fi

pushd "${ROOT_DIR}/test" > /dev/null
${JDK_DIR}/bin/javac -cp log4j-core-2.12.1.jar:log4j-api-2.12.1.jar Vuln.java Empty.java
${JDK_DIR}/bin/javac -cp log4j-core-2.12.1.jar:log4j-api-2.12.1.jar Vuln.java Empty.java Vuln2.java
popd > /dev/null

echo
Expand Down Expand Up @@ -353,7 +385,17 @@ if [[ -z "${SKIP_STATIC}" ]]; then
start_target ${JDK_DIR}
VULN_PID=$!


sleep 2

verify_target $VULN_PID
unset _JAVA_OPTIONS
echo
echo "******************"
echo "Running Literal Pattern Converter JDK${JVM_MV} Test"
echo "------------------"

start_dos_test ${JDK_DIR} ${AGENT_JAR}
sleep 2
verify_dos_test
fi
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public class HotPatchAgent {
public static int asmApiVersion() {
return Opcodes.ASM9;
}

/**
* This is the entry point when the agent is loaded during startup. The main difference in this scenario will be that
* we only need to load our transformers, but there is no need to retransform existing classes as they have not been
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file 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.
*/

package com.amazon.corretto.hotpatch.patch.impl.log4j2;

import com.amazon.corretto.hotpatch.org.objectweb.asm.ClassReader;
import com.amazon.corretto.hotpatch.org.objectweb.asm.ClassVisitor;
import com.amazon.corretto.hotpatch.org.objectweb.asm.ClassWriter;
import com.amazon.corretto.hotpatch.org.objectweb.asm.Label;
import com.amazon.corretto.hotpatch.org.objectweb.asm.MethodVisitor;
import com.amazon.corretto.hotpatch.org.objectweb.asm.Opcodes;
import com.amazon.corretto.hotpatch.patch.ClassTransformerHotPatch;

public class Log4j2DisableLiteralPatternConverter implements ClassTransformerHotPatch {
static final String CLASS_NAME = "org.apache.logging.log4j.core.pattern.LiteralPatternConverter";
static final String CLASS_NAME_SLASH = CLASS_NAME.replace(".", "/");

private final static String NAME = "Log4j2_DisableLiteralPatternConverter";

@Override
public String getName() {
return NAME;
}

@Override
public String getDescription() {
return "Fixes CVE-2021-45105 by patching the LiteralPatternConverter to disable lookup in message patterns.";
}

@Override
public boolean isTargetClass(String className) {
return className.endsWith(CLASS_NAME) || className.endsWith(CLASS_NAME_SLASH);
}

public static boolean isEnabled(String args) {
String param = "--enable-" + NAME;
return args != null && args.contains(param);
}

@Override
public byte[] apply(int asmApiVersion, String className, byte[] classfileBuffer) {
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
ClassVisitor cv = new DisableLiteralPatternConverterClassVisitor(asmApiVersion, cw);
ClassReader cr = new ClassReader(classfileBuffer);
cr.accept(cv, 0);
return cw.toByteArray();
}

public static class DisableLiteralPatternConverterClassVisitor extends ClassVisitor {

public DisableLiteralPatternConverterClassVisitor(int asmApiVersion, ClassVisitor classVisitor) {
super(asmApiVersion, classVisitor);
}

@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
if ("format".equals(name)) {
mv = new LiteralPatternConverterMethodVisitor(api, mv);
}
return mv;
}
}

public static class LiteralPatternConverterMethodVisitor extends MethodVisitor implements Opcodes {
private static final String OWNER = CLASS_NAME_SLASH;
private static final String DESC = "Z";
private static final String NAME = "substitute";
enum State {
CLEAR,
LOADED_SUBSTITUTE,
}

private State state = State.CLEAR;

public LiteralPatternConverterMethodVisitor(int asmApiVersion, MethodVisitor methodVisitor) {
super(asmApiVersion, methodVisitor);
}

@Override
public void visitFieldInsn(int opc, String owner, String name, String desc) {
if (OWNER.equals(owner) && NAME.equals(name) && DESC.equals(desc) && opc == GETFIELD) {
visitState();
} else {
clearState();
}
mv.visitFieldInsn(opc, owner, name, desc);
}

@Override
public void visitJumpInsn(int opc, Label label) {
mv.visitJumpInsn(opc, label);
if (state == State.LOADED_SUBSTITUTE && opc == IFEQ) {
mv.visitJumpInsn(GOTO, label);
}
clearState();

}

private void clearState() {
state = State.CLEAR;
}

private void visitState() {
state = State.LOADED_SUBSTITUTE;
}

@Override
public void visitVarInsn(int opcode, int var) {
clearState();
mv.visitVarInsn(opcode, var);
}

@Override
public void visitTypeInsn(int opcode, String desc) {
clearState();
mv.visitTypeInsn(opcode, desc);
}

@Override
public void visitMethodInsn(int opcode, String owner, String name, String desc) {
clearState();
mv.visitMethodInsn(opcode, owner, name, desc);
}

@Override
public void visitLabel(Label label) {
mv.visitLabel(label);
}

@Override
public void visitLdcInsn(Object cst) {
clearState();
mv.visitLdcInsn(cst);
}

@Override
public void visitIincInsn(int var, int increment) {
clearState();
mv.visitIincInsn(var, increment);
}

@Override
public void visitTableSwitchInsn(int min, int max, Label dflt, Label[] labels) {
mv.visitTableSwitchInsn(min, max, dflt, labels);
}

@Override
public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) {
mv.visitLookupSwitchInsn(dflt, keys, labels);
}

@Override
public void visitMultiANewArrayInsn(String desc, int dims) {
mv.visitMultiANewArrayInsn(desc, dims);
}

@Override
public void visitTryCatchBlock(Label start, Label end, Label handler, String type) {
mv.visitTryCatchBlock(start, end, handler, type);
}

@Override
public void visitLocalVariable(String name, String desc, String signature, Label start, Label end, int index) {
mv.visitLocalVariable(name, desc, signature, start, end, index);
}

@Override
public void visitLineNumber(int line, Label start) {
mv.visitLineNumber(line, start);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file 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.
*/

package com.amazon.corretto.hotpatch.patch.impl.set;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import com.amazon.corretto.hotpatch.patch.ClassTransformerHotPatch;
import com.amazon.corretto.hotpatch.patch.impl.log4j2.Log4j2DisableLiteralPatternConverter;
import com.amazon.corretto.hotpatch.patch.impl.log4j2.Log4j2NoJndiLookup;

/**
* Patch set contains the following patches
* {@link Log4j2NoJndiLookup}
* {@link Log4j2DisableLiteralPatternConverter}
*/
public class Log4j2PatchSetWithDisableLookups extends PatchSetPatcher {
private final List<ClassTransformerHotPatch> patches = Collections.unmodifiableList(Arrays.asList(
(ClassTransformerHotPatch) new Log4j2NoJndiLookup(),
(ClassTransformerHotPatch) new Log4j2DisableLiteralPatternConverter()
));

@Override
public List<ClassTransformerHotPatch> getPatches() {
return patches;
}

@Override
public String getName() {
return "log4j2";
}

@Override
public int getVersion() {
return 2;
}

@Override
public String getShortDescription() {
return "Fix vulnerabilities in Log4j2 related to message lookups and recursive lookups";
}
}
7 changes: 7 additions & 0 deletions src/test/resources/log4j-vuln2.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
appender.console.type = Console
appender.console.name = STDOUT
appender.console.layout.type = PatternLayout
appender.console.layout.pattern = ${ctx:myvar} - %m%n

rootLogger.level = info
rootLogger.appenderRef.stdout.ref = STDOUT
14 changes: 14 additions & 0 deletions test/Vuln2.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.ThreadContext;

public class Vuln2 {
private static final Logger logger = LogManager.getLogger(Vuln2.class);
public static void main(String[] args) throws Exception {
// Have an appender that reads myvar configured like
// appender.console.layout.pattern = ${ctx:myvar} - %m%n
ThreadContext.put("myvar", "${${ctx:myvar}}");
logger.error("Any string");
logger.error("${jndi:ldap://localhost:4444/exp}");
}
}

0 comments on commit 4d43a6c

Please sign in to comment.