Skip to main content
Sandfish

OAuth error-redirect phishing: CodeStorm evidence and a possible Storm-1167 link

A November capture supports a CodeStorm-family assessment. Related Sandfish cases reveal identical client files, copied delivery pages, and the limits of a Storm-1167 connection.

The link began at login.microsoftonline.com, Microsoft's real sign-in service. It ended at a company-branded password page backed by a separate collector. The intervening scripts and the collector's protocol point to CodeStorm, the Microsoft 365 phishing kit named by Hexastrike, with moderate confidence.

Following the components reveals more than one relationship. Three earlier Sandfish captures contain the identical client program with different collectors. A shared Cloudflare analytics token leads to copied delivery code, including a page that serves a different password-stealing implementation. The same delivery template can feed different kits, while the same kit can send data to different collectors.

This investigation uses a capture from 5th November 2026, related public captures, and archived source. The browser submitted a synthetic password and received an error. No successful account access or multifactor authentication (MFA) interception was observed. We compare this evidence with the original CodeStorm research and explain the proposed Storm-1167 connection below. Recipient addresses and session values are omitted.

Why this looks like CodeStorm

Hexastrike introduced the CodeStorm name in “CodeStorm – A Microsoft 365 AiTM Phishing Kit with Storm-1167 Overlap,” published on 20th May 2026. Its investigation describes an encoded page loader, a second-stage client hosted on Tencent Cloud Object Storage, and a configurable PHP collector. The client builds a custom Microsoft-style interface and receives branding and authentication state from the collector.

That design can support an adversary-in-the-middle (AiTM) attack: the victim interacts with a fake interface while backend software conducts a separate authentication session with the real provider. Credentials and MFA responses can pass between those sessions. Hexastrike reports successful compromises in its investigations. Our browser capture establishes only the client-side steps and collector exchange described here.

ZeroBEC's follow-up, published on 21st June 2026, documents a more specific controller protocol. A do=check request returns account and organization information. A do=login request submits credentials. Its controlled test linked a fake-password submission to failed Microsoft sign-ins in the test tenant. Those tenant logs belong to ZeroBEC's experiment, not this case.

The November capture shares an unusual combination of these features:

Published CodeStorm featureNovember capture
Five-character landing path/IKyjz on a Microsoft-themed subdomain.
Legitimate libraries followed by a disguised clientGenuine jQuery and Bootstrap, then a different bootstrap.min.js from Tencent COS.
Separate PHP collectorCross-site requests to /google.php.
Identity-discovery protocoldo=check with email; response fields include status, banner, federationLogin, canary, and type.
Configurable collection destinationThe loader supplies a collector override used by the shared client file.

Together, these features support a moderate-confidence CodeStorm-family assessment. A PHP filename, company logo, or cloud provider alone would be weak evidence. The combination of staging, configuration, and controller fields is more specific.

There are differences. This deployment uses a custom checkbox gate rather than the reports' Turnstile gate. Its login request uses user, whereas ZeroBEC documents email. No exact comparison with a verified public CodeStorm reference sample was available. ZeroBEC's current Figure 5 also shows obfuscated HTML rather than the HTTP exchange described by its caption; the protocol comparison uses the written report. The source notes record that limitation.

The useful addition is a later, publicly inspectable deployment with related client files and a different delivery route. OAuth error-redirect abuse gets the browser to this kit; it is not, by itself, a CodeStorm identifier.

The failed sign-in supplies the destination

The starting request used Microsoft's OAuth authorization endpoint. OAuth normally lets an application send a user to an identity provider, then receive a result at a registered reply URL, also called a callback. That result can be an error.

Four parameters explain the relevant part of this request:

ParameterRole in this capture
client_idIdentifies application ddab2a6c-5d0f-4c87-bc90-78cfb125769d.
scope=openidRequests an OpenID Connect scope.
prompt=noneRequests a sign-in without interactive prompts.
stateCarries the recipient address, encoded with Base64.

The original request had no redirect_uri or response_type. It also had no login_hint identifying the recipient to Microsoft. The address was inside state, a value an application can send and receive back during the flow.

The decisive detail is in the Microsoft response. It returned HTTP 200 with the title “Redirecting.” Its configuration set urlAppError to an Amazon S3 page, with error=interaction_required, the original state, and this explanation:

