Skip to content

Commit 142b8a0

Browse files
committed
Log4j Disable Literal Pattern Converter
1 parent 999092f commit 142b8a0

File tree

8 files changed

+320
-3
lines changed

8 files changed

+320
-3
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ Release Date: TBD
99
### [#39](https://github.com/corretto/hotpatch-for-apache-log4j2/pull/39) Support for multiple patches
1010
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.
1111

12+
### [#46](https://github.com/corretto/hotpatch-for-apache-log4j2/pull/46) Add patch for CVE-2021-45105
13+
Patch for [CVE-2021-45105](https://nvd.nist.gov/vuln/detail/CVE-2021-45105) is added to the tool. This patches the
14+
LiteralPatternConverter to disable lookups in format messages.
15+
1216
### Ongoing Changes ###
1317
See [all changes](https://github.com/corretto/hotpatch-for-apache-log4j2/compare/1.3.0...main) since the previous version.
1418

README.md

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
# Log4jHotPatch
22

3-
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/).
3+
This is a tool which injects a Java agent into a running JVM process. The agent will attempt to patch the `lookup()`
4+
method of all loaded `org.apache.logging.log4j.core.lookup.JndiLookup` instances to unconditionally return the string
5+
"Patched JndiLookup::lookup()". It is designed to address the
6+
[CVE-2021-44228](https://nvd.nist.gov/vuln/detail/CVE-2021-44228/) remote code execution vulnerability in Log4j without
7+
restarting the Java process. This tool will also address
8+
[CVE-2021-45046](https://nvd.nist.gov/vuln/detail/CVE-2021-45046/).
9+
10+
To Patch for [CVE-2021-45105](https://nvd.nist.gov/vuln/detail/CVE-2021-45105), you can run the tool with the following
11+
parameters `patcherClassName=com.amazon.corretto.hotpatch.patch.impl.set.Log4j2PatchSetWithDisableLookups`. This will
12+
patch the LiteralPatternConverter to disable the lookups in log patterns.
413

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

build-tools/bin/run_tests.sh

+43-1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,19 @@ function start_static_target() {
4646
popd > /dev/null
4747
}
4848

49+
function start_dos_test() {
50+
if [[ -f /tmp/vuln2.log ]]; then
51+
rm /tmp/vuln2.log
52+
fi
53+
local jdk_dir=$1
54+
local agent_jar=$2
55+
56+
pushd "${ROOT_DIR}/test" > /dev/null
57+
${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 &
58+
59+
popd > /dev/null
60+
}
61+
4962
function static_agent_configure_verbose() {
5063
if [ "$3" = "unset" ]; then
5164
PROP_VALUE=""
@@ -107,6 +120,25 @@ function verify_idempotent_agent() {
107120
fi
108121
}
109122

123+
function verify_dos_test() {
124+
if grep -q '${ctx:myvar} - Any string' /tmp/vuln2.log
125+
then
126+
echo "Test passed. JVM did not run into StackOverflow"
127+
else
128+
echo "Test failed. JVM failed."
129+
cat /tmp/vuln2.log
130+
exit 1
131+
fi
132+
if grep -q '${ctx:myvar} - Patched JndiLookup::lookup()' /tmp/vuln2.log
133+
then
134+
echo "Test passed. Patched JndiLookup::lookup"
135+
else
136+
echo "Test failed. Did not patch JndiLookup"
137+
cat /tmp/vuln2.log
138+
exit 1
139+
fi
140+
}
141+
110142
if [[ $# -lt 2 ]]; then
111143
usage
112144
exit 1
@@ -165,7 +197,7 @@ if [[ "${JVM_MV}" == "17" ]]; then
165197
fi
166198

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

171203
echo
@@ -353,7 +385,17 @@ if [[ -z "${SKIP_STATIC}" ]]; then
353385
start_target ${JDK_DIR}
354386
VULN_PID=$!
355387

388+
356389
sleep 2
357390

358391
verify_target $VULN_PID
392+
unset _JAVA_OPTIONS
393+
echo
394+
echo "******************"
395+
echo "Running Literal Pattern Converter JDK${JVM_MV} Test"
396+
echo "------------------"
397+
398+
start_dos_test ${JDK_DIR} ${AGENT_JAR}
399+
sleep 2
400+
verify_dos_test
359401
fi

src/main/java/com/amazon/corretto/hotpatch/HotPatchAgent.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public class HotPatchAgent {
7373
public static int asmApiVersion() {
7474
return Opcodes.ASM9;
7575
}
76-
76+
7777
/**
7878
* This is the entry point when the agent is loaded during startup. The main difference in this scenario will be that
7979
* we only need to load our transformers, but there is no need to retransform existing classes as they have not been
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package com.amazon.corretto.hotpatch.patch.impl.log4j2;
17+
18+
import com.amazon.corretto.hotpatch.org.objectweb.asm.ClassReader;
19+
import com.amazon.corretto.hotpatch.org.objectweb.asm.ClassVisitor;
20+
import com.amazon.corretto.hotpatch.org.objectweb.asm.ClassWriter;
21+
import com.amazon.corretto.hotpatch.org.objectweb.asm.Label;
22+
import com.amazon.corretto.hotpatch.org.objectweb.asm.MethodVisitor;
23+
import com.amazon.corretto.hotpatch.org.objectweb.asm.Opcodes;
24+
import com.amazon.corretto.hotpatch.patch.ClassTransformerHotPatch;
25+
26+
public class Log4j2DisableLiteralPatternConverter implements ClassTransformerHotPatch {
27+
static final String CLASS_NAME = "org.apache.logging.log4j.core.pattern.LiteralPatternConverter";
28+
static final String CLASS_NAME_SLASH = CLASS_NAME.replace(".", "/");
29+
30+
private final static String NAME = "Log4j2_DisableLiteralPatternConverter";
31+
32+
@Override
33+
public String getName() {
34+
return NAME;
35+
}
36+
37+
@Override
38+
public String getDescription() {
39+
return "Fixes CVE-2021-45105 by patching the LiteralPatternConverter to disable lookup in message patterns.";
40+
}
41+
42+
@Override
43+
public boolean isTargetClass(String className) {
44+
return className.endsWith(CLASS_NAME) || className.endsWith(CLASS_NAME_SLASH);
45+
}
46+
47+
public static boolean isEnabled(String args) {
48+
String param = "--enable-" + NAME;
49+
return args != null && args.contains(param);
50+
}
51+
52+
@Override
53+
public byte[] apply(int asmApiVersion, String className, byte[] classfileBuffer) {
54+
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
55+
ClassVisitor cv = new DisableLiteralPatternConverterClassVisitor(asmApiVersion, cw);
56+
ClassReader cr = new ClassReader(classfileBuffer);
57+
cr.accept(cv, 0);
58+
return cw.toByteArray();
59+
}
60+
61+
public static class DisableLiteralPatternConverterClassVisitor extends ClassVisitor {
62+
63+
public DisableLiteralPatternConverterClassVisitor(int asmApiVersion, ClassVisitor classVisitor) {
64+
super(asmApiVersion, classVisitor);
65+
}
66+
67+
@Override
68+
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
69+
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
70+
if ("format".equals(name)) {
71+
mv = new LiteralPatternConverterMethodVisitor(api, mv);
72+
}
73+
return mv;
74+
}
75+
}
76+
77+
public static class LiteralPatternConverterMethodVisitor extends MethodVisitor implements Opcodes {
78+
private static final String OWNER = CLASS_NAME_SLASH;
79+
private static final String DESC = "Z";
80+
private static final String NAME = "substitute";
81+
enum State {
82+
CLEAR,
83+
LOADED_SUBSTITUTE,
84+
}
85+
86+
private State state = State.CLEAR;
87+
88+
public LiteralPatternConverterMethodVisitor(int asmApiVersion, MethodVisitor methodVisitor) {
89+
super(asmApiVersion, methodVisitor);
90+
}
91+
92+
@Override
93+
public void visitFieldInsn(int opc, String owner, String name, String desc) {
94+
if (OWNER.equals(owner) && NAME.equals(name) && DESC.equals(desc) && opc == GETFIELD) {
95+
visitState();
96+
} else {
97+
clearState();
98+
}
99+
mv.visitFieldInsn(opc, owner, name, desc);
100+
}
101+
102+
@Override
103+
public void visitJumpInsn(int opc, Label label) {
104+
mv.visitJumpInsn(opc, label);
105+
if (state == State.LOADED_SUBSTITUTE && opc == IFEQ) {
106+
mv.visitJumpInsn(GOTO, label);
107+
}
108+
clearState();
109+
110+
}
111+
112+
private void clearState() {
113+
state = State.CLEAR;
114+
}
115+
116+
private void visitState() {
117+
state = State.LOADED_SUBSTITUTE;
118+
}
119+
120+
@Override
121+
public void visitVarInsn(int opcode, int var) {
122+
clearState();
123+
mv.visitVarInsn(opcode, var);
124+
}
125+
126+
@Override
127+
public void visitTypeInsn(int opcode, String desc) {
128+
clearState();
129+
mv.visitTypeInsn(opcode, desc);
130+
}
131+
132+
@Override
133+
public void visitMethodInsn(int opcode, String owner, String name, String desc) {
134+
clearState();
135+
mv.visitMethodInsn(opcode, owner, name, desc);
136+
}
137+
138+
@Override
139+
public void visitLabel(Label label) {
140+
mv.visitLabel(label);
141+
}
142+
143+
@Override
144+
public void visitLdcInsn(Object cst) {
145+
clearState();
146+
mv.visitLdcInsn(cst);
147+
}
148+
149+
@Override
150+
public void visitIincInsn(int var, int increment) {
151+
clearState();
152+
mv.visitIincInsn(var, increment);
153+
}
154+
155+
@Override
156+
public void visitTableSwitchInsn(int min, int max, Label dflt, Label[] labels) {
157+
mv.visitTableSwitchInsn(min, max, dflt, labels);
158+
}
159+
160+
@Override
161+
public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) {
162+
mv.visitLookupSwitchInsn(dflt, keys, labels);
163+
}
164+
165+
@Override
166+
public void visitMultiANewArrayInsn(String desc, int dims) {
167+
mv.visitMultiANewArrayInsn(desc, dims);
168+
}
169+
170+
@Override
171+
public void visitTryCatchBlock(Label start, Label end, Label handler, String type) {
172+
mv.visitTryCatchBlock(start, end, handler, type);
173+
}
174+
175+
@Override
176+
public void visitLocalVariable(String name, String desc, String signature, Label start, Label end, int index) {
177+
mv.visitLocalVariable(name, desc, signature, start, end, index);
178+
}
179+
180+
@Override
181+
public void visitLineNumber(int line, Label start) {
182+
mv.visitLineNumber(line, start);
183+
}
184+
}
185+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package com.amazon.corretto.hotpatch.patch.impl.set;
17+
18+
import java.util.Arrays;
19+
import java.util.Collections;
20+
import java.util.List;
21+
22+
import com.amazon.corretto.hotpatch.patch.ClassTransformerHotPatch;
23+
import com.amazon.corretto.hotpatch.patch.impl.log4j2.Log4j2DisableLiteralPatternConverter;
24+
import com.amazon.corretto.hotpatch.patch.impl.log4j2.Log4j2NoJndiLookup;
25+
26+
/**
27+
* Patch set contains the following patches
28+
* {@link Log4j2NoJndiLookup}
29+
* {@link Log4j2DisableLiteralPatternConverter}
30+
*/
31+
public class Log4j2PatchSetWithDisableLookups extends PatchSetPatcher {
32+
private final List<ClassTransformerHotPatch> patches = Collections.unmodifiableList(Arrays.asList(
33+
(ClassTransformerHotPatch) new Log4j2NoJndiLookup(),
34+
(ClassTransformerHotPatch) new Log4j2DisableLiteralPatternConverter()
35+
));
36+
37+
@Override
38+
public List<ClassTransformerHotPatch> getPatches() {
39+
return patches;
40+
}
41+
42+
@Override
43+
public String getName() {
44+
return "log4j2-v2";
45+
}
46+
47+
@Override
48+
public int getVersion() {
49+
return 1;
50+
}
51+
52+
@Override
53+
public String getShortDescription() {
54+
return "Fix vulnerabilities in Log4j2 related to message lookups and recursive lookups";
55+
}
56+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
appender.console.type = Console
2+
appender.console.name = STDOUT
3+
appender.console.layout.type = PatternLayout
4+
appender.console.layout.pattern = ${ctx:myvar} - %m%n
5+
6+
rootLogger.level = info
7+
rootLogger.appenderRef.stdout.ref = STDOUT

test/Vuln2.java

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import org.apache.logging.log4j.LogManager;
2+
import org.apache.logging.log4j.Logger;
3+
import org.apache.logging.log4j.ThreadContext;
4+
5+
public class Vuln2 {
6+
private static final Logger logger = LogManager.getLogger(Vuln2.class);
7+
public static void main(String[] args) throws Exception {
8+
// Have an appender that reads myvar configured like
9+
// appender.console.layout.pattern = ${ctx:myvar} - %m%n
10+
ThreadContext.put("myvar", "${${ctx:myvar}}");
11+
logger.error("Any string");
12+
logger.error("${jndi:ldap://localhost:4444/exp}");
13+
}
14+
}

0 commit comments

Comments
 (0)