Fake AI Ads Browser-in-the-Browser Identity Phishing Hunt

Threat Overview

Island researchers disclosed an active, human-operated phishing platform disguised as AI advertising and account-management products. The operation has impersonated ChatGPT, Gemini, Claude, Perplexity, Manus, Meta’s Muse, and related advertising workflows. A “Connect” action opens a browser window drawn inside the real web page; the fake chrome displays a trusted origin such as accounts.google.com or an Okta tenant even though the real browser remains on the phishing site.

The platform is not a transparent adversary-in-the-middle proxy. It locally rebuilds Google, Meta, TikTok, and Okta authentication flows, stores up to three password attempts, fingerprints the device, and lets a human operator select the next MFA prompt in real time. Supported prompts include SMS codes, authenticator codes, Google approval/number matching, QR verification, and Okta push or authenticator challenges. Island observed hundreds of victim submissions and ongoing activity at publication. Reused Next.js, Socket.IO, Railway, and Render infrastructure links the AI-advertising lures to refund and recruitment themes.

References

Impacted Systems

  • Identity providers/accounts: Google accounts and Workspace identities, Okta identities, Meta accounts, and TikTok accounts entered into the phishing flows. Microsoft Entra telemetry is relevant when a captured corporate identity is also federated, reused, or followed by access to Microsoft resources; the cited campaign does not claim a native Microsoft sign-in lure.
  • Business environments: advertising agencies, media buyers, Google Ads manager/MCC administrators, client advertising accounts, and organizations whose employees use work identities for recruitment or refund lures.
  • Endpoints/platforms: Windows, macOS, iOS, and Android browsers; the fake browser adapts its visual chrome to the victim platform.
  • Delivery/exposure: invitation emails and links to attacker-controlled domains; public pages commonly hosted on Vercel with Railway or Render backends.
  • Prerequisites: user visits the lure, selects Connect, and submits credentials and/or MFA responses. Phishing-resistant, origin-bound authentication materially reduces reusable credential theft.
  • Not established: the cited research does not say that every Railway, Render, Vercel, AI-advertising, refund, or recruitment site is malicious.

Why this matters

The campaign is active, targets high-value business spending accounts, and can also capture corporate Google or Okta identities through recruitment lures. Human-controlled MFA prompts defeat simple “one bad password submission” assumptions and may create convincing sequences of failures, retries, and approval prompts. Published domains, backend hosts, API paths, and control vocabulary support immediate Sentinel hunting across email, URL-click, endpoint network, proxy/DNS, and identity telemetry.

Exploitation Status

Confirmed active phishing activity. Island observed hundreds of submissions and stated that the operation remained active when the report was published on October 6, 2026. Researchers attribute operation of the live state machine to human controllers based on exposed source code and observed command behavior. The infrastructure list is a researcher-published IOC set, not a vendor blocklist; commodity hosting domains require exact-host matching and surrounding context.

What this hunt looks for

Published lure and backend domains, campaign API and Socket.IO paths, delivered and clicked URLs, endpoint connections, authentication retries followed by success, new user/IP/device combinations, post-sign-in identity changes, and mailbox persistence.

Required logs

Microsoft 365 Defender EmailEvents, EmailUrlInfo, and UrlClickEvents; Microsoft Defender for Endpoint DeviceNetworkEvents; proxy, DNS, or firewall telemetry; Microsoft Entra SigninLogs and AuditLogs; OfficeActivity; and Google or Okta identity logs where applicable.

Hunt 1 — Published campaign domains in email URL telemetry

let lookback = 30d;
let domains = dynamic([
"museads.ai","advertising-chatgpt.com","ads-team-openai.com","openai-ads.ai","chatgpt-monday-brief.com",
"advertising-gemini.com","gemini-ads.ai","gemini-google-ads.com","ads-claude.com","claude-ads.ai",
"perplexity-advertising.com","manus-meta.im","mcc-account-sync.com","sync-ads-account.com","sync-business.com",
"backend-production-6d75.up.railway.app","museadsback-production.up.railway.app","chatgptadsback-production.up.railway.app",
"geminiback-production.up.railway.app","anthropicadsback.onrender.com","syncgoogleadsback.onrender.com"
]);
EmailUrlInfo
| where Timestamp >= ago(lookback)
| extend Host=tolower(tostring(parse_url(Url).Host))
| where Host in (domains)
| join kind=leftouter (EmailEvents | where Timestamp >= ago(lookback) | project NetworkMessageId, RecipientEmailAddress, SenderFromAddress, Subject, DeliveryAction, EmailDirection) on NetworkMessageId
| project Timestamp, RecipientEmailAddress, SenderFromAddress, Subject, DeliveryAction, Url, Host, NetworkMessageId
| order by Timestamp desc

Hunt 2 — User clicks to published lure infrastructure