Session information is not sufficient for single-sign-on.

Microsoft's captured session-handling script routes this error path through document.location.replace. The next browser request matched the configured S3 URL. This was page-side navigation, so an inspection that followed only HTTP redirect headers would miss the first transition.

Microsoft's MSAL Python reference documents a pre-registered reply URL as the default when the callback argument is omitted. That is consistent with this response, but we did not inspect this application's registration. Its callback list and owner remain unknown. Microsoft supplied the destination even though the captured starting URL did not contain it.

Registration does not make a destination trustworthy. RFC 9700, section 4.11.2, describes phishing through correctly registered callbacks, including silent requests. LevelBlue's 18th February 2026 demonstration gives a close comparison: client_id, scope, and prompt=none cause a redirect without interactive authentication or an explicit callback. The returned error is what matters here; this case uses the valid openid scope.

Two storage pages carry the recipient forward

The S3 page read state, decoded it, and put the resulting address into an e parameter on a DigitalOcean Spaces URL. The DigitalOcean page forwarded that value to outlookredirect029838272.realitysedge[.]sbs. The recipient context survived each transition, ready to prefill the eventual login.

Observed browser navigation from Microsoft's error-return page to Amazon S3, DigitalOcean Spaces, and the phishing gate. The recipient address travels in state, then in e.

Browser navigation in the 5th November capture. Arrows show the recorded sequence, not server ownership. The S3 script decodes the supplied address; Microsoft does not disclose it from an account. The final arrow combines an intermediate redirect and HTTPS upgrade. S3 source · DigitalOcean source.

The original link also contained a nonstandard uri parameter naming a Salesforce developer page and a long block of code-like text. No request reached Salesforce, and neither forwarding script read uri. Those strings did not explain the recorded route.

The S3 script did not inspect the OAuth error. It needed only the carried address. The error return delivered the browser, while the next pages handled personalization.

The checkbox decides who sees the login

The next page asked the visitor to complete “Security Verification” by selecting “Not a robot.” Its source implemented its own check. No third-party CAPTCHA challenge appears in this stage.

The code scored browser properties, movement events, device type, and time before the click. It checked automation indicators such as navigator.webdriver, plugin availability, and outer-window dimensions. A hidden text field supplied a further trap for software that fills every input. A low score sent the browser to Google; a sufficient score opened the local loader.

The recorded verification reported a score of 100, a mobile device, 25 movement events, and about four seconds before the click. The browser passed. These values show which branch this capture reached; they do not measure how well the gate detects other analysis systems.

Before the verification click

Custom Security Verification card with a Not a robot checkbox

After the login loads

Fake password page with Banfield Pet Hospital and BluePearl logos, a masked recipient address, and an empty password field

Captured on 5th November 2026 at 17:21:27 and 17:21:42 UTC. Both images are cropped. The recipient address is masked in the login image, which precedes password entry. These screenshots show the page states; the requests below establish where the password went. Gate source · Source analysis.

Google was the configured exit for a rejected visitor; this capture did not take that branch. Reaching the password page required the verification click and the next page's scripts.

A separate server chooses the branding

After verification, an encoded loader fetched genuine jQuery and Bootstrap libraries, followed by a separate Tencent Cloud Object Storage file named bootstrap.min.js. That third file was a 5,072,545-byte phishing program. It built the login interface and communicated with vast-wagon.breakfastforbirds[.]surf/google.php, a collector on a different domain.

The recorded requests show how the interface acquired its organization-specific appearance:

Time on 5th November 2026, UTCRequestResult
17:21:36do=user-checkFalse status; the client continued to build the login.
17:21:40do=check and the recipient addressBranding URLs and federation-related configuration.
17:21:46do=login, user, and passIncorrect-account/password error.

The browser fetched the returned banner from Microsoft's tenant-image service. That is why the imitation displayed Banfield Pet Hospital and BluePearl logos. The image was a borrowed resource; it did not make the page or its password destination part of the organization.

One response detail limits what the apparent federation proves. The collector returned type=okta and an Okta sign-in destination. The recovered client, however, had no Okta-specific branch. It selected its default Microsoft-style page and applied the returned branding. The browser did not navigate to Okta.

