How to Build a Log4Shell Detector with ProGuardCORE
Log4Shell (CVE-2021-44228) is a zero-day vulnerability in Log4J, a popular open-source Java logging framework used by many organizations around the world. Though the vulnerability has been patched, and upgrading to a newer Log4J version solves the problem, not everyone has completed the necessary upgrade.
A simple technique to detect the Log4Shell vulnerability is to find something unique in the vulnerable versions that is not in the patched version. That’s exactly the approach used by this Yara rule which detects a particular constructor that only appears in the vulnerable versions.
In this blog post, we will show you how to build a Log4Shell detector using ProGuardCORE to determine if applications are using an older Log4J version that is susceptible to the vulnerability.
Using ProGuardCORE to Detect Log4Shell
ProGuardCORE is a library to parse, modify and analyze Java class files upon which the well-known shrinker, optimizer, and obfuscator ProGuard, and the compatible Android security solution DexGuard, are built.
Using the ProGuardCORE toolbox, we can easily:
- Read Java class files and extract classes from Jar files
- Filter those classes for a specific class
- Filter that specific class for a specific constructor
We’ll concentrate the discussion on the last two points: these implement the actual detection logic. The input reading code will return aClassPool
instance containing a collection ofProgramClass
instances given an input file or directory, which we can use to look for the constructor.
Applying actions on a specific class
ProGuardCORE uses the visitor pattern to interact with the model classes and provides many useful built-in visitors which allow traversing and filtering.
The class we are interested in finding isorg.apache.logging.log4j.core.net.JndiManager
. To apply a visitor to this class, we can call theclassesAccept
method on the program class pool with the class name as the first parameter (note that ProGuardCORE uses internal naming with/
instead of.
):
programClassPool.classesAccept(
"org/apache/logging/log4j/core/net/JndiManager",
classVisitor
)
classVisitor
in this snippet is an instance ofClassVisitor
: it can be implemented by combining some of ProGuardCORE’s built-in visitors to delegate to aMemberVisitor
that can be applied to the constructor we’re interested in.
Applying actions on a specific member
TheJndiManager
constructor we’re looking for has the following signature:
private <init>(Ljava/lang/String;Ljavax/naming/Context;)V
There are a few built-in ProGuardCORE visitors we can use to visit a specific constructor of a class:
AllMemberVisitor
: aClassVisitor
that applies aMemberVisitor
to all members (methods and fields) of a class.MethodFilter
: aMemberVisitor
that delegates to anotherMemberVisitor
if the member is a method (i.e. not a field).ConstructorMethodFilter
: aMemberVisitor
that delegates to anotherMemberVisitor
if the method is a constructor.MemberAccessFilter
: aMemberVisitor
that delegates to anotherMemberVisitor
if the access flags match those given.MemberDescriptorFilter
: aMemberVisitor
that delegates to anotherMemberVisitor
if the member descriptor matches the given descriptor.
Putting these together, we can construct aClassVisitor
that delegates to aMemberVisitor
if the member matches the specificJndiManager
constructor signature:
val classVisitor = AllMemberVisitor(
MethodFilter(
ConstructorMethodFilter(
MemberAccessFilter(
/* requiredSetAccessFlags = */ PRIVATE,
/* requiredUnsetAccessFlags = */ 0,
MemberDescriptorFilter(
"(Ljava/lang/String;Ljavax/naming/Context;)V",
memberVisitor
)
)
)
)
)
Counting visits to members
The main logic of our Log4Shell detector involves finding a specificJndiManager
constructor. Now that we’ve constructed aClassVisitor
that will delegate to aMemberVisitor
if this constructor is found - the implementation of thatMemberVisitor
should be as simple as knowing whether or not it was applied to any member of the visited class.
MemberVisitor
that can be used for this purpose:
MemberCounter
: aMemberVisitor
that counts the number of class members that have been visited.
Using this, we can create thememberVisitor
to be used with ourclassVisitor
from the previous section:
val memberVisitor = MemberCounter()
val classVisitor = AllMemberVisitor(
MethodFilter(
ConstructorMethodFilter(
MemberAccessFilter(
/* requiredSetAccessFlags = */ PRIVATE,
/* requiredUnsetAccessFlags = */ 0,
MemberDescriptorFilter(
"(Ljava/lang/String;Ljavax/naming/Context;)V",
memberVisitor
)
)
)
)
)
After theclassVisitor
is applied to the program class pool we can check how many times thememberVisitor
was applied. It should be once if this is an application using a vulnerable Log4J version.
programClassPool.classesAccept(
"org/apache/logging/log4j/core/net/JndiManager",
classVisitor
)
println(memberVisitor.count) // prints 1 if vulnerable to Log4Shell
Putting it all together
Putting all this together, we can construct a function that takes aClassPool
of a given application as a parameter and returnstrue
orfalse
based on whether or not the application is vulnerable to Log4Shell.
- The class and member filters in ProGuardCORE accept wildcards. This allows us to take into account shadow packing by prefixing the class name with a wildcard that matches any package:
**org/apache/logging/log4j/core/net/JndiManager
- A workaround for protecting against Log4Shell is to remove the class
org/apache/logging/log4j/core/lookup/JndiLookup
so we can first check if this class exists before we check if theJndiManage
constructor exists.
fun check(programClassPool: ClassPool): Boolean {
val jndiLookupCounter = ClassCounter()
val jndiManagerOldConstructorCounter = MemberCounter()
programClassPool.classesAccept(
"**org/apache/logging/log4j/core/lookup/JndiLookup",
jndiLookupCounter)
if (jndiLookupCounter.count == 0) return false
programClassPool.classesAccept(
"**org/apache/logging/log4j/core/net/JndiManager",
AllMemberVisitor(
MethodFilter(
ConstructorMethodFilter(
MemberAccessFilter(
/* requiredSetAccessFlags = */ PRIVATE,
/* requiredUnsetAccessFlags = */ 0,
MemberDescriptorFilter(
"(Ljava/lang/String;Ljavax/naming/Context;)V",
jndiManagerOldConstructorCounter
)
)
)
)
)
)
return jndiManagerOldConstructorCounter.count > 0
}
Conclusion
We’ve shown how ProGuardCORE provides a toolbox to easily implement a Log4Shell detector based on pattern matching classes and members. ProGuardCORE provides even more features not demonstrated here such as partial evaluation, Kotlin metadata support, and powerful instruction sequence matching and replacement.
These powerful features are used at Guardsquare to build software, including ProGuard, the Kotlin metadata printer used in ProGuard Playground, and the Android security solution DexGuard.