let lookback = 30d;
let domains = dynamic(["museads.ai","advertising-chatgpt.com","ads-team-openai.com","openai-ads.ai","chatgpt-monday-brief.com","advertising-gemini.com","gemini-ads.ai","gemini-google-ads.com","ads-claude.com","claude-ads.ai","perplexity-advertising.com","manus-meta.im","mcc-account-sync.com","sync-ads-account.com","sync-business.com"]);
UrlClickEvents
| where Timestamp >= ago(lookback)
| extend Host=tolower(tostring(parse_url(Url).Host))
| where Host in (domains)
| project Timestamp, AccountUpn, ActionType, IsClickedThrough, Url, Host, NetworkMessageId, IPAddress
| order by Timestamp desc

Hunt 3 — Endpoint connections to lure or backend infrastructure

let lookback = 30d;
let domains = dynamic(["museads.ai","advertising-chatgpt.com","ads-team-openai.com","openai-ads.ai","chatgpt-monday-brief.com","advertising-gemini.com","gemini-ads.ai","gemini-google-ads.com","ads-claude.com","claude-ads.ai","perplexity-advertising.com","manus-meta.im","backend-production-6d75.up.railway.app","museadsback-production.up.railway.app","chatgptadsback-production.up.railway.app","geminiback-production.up.railway.app","anthropicadsback.onrender.com","syncgoogleadsback.onrender.com"]);
DeviceNetworkEvents
| where Timestamp >= ago(lookback)
| where tolower(RemoteUrl) in (domains)
| project Timestamp, DeviceName, InitiatingProcessAccountUpn, InitiatingProcessFileName, InitiatingProcessCommandLine, RemoteUrl, RemoteIP, RemotePort
| order by Timestamp desc

Hunt 4 — Campaign API and Socket.IO patterns in proxy or web-security logs

let lookback = 30d;
union isfuzzy=true CommonSecurityLog, AzureDiagnostics
| where TimeGenerated >= ago(lookback)
| extend Raw=tostring(pack_all()), Src=coalesce(column_ifexists("SourceIP", ""), column_ifexists("clientIp_s", "")), User=coalesce(column_ifexists("SourceUserName", ""), column_ifexists("user_s", ""))
| where Raw has_any ("/api/create/user", "/api/send/ip", "operator-command", "telegram-command", "socket.io")
| where Raw has_any ("railway.app", "onrender.com", "museads.ai", "advertising-chatgpt.com", "advertising-gemini.com", "ads-claude")
| project TimeGenerated, Src, User, Evidence=substring(Raw, 0, 1800)
| order by TimeGenerated desc

Hunt 5 — Published-domain click followed by Entra sign-in failures or success

let lookback = 30d;
let domains = dynamic(["museads.ai","advertising-chatgpt.com","ads-team-openai.com","openai-ads.ai","chatgpt-monday-brief.com","advertising-gemini.com","gemini-ads.ai","gemini-google-ads.com","ads-claude.com","claude-ads.ai","perplexity-advertising.com","manus-meta.im"]);
let Clicks = UrlClickEvents
| where Timestamp >= ago(lookback)
| extend Host=tolower(tostring(parse_url(Url).Host))
| where Host in (domains) and isnotempty(AccountUpn)
| project AccountUpn=tolower(AccountUpn), ClickTime=Timestamp, Url, ClickIP=IPAddress;
SigninLogs
| where TimeGenerated >= ago(lookback)
| extend AccountUpn=tolower(UserPrincipalName)
| join kind=inner Clicks on AccountUpn
| where TimeGenerated between (ClickTime .. ClickTime + 4h)
| project TimeGenerated, UserPrincipalName, ResultType, ResultDescription, IPAddress, AppDisplayName, ClientAppUsed, DeviceDetail, LocationDetails, ConditionalAccessStatus, ClickTime, Url, ClickIP
| order by TimeGenerated desc

Hunt 6 — Rapid authentication failures followed by success from a different IP

let lookback = 14d;
let Failures = SigninLogs
| where TimeGenerated >= ago(lookback) and ResultType != 0
| summarize FailureCount=count(), FailureIPs=make_set(IPAddress, 20), FirstFailure=min(TimeGenerated), LastFailure=max(TimeGenerated) by UserPrincipalName, bin(TimeGenerated, 15m);
let Successes = SigninLogs
| where TimeGenerated >= ago(lookback) and ResultType == 0
| project SuccessTime=TimeGenerated, UserPrincipalName, SuccessIP=IPAddress, AppDisplayName, DeviceDetail, LocationDetails, AuthenticationDetails;
Failures
| join kind=inner Successes on UserPrincipalName
| where SuccessTime between (LastFailure .. LastFailure + 30m)
| where FailureCount >= 2 and set_has_element(FailureIPs, SuccessIP) == false
| project UserPrincipalName, FailureCount, FailureIPs, FirstFailure, LastFailure, SuccessTime, SuccessIP, AppDisplayName, DeviceDetail, LocationDetails, AuthenticationDetails
| order by SuccessTime desc