The login POST carried the recipient identity and an encoded synthetic password to the collector. The client applies URI-component encoding followed by Base64. This reversible transformation is not password hashing. The collector returned an error, and the client displayed a generic failure message. That exchange proves the outbound credential request, but not that Microsoft or Okta checked it.

The script also contained authenticator-code, SMS, approval-notification, and recovery branches. These remain untested, and some have apparent code defects. This capture reached only the password stage. Establishing backend authentication or successful MFA interception would require the server-side exchange or identity-provider logs.

The same script works with different collectors

The large script supplied a stronger comparison than the logos. Its complete body had the same SHA-256 in three earlier Sandfish captures. The comparison uses the bytes after HTTP content decoding and before JavaScript deobfuscation:

Capture date, 2026Related caseRecorded collector
19th October158ccbc5…pierce99.essentialdocstorage[.]de
29th October05662803…hazel66.silverveilvalley[.]de
31st October086d3508…clevermarsh.noticedtoday[.]surf

The source explains how this works. A loader-supplied window.__PANEL_URL_B64__ overrides the script's fallback collector. The same program can therefore use a different destination without changing its own bytes. The 29th October capture reached a credential interface and recorded submissions. The other two confirm script delivery and collector contact, but not a credential-form submission.

The delivery layer can change too. A URLscan capture from 3rd November starts with a Beehiiv tracking link and a different S3 object. It then reaches the same DigitalOcean object, phishing landing path, and visit-report endpoint as the 5th November case. URLscan's reported hash matches the HTTP-decoded DigitalOcean response. This connects the downstream deployment through another entry route, without showing reuse of the same OAuth app.

The November loader also rewrites legacy gentlesunrise[.]de destinations to its configured collector. This is further evidence of an adapted, reusable client. Those legacy hosts were not contacted in the capture, so their presence in source does not establish a live collector relationship.

These comparisons reveal a separation between delivery links, the client program, and its configured collector. Exact file reuse connects software. A shared destination connects a collection path. Neither identifies the people behind every copy.

A copied redirect also reaches a different kit

The November S3 page contains a Cloudflare Web Analytics token: 10f6674ecde64ac78cb062e09acb0537. An exact public Sandfish search returned three captures, including the starting case. This is an analytics token embedded in page source, not a Turnstile key or proof of access to a Cloudflare account.

The 5th October capture supplies the strongest delivery-code comparison. Its S3 response becomes identical to the November response after replacing only the quoted redirect destination with a common placeholder. The comments, recipient-decoding function, and Cloudflare beacon are unchanged. This is a normalized source match, not an identical original-file hash.

The October route used another OAuth application and led to adverse151.ashbyepoint.com[.]de. It stopped at “Working on it.” The delivered script contains browser-fingerprinting and a subsequent form POST, but that POST is absent from the recorded requests. The stored benign verdict describes the evidence and scoring policy of that run; it does not establish the safety of an unseen later stage.

The 28th October capture shows why the analytics token cannot identify the final kit. Its page on docusignauth-web-signin.fly[.]dev retains the same recipient-decoding helper and beacon snippet. Instead of redirecting, it adds the address to its own URL and loads a script from ong3[.]store. The capture starts at this page with OAuth-style error parameters; it does not record a preceding Microsoft authorization request.

That script builds a DBS-branded login with account selection, a company logo from a logo service, and a background image from a website-screenshot service. Its recorded password submission goes to tg8.internalwave.workers[.]dev/send. The request packages the test password and email in a message. The Worker returns a Telegram-shaped channel-message receipt with ok: true. Client code and that response support Telegram forwarding; we did not independently inspect Telegram's server-side receipt or the channel.

The password check is an imitation too. Static decoding of the complete archived script reveals a three-attempt counter. The first two submissions trigger a failure animation; the third triggers success. This decision follows a timer and the counter, not the collector's authentication result. Code also prepares collection when the tab becomes hidden or the page becomes idle. Only one form submission was observed in this run, after which the interface displayed “Attempt 2/3.”

This is a different observed implementation from the Tencent client and /google.php controller. The shared code is in the delivery page. A common operator could use both implementations, but a copied template or shared builder also explains the overlap. The public evidence does not choose between those explanations.

