← Back to Feed CACHED · 2026-05-17 09:42:19 · cache_key CVE-2025-29912
CVE-2021-44228 · CWE-20 · Disclosed 2021-12-10

Apache Log4j2 2

ASSESSED — NOISGATE V0.5
Vendor
Reassessed
Verdict:
01 · The Real Story

This is a master key hidden inside a library book that half your estate forgot it was carrying

CVE-2021-44228, *Log4Shell*, is a JNDI lookup remote code execution bug in log4j-core, not just log4j-api. The vulnerable upstream ranges span 2.0-beta9 through 2.14.1 for the original bug, and the broader operationally unsafe set now includes 2.15.0 because Apache's first fix was incomplete; Apache's current security guidance drives defenders to safe targets 2.17.0 (Java 8+), 2.12.3 (Java 7), or 2.3.1 (Java 6). The attacker only needs one reachable code path that logs attacker-controlled data such as headers, usernames, URLs, API fields, or chat/game text.

The vendor's 10.0 CRITICAL label matches reality. The usual reasons to downgrade a network bug—required auth, narrow exposure, brittle exploit chain, or limited blast radius—do not hold here: exploitation is unauthenticated, scanning was immediate and global, the dependency sat invisibly inside third-party products, and KEV plus ransomware usage prove this was not a lab-only edge case.

"Log4Shell still earns a hard 10: unauthenticated remote initial access, buried dependency risk, and proven mass exploitation."
02 · The Attack Path

4 steps from start to impact.

STEP 01

Reach a logging sink with attacker-controlled input