Hunt 7 — Successful sign-in from a new user/IP or device combination

let baseline = 30d;
let recent = 2d;
let Known = SigninLogs
| where TimeGenerated between (ago(baseline) .. ago(recent)) and ResultType == 0
| extend DeviceId=tostring(DeviceDetail.deviceId)
| summarize by UserPrincipalName, IPAddress, DeviceId;
SigninLogs
| where TimeGenerated >= ago(recent) and ResultType == 0
| extend DeviceId=tostring(DeviceDetail.deviceId), Browser=tostring(DeviceDetail.browser), OS=tostring(DeviceDetail.operatingSystem)
| join kind=leftanti Known on UserPrincipalName, IPAddress, DeviceId
| project TimeGenerated, UserPrincipalName, IPAddress, DeviceId, Browser, OS, AppDisplayName, LocationDetails, ConditionalAccessStatus, RiskLevelDuringSignIn
| order by TimeGenerated desc

Hunt 8 — Identity or privilege changes soon after a suspicious sign-in

let lookback = 14d;
let RecentSignins = SigninLogs
| where TimeGenerated >= ago(lookback) and ResultType == 0
| project UserPrincipalName=tolower(UserPrincipalName), SigninTime=TimeGenerated, SigninIP=IPAddress;
AuditLogs
| where TimeGenerated >= ago(lookback)
| where OperationName has_any ("Add member to role", "Add app role assignment", "Consent to application", "Update user", "Update authentication methods", "Add service principal", "Add owner to application")
| extend ActorUPN=tolower(tostring(InitiatedBy.user.userPrincipalName)), ActorIP=tostring(InitiatedBy.user.ipAddress)
| join kind=inner RecentSignins on $left.ActorUPN == $right.UserPrincipalName
| where TimeGenerated between (SigninTime .. SigninTime + 4h)
| project TimeGenerated, OperationName, Result, ActorUPN, ActorIP, SigninTime, SigninIP, TargetResources, AdditionalDetails
| order by TimeGenerated desc

Hunt 9 — Post-compromise mailbox forwarding or inbox-rule changes

let lookback = 14d;
OfficeActivity
| where TimeGenerated >= ago(lookback)
| where OfficeWorkload == "Exchange"
| where Operation in ("New-InboxRule", "Set-InboxRule", "Set-Mailbox", "UpdateInboxRules")
| extend Raw=tostring(pack_all())
| where Raw has_any ("ForwardTo", "ForwardingSmtpAddress", "RedirectTo", "DeleteMessage", "MarkAsRead")
| project TimeGenerated, UserId, Operation, ClientIP, ResultStatus, Parameters, RawEvent=substring(Raw, 0, 1800)
| order by TimeGenerated desc

Hunt 10 — DNS and security-device fallback for exact campaign hosts

let lookback = 30d;
let domains = dynamic(["museads.ai","advertising-chatgpt.com","ads-team-openai.com","openai-ads.ai","chatgpt-monday-brief.com","advertising-gemini.com","gemini-ads.ai","gemini-google-ads.com","ads-claude.com","claude-ads.ai","perplexity-advertising.com","manus-meta.im","backend-production-6d75.up.railway.app","museadsback-production.up.railway.app","chatgptadsback-production.up.railway.app","geminiback-production.up.railway.app","anthropicadsback.onrender.com"]);
union isfuzzy=true DnsEvents, CommonSecurityLog
| where TimeGenerated >= ago(lookback)
| extend Raw=tostring(pack_all()), Host=tolower(coalesce(column_ifexists("Name", ""), column_ifexists("DestinationHostName", ""), column_ifexists("RequestURL", "")))
| where Host in (domains) or Raw has_any (domains)
| project TimeGenerated, Host, Evidence=substring(Raw, 0, 1600)
| order by TimeGenerated desc

Detection Notes

  • Hunts 1–5 are the highest-value campaign-specific detections. Exact lure-domain delivery or click plus endpoint/proxy API evidence substantially raises confidence.
  • Railway, Render, Vercel, Socket.IO, api.ipify.org, and ipapi.co are legitimate services. Match exact published backend hosts and combine them with lure domains or campaign paths; do not block entire shared-hosting domains.
  • The operator may replay Google or Okta credentials outside Microsoft Entra. Entra-only customers can see downstream Microsoft access only if the same identity is federated, reused, or used to reach Microsoft applications.
  • Push fatigue or repeated MFA events may occur entirely in Google or Okta logs. Without those connectors, Sentinel cannot see the decisive identity-provider sequence.
  • A click without a sign-in does not establish credential submission. Conversely, a credential can be submitted from a personal/unmanaged device that has no endpoint or click telemetry.
  • New-IP/device and mailbox-rule hunts are post-compromise behavioral detections, not unique signatures of this campaign. Confirm the published-link exposure or other identity evidence before attribution.
  • UrlClickEvents, EmailUrlInfo, and EmailEvents availability depends on Defender for Office 365 licensing and connector configuration.