Two kinds of reuse around the November 5 capture: three October captures share the complete client file, while October 5 and October 28 share delivery-page code. The October 28 page uses a different collector.

Lines mark source comparisons, not traffic or common ownership. The upper group shares the complete HTTP-decoded client file. The lower group shares delivery code: a destination-only normalized match on 5th October, and the same helper and beacon on 28th October. Dates are capture dates in 2026. Comparison sources and method.

How the Storm-1167 comparison is supported

Storm-1167 predates the CodeStorm name. In “Detecting and mitigating a multi-stage AiTM phishing and BEC campaign,” published on 8th June 2023, Microsoft described a kit developed, maintained, and operated by the activity cluster it tracks as Storm-1167. The report calls its architecture an indirect proxy: attacker-built pages collect inputs while a separate backend authenticates to Microsoft. It differs from a transparent reverse proxy that forwards the real site's pages.

Microsoft's investigation followed the attack beyond the browser. It documented Tencent-hosted phishing, infrastructure on Indonesian IP addresses, session-cookie replay, MFA-method changes, inbox rules, and onward phishing from compromised accounts. These account-side observations supported its activity-cluster attribution.

Hexastrike proposes a CodeStorm–Storm-1167 overlap at moderate confidence. Its reasoning combines Tencent cloud use, Indonesian-language artifacts in recovered render code, and similar post-compromise behavior. The language artifacts and Microsoft's Indonesian IP observations are different evidence. Neither proves operator nationality, and Tencent is a shared hosting provider.

ComparisonWhat supports itWhat remains missing here
November client to CodeStormStaging design, configurable collector, and specific discovery fields.An exact verified reference-source match.
CodeStorm to Storm-1167Hexastrike's infrastructure, source-language, and incident-response comparison with Microsoft's report.A confirmed identity or exclusive operator relationship.
November case to Storm-1167A plausible lead through the CodeStorm-family assessment.Case-specific account activity, backend authentication evidence, or infrastructure control linking this deployment to that cluster.

The connection is worth discussing because the architecture and published history fit. It cannot be made transitive: a CodeStorm-like client does not make every deployment a Storm-1167 operation. Shared or adapted software can serve different operators, just as the copied redirect here serves different clients. We therefore retain the kit-family assessment and leave this case's operator unassigned.

The first domain does not describe the whole flow

Ordinary silent sign-in also uses prompt=none; its presence alone is not a phishing finding. The useful distinction is what happens after the error returns the browser to the application.

This capture shows where each part of the flow takes control. Microsoft supplies the S3 error-return destination. Two storage pages preserve the recipient context. The custom gate decides whether to expose the login. A CodeStorm-like client asks a separate collector for branding and sends it the password request.

The related captures make the result more useful. An exact client match follows the software across collectors. A copied redirect follows the delivery template across different implementations. These are two supported relationships, not one proven operator network. Storm-1167 supplies a relevant historical comparison, while attribution and successful authentication remain unresolved for the November case.

The genuine starting domain is therefore compatible with a malicious outcome. To understand that outcome, follow the component that is actually shared, then check what each deployment does with it.

Source notes

Research was completed on 5th November 2026. The main capture used mobile Firefox and retained 28 requests and 13 snapshots. It stopped after the synthetic-password error. We reviewed stored evidence and performed static decoding; this investigation started no new phishing-site visit or form submission. The original delivery email was not available.

The source ledger lists exact request and snapshot IDs, figure transformations, complete-file hashes, comparison records, and search limits. It records our retained observations; it is not an independent copy of every source. The script response has this SHA-256 after HTTP content decoding and before JavaScript deobfuscation:

aa7351b192f1e577e2033e114155d9ce520762e1abd19a4b3b777b4483ffbb4c

Public articles were retrieved on 5th November. Their dates above are publisher-displayed dates; independent historical copies were not checked. The related captures establish reuse, not a victim count or a complete campaign inventory.

Article update, 5th November 2026: Expanded the CodeStorm comparison, added the Cloudflare-linked delivery cases and their different collector behavior, and explained the evidence behind the proposed Storm-1167 relationship.