Using any HTTP client, curl, Burp, or a scanner such as fullhunt/log4j-scan, the attacker sends ${jndi:ldap://...} in a field likely to be logged: User-Agent, X-Forwarded-For, URI, form data, auth failure text, or app-specific parameters. This is initial access, not post-compromise exploitation, which is why the severity stays pinned at the top.
Conditions required:
  • A reachable service or appliance processes attacker input
  • The application logs some attacker-controlled field
  • A vulnerable log4j-core version is present somewhere in the request path
Where this breaks in practice:
  • Not every endpoint logs attacker data
  • Some products sanitize or truncate headers before logging
  • Internal-only Java services reduce attacker population, but do not remove risk after foothold
Detection/coverage: Network IDS/WAF can catch obvious ${jndi: strings, but coverage is incomplete because payloads can be obfuscated, nested, encoded, or delivered through uncommon headers and parameters.
STEP 02

Trigger JNDI resolution and outbound lookup

The vulnerable log4j-core instance resolves the attacker string and attempts a JNDI lookup over LDAP, LDAPS, RMI, DNS, or related protocols. In real attacks this was commonly paired with marshalsec, JNDIExploit, or simple LDAP referral servers to hand back a second-stage location.
Conditions required:
  • log4j-core is in a vulnerable range
  • The vulnerable logging code path executes on the supplied input
  • The target can perform outbound name resolution or callback traffic
Where this breaks in practice:
  • Egress filtering can break LDAP/RMI/HTTP retrieval
  • Newer JDK behavior and environment hardening can reduce clean RCE outcomes
  • Some paths degrade to information disclosure or failed lookup instead of code execution
Detection/coverage: Best detections are outbound LDAP/RMI/DNS from app tiers, unusual Java process network egress, and DNS canary hits. Pure signature-only HTTP detection misses too much.
STEP 03

Deliver second-stage bytecode or payload

If the callback succeeds, the attacker-controlled infrastructure serves a malicious class or referral that causes arbitrary code execution in the Java process. At this stage the exploit moves from string injection to code-loading, which is why outbound control points matter but are not strong enough to justify a downgrade after the observed exploitation history.
Conditions required:
  • Target egress path reaches attacker infrastructure
  • Runtime/JDK behavior permits the code-loading path used by the attacker
  • The Java process has enough filesystem and process permissions to stage follow-on actions
Where this breaks in practice:
  • Container sandboxes and non-root service accounts reduce post-exploitation options
  • Egress proxies and TLS inspection can break simple off-the-shelf exploit kits
  • Some modern JDK combinations require attacker adaptation
Detection/coverage: EDR can catch Java spawning shells, downloaders, or unusual child processes; NDR can catch LDAP-to-HTTP or DNS-to-HTTP staging chains.
STEP 04

Monetize access with commodity follow-on tooling

Once code execution lands, the attacker typically pivots into webshells, miners, credential theft, or ransomware deployment using the service account's privileges. This step is where enterprise controls like EDR, segmentation, PAM, and least privilege can save you—but by then the vulnerability has already delivered initial access.
Conditions required:
  • Successful code execution in the target process
  • Some privilege or lateral movement path exists beyond the app container
  • Command-and-control or payload staging is not blocked
Where this breaks in practice:
  • Least-privilege service accounts constrain blast radius
  • EDR/NGAV may kill commodity payloads quickly
  • Segmentation can trap the compromise in one tier
Detection/coverage: High-quality EDR and SIEM detections are effective here, but this is post-execution containment, not prevention. Treat alerts here as incident response, not vulnerability management success.
03 · Intelligence Metadata

The supporting signals.

In-the-wild statusYes, emphatically. CISA says exploitation began around 2021-12-01, listed it in KEV on 2021-12-10, and later warned of continued exploitation for initial access on unpatched VMware Horizon/UAG.
KEV / ransomware signalKEV-listed with Known To Be Used in Ransomware Campaigns: Known. That alone removes any argument that this is a theoretical or niche bug.
Proof-of-concept availabilityCommodity-level. Public exploit, scanner, and validation tooling appeared immediately, including fullhunt/log4j-scan; operational exploit kits using JNDI/LDAP referral infrastructure were widespread within days.
EPSS0.94358 from the user-supplied intel. That's an outlier-high exploitation probability signal; secondary mirrors place it around the 94th percentile range.
CVSS vector meaningAV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H means unauthenticated remote exploitation with no user click and full triad impact. In practice, this is the exact kind of bug defenders cannot afford to normalize.
Affected component / versionsOnly log4j-core is affected. Apache lists upstream vulnerable ranges as [2.0-beta9, 2.3.1) ∪ [2.4, 2.12.2) ∪ [2.13.0, 2.15.0) for CVE-2021-44228, but operationally you should also treat 2.15.0 as unsafe because the first fix was incomplete.
Fixed versionsPractical safe targets: 2.17.0 for Java 8+, 2.12.3 for Java 7, 2.3.1 for Java 6. Distros also shipped backports; for example Ubuntu fixed affected packages with release-specific package versions such as 2.10.0-2ubuntu0.1 on 18.04.
Exposure / scanning dataMassive internet pressure. GreyNoise documented sharply increasing opportunistic scanning, and Censys measured 85,328 potentially vulnerable UniFi Network services alone during the response window.
Disclosure timelinePublic disclosure landed on 2021-12-10. Apache credits Chen Zhaojun of Alibaba Cloud Security Team with discovery.
Detection realityAuthenticated file/SBOM scanning beats network-only checks. The hard part was never writing signatures; it was finding shaded, nested, and appliance-bundled log4j-core copies across sprawling estates.
04 · The Call

noisgate verdict.

Final Verdict
= UNCHANGED to CRITICAL (10.0/10)

The decisive factor is that this bug delivers unauthenticated remote initial access on widely deployed Java software with almost no preconditions, and it has a long, proven exploitation record in KEV and ransomware activity. The one meaningful friction point—outbound callback and runtime behavior—was not enough to stop real attackers at scale, and hidden dependency sprawl actually amplifies exposure rather than narrowing it.

HIGH Severity bucket and exploitation status
HIGH Affected-version and safe-target guidance
MEDIUM Per-environment reachable exposure without local inventory

Why this verdict

  • Baseline stays at 10.0: vendor scoring is fair here because the chain starts with unauthenticated remote input and no user interaction; there is no auth, role, or internal-network prerequisite to shave points off.
  • Friction audit does not rescue you: outbound LDAP/RMI/HTTP and JDK hardening can break some exploit paths, but that is only one mid-chain choke point; in the wild, attackers had abundant working combinations and moved to alternative callbacks and payload forms quickly.
  • Exposure uncertainty is an amplifier, not a discount: Log4j lived as a buried dependency inside apps, appliances, and shaded JARs, so many enterprises could not even enumerate where it was. KEV listing, ransomware association, and continued post-2021 exploitation remove any case for downgrading.

Why not higher?

There is nowhere higher than 10.0, and even this bug still depends on a reachable logging path and a workable runtime/egress combination. Some deployments only exposed internal services, and some hardened JDK or egress controls degraded clean RCE outcomes.

Why not lower?

A downgrade would require meaningful population narrowing—authentication, tenant isolation, local access, rare feature enablement, or a brittle exploit chain. Log4Shell had none of those advantages for defenders, and the KEV/ransomware history proves the real-world path was broad enough to matter at enterprise scale.

05 · Compensating Control

What to do — in priority order.

  1. Block risky egress now — Deny outbound LDAP, LDAPS, RMI, and other unnecessary callback paths from application tiers and appliances immediately, within hours. This is the fastest choke point against second-stage retrieval when you cannot patch every buried copy at once.
  2. Hunt every log4j-core copy — Run authenticated JAR/SBOM scans on servers, containers, golden images, and third-party product directories immediately, within hours, because shaded and nested archives are where vulnerable copies hide. Treat vendor appliances and Java fat JARs as first-class search targets.
  3. Fence off unpatchable systems — Isolate internet-facing or high-value unpatchable assets behind tight ACLs, reverse proxies, or temporary withdrawal from service immediately, within hours. If a vendor cannot provide a clean fix quickly, exposure reduction is your only honest control.
  4. Deploy detection on both ingress and egress — Push WAF/IDS/EDR detections for ${jndi: patterns, Java child-process spawning, and outbound LDAP/RMI/DNS anomalies immediately, within hours. Use these as tripwires, not as your primary mitigation story.
What doesn't work
  • Setting only log4j2.formatMsgNoLookups=true is not enough; it did not hold up across the later Log4j vulnerability chain and does not equal a safe end state.
  • Relying on a WAF alone is not enough because payloads can be obfuscated, encoded, or delivered via nonstandard headers and non-HTTP paths.
  • Assuming 'not internet-facing' means low risk is wrong; once an attacker gets any foothold, internal Java services become easy post-initial-access targets.
06 · Verification

Crowdsourced verification payload.

Run this on the target host, container image filesystem, mounted VM disk, or extracted application directory where Java archives live. Invoke it as python3 log4j_check.py /opt or python3 log4j_check.py C:\apps; no admin rights are required beyond read access to the directories and archives being scanned.

noisgate-verify.py
PYTHONREAD-ONLYSAFE
#!/usr/bin/env python3
# log4j_check.py
# Scan a directory tree for log4j-core versions inside JAR/WAR/EAR/ZIP archives.
# This script uses the *practical safe target* logic for Log4Shell-era risk:
#   PATCHED = 2.17.0+ (Java 8+), 2.12.3+ on the 2.12.x line, or 2.3.1+ on the 2.3.x line
#   VULNERABLE = versions in the Log4Shell / incomplete-fix window, including 2.15.0 and 2.12.2
# Output: VULNERABLE / PATCHED / UNKNOWN
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN

import io
import os
import re
import sys
import zipfile
from typing import List, Tuple, Optional

ARCHIVE_EXTS = {'.jar', '.war', '.ear', '.zip'}
POM_PATH = 'META-INF/maven/org.apache.logging.log4j/log4j-core/pom.properties'
MANIFEST_PATH = 'META-INF/MANIFEST.MF'
VERSION_RE = re.compile(r'^version\s*=\s*(.+)$', re.MULTILINE)
IMPL_VERSION_RE = re.compile(r'^Implementation-Version:\s*(.+)$', re.MULTILINE)

class Version:
    def __init__(self, raw: str):
        self.raw = raw.strip()
        self.main, self.stage_rank, self.stage_num = self._parse(self.raw)

    def _parse(self, s: str):
        s = s.strip().lower()
        m = re.match(r'^(\d+)\.(\d+)(?:\.(\d+))?(?:[.-]?(alpha|beta|rc)(\d+)?)?$', s)
        if not m:
            nums = [int(x) for x in re.findall(r'\d+', s)]
            while len(nums) < 3:
                nums.append(0)
            return tuple(nums[:3]), 3, 0
        major = int(m.group(1))
        minor = int(m.group(2))
        patch = int(m.group(3) or 0)
        stage = m.group(4)
        stage_num = int(m.group(5) or 0)
        rank_map = {'alpha': 0, 'beta': 1, 'rc': 2, None: 3}
        return (major, minor, patch), rank_map[stage], stage_num

    def _cmp_tuple(self):
        return self.main + (self.stage_rank, self.stage_num)

    def __lt__(self, other):
        return self._cmp_tuple() < other._cmp_tuple()

    def __le__(self, other):
        return self._cmp_tuple() <= other._cmp_tuple()

    def __ge__(self, other):
        return self._cmp_tuple() >= other._cmp_tuple()

    def __gt__(self, other):
        return self._cmp_tuple() > other._cmp_tuple()

    def __eq__(self, other):
        return self._cmp_tuple() == other._cmp_tuple()


def in_range(v: Version, low: str, high: str) -> bool:
    return Version(low) <= v < Version(high)


def is_practically_safe(vs: str) -> bool:
    v = Version(vs)
    # Safe targets for real-world Log4j remediation, not just the first 44228 fix.
    if v.main[0] != 2:
        return False
    if v.main[1] == 3:
        return v >= Version('2.3.1')
    if v.main[1] == 12:
        return v >= Version('2.12.3')
    return v >= Version('2.17.0')


def is_vulnerable(vs: str) -> bool:
    v = Version(vs)
    # Practical vulnerable set for Log4Shell-era risk:
    # 2.0-beta9 <= v < 2.3.1
    # 2.4 <= v < 2.12.3
    # 2.13.0 <= v < 2.17.0
    return (
        in_range(v, '2.0-beta9', '2.3.1') or
        in_range(v, '2.4.0', '2.12.3') or
        in_range(v, '2.13.0', '2.17.0')
    )


def read_version_from_zip(zf: zipfile.ZipFile) -> Optional[str]:
    # Preferred source: pom.properties
    try:
        with zf.open(POM_PATH) as f:
            text = f.read().decode('utf-8', errors='ignore')
            m = VERSION_RE.search(text)
            if m:
                return m.group(1).strip()
    except KeyError:
        pass
    except Exception:
        pass

    # Fallback: manifest
    try:
        with zf.open(MANIFEST_PATH) as f:
            text = f.read().decode('utf-8', errors='ignore')
            if 'log4j' in text.lower():
                m = IMPL_VERSION_RE.search(text)
                if m:
                    return m.group(1).strip()
    except KeyError:
        pass
    except Exception:
        pass
    return None


def scan_zip_bytes(data: bytes, logical_name: str, findings: List[Tuple[str, str]], errors: List[str]):
    try:
        with zipfile.ZipFile(io.BytesIO(data)) as zf:
            ver = read_version_from_zip(zf)
            if ver:
                findings.append((logical_name, ver))
            for name in zf.namelist():
                lower = name.lower()
                _, ext = os.path.splitext(lower)
                if ext in ARCHIVE_EXTS:
                    try:
                        nested = zf.read(name)
                        scan_zip_bytes(nested, logical_name + '!' + name, findings, errors)
                    except Exception as e:
                        errors.append(f'nested read failed: {logical_name}!{name}: {e}')
    except Exception as e:
        errors.append(f'zip open failed: {logical_name}: {e}')


def scan_path(root: str):
    findings: List[Tuple[str, str]] = []
    errors: List[str] = []
    for base, _, files in os.walk(root):
        for fn in files:
            full = os.path.join(base, fn)
            _, ext = os.path.splitext(fn.lower())
            if ext not in ARCHIVE_EXTS:
                continue
            try:
                with open(full, 'rb') as f:
                    scan_zip_bytes(f.read(), full, findings, errors)
            except Exception as e:
                errors.append(f'file read failed: {full}: {e}')
    return findings, errors


def main():
    if len(sys.argv) != 2:
        print('UNKNOWN - usage: python3 log4j_check.py <path>')
        sys.exit(2)

    root = sys.argv[1]
    if not os.path.exists(root):
        print(f'UNKNOWN - path does not exist: {root}')
        sys.exit(2)

    findings, errors = scan_path(root)

    if not findings and errors:
        print('UNKNOWN - scan errors prevented reliable assessment')
        for e in errors[:20]:
            print(e)
        sys.exit(2)

    if not findings:
        print('UNKNOWN - no log4j-core artifacts found')
        sys.exit(2)

    vulnerable = []
    patched = []
    unknown_versions = []

    for path, ver in findings:
        try:
            if is_vulnerable(ver):
                vulnerable.append((path, ver))
            elif is_practically_safe(ver):
                patched.append((path, ver))
            else:
                unknown_versions.append((path, ver))
        except Exception:
            unknown_versions.append((path, ver))

    if vulnerable:
        print('VULNERABLE')
        for path, ver in vulnerable[:100]:
            print(f'{path} :: {ver}')
        if patched:
            print(f'INFO - also found {len(patched)} patched artifact(s)')
        if unknown_versions:
            print(f'INFO - also found {len(unknown_versions)} artifact(s) with unclassified version strings')
        sys.exit(1)

    if patched and not unknown_versions:
        print('PATCHED')
        for path, ver in patched[:100]:
            print(f'{path} :: {ver}')
        sys.exit(0)

    print('UNKNOWN - only unclassified versions found or mixed non-vulnerable artifacts without clear safe status')
    for path, ver in (unknown_versions or patched)[:100]:
        print(f'{path} :: {ver}')
    sys.exit(2)


if __name__ == '__main__':
    main()
07 · Bottom Line

If you remember one thing.

TL;DR
Monday morning, treat any remaining Log4Shell exposure as an incident-prevention problem, not backlog hygiene: hunt every internet-facing and high-value Java system for buried log4j-core, block risky outbound callback paths, and isolate unpatchable appliances immediately, within hours because KEV and active exploitation override the normal noisgate mitigation SLA. Then drive all affected assets to safe targets (2.17.0, 2.12.3, or 2.3.1, or validated vendor backports) within the noisgate remediation SLA of 90 days, with internet-facing and externally managed products first, followed by internal services, base images, and dormant-but-deployable artifacts.

Sources

  1. Apache Log4j Security Vulnerabilities
  2. Ubuntu CVE tracker for CVE-2021-44228
  3. CISA Known Exploited Vulnerabilities Catalog entry
  4. CISA Joint Advisory AA21-356A
  5. CISA Advisory AA22-174A on continued VMware Horizon exploitation
  6. FIRST EPSS CVE page
  7. Censys Log4j exposure analysis
  8. FullHunt log4j-scan repository
Peer Review

What defenders are saying.

Submit a review attribution: handle + country only
0 flags selected · stored anonymously
Validation Results

Crowdsourced verification outputs.

Results submitted by users who ran the verification payload against their environment.