Skip to main content
Sandfish

· Updated

The checkbox that hides a phishing kit

An imitation Cloudflare check hides a configurable payment-phishing application. Its client code reveals commands for steering verification prompts; 2,076 public scans show the limits of gate-only captures.

On 6th October 2026, a German-language phishing page was submitted to Sandfish. The initial capture recorded two requests and a screenshot of what looked like a Cloudflare security check. It never reached the payment page behind it.

The Cloudflare check was fake. Behind it, a payment-phishing application combined card collection with a command channel designed to let an operator choose the visitor's next verification prompt. Reading that client code explains why the apparent €3,99 health-card payment can become a much longer interaction.

The screen in front of it is a static page with one unlabelled button. It is enough to stop the initial Sandfish capture. The investigation's public-scan corpus contained 2,076 submissions between 11th May and 6th October 2026, with no page behind the gate recorded. That measures coverage in this corpus, not every scanner's ability to pass it.

Investigation scope

This article covers attacker infrastructure and public scan records. The session used to reach the kit was generated on a researcher's own machine. No victim data was collected, and no phishing form was submitted with real values.

A check that never talks to Cloudflare

The gate page is 22,882 bytes. It copies the Cloudflare wordmark, prints "Privacy · Help" where the real Turnstile widget puts its footer, and selects a prompt from navigator.languages. The table holds 62 locale keys that resolve to 54 distinct prompt strings. A German visitor reads "Bitte bestätigen Sie, dass Sie ein Mensch sind".

Imitation Cloudflare verification widget with a German prompt, an empty checkbox, and the Cloudflare wordmark

The gate as a German visitor sees it. Rendered offline from the preserved page, with all network requests blocked. The box on the left is the only control, and it carries no visible label.

It makes no request to a Cloudflare challenge endpoint. The complete capture of this page is two requests: the HTML and a favicon.

The only control that advances the page is this:

<button type="button" class="checkbox" id="verifyTrigger" aria-label="点击验证">

The accessible name is Chinese for "click to verify". The button carries no visible text, only a drawn square box.

Clicking it posts to the same URL:

POST /E8VuD7i
__verify_submit=1&verify_code=<16 hex characters>
 
{"code":200,"message":"ok"}

The page then reloads, and the server returns the phishing kit instead of the gate. A wrong or expired token returns {"code":400,"message":"verify failed"} along with a fresh value to try next.

Why scanners stop here

The initial Sandfish capture missed the gate for two related reasons. There was no form to fill, and the icon-only control lacked the visible text used to select buttons. The successful recapture required recognizing the control's verification role. The public scans show the same stopping point, but do not expose each scanner's internal decision.

Across the 237 hostnames selected by the public query, the investigation found no capture of a later page. The query's selection limits matter: hosts without the expected title and server banner fall outside it.

What the gate hides

Once the gate opens, the server serves a Vue application under /apps/DE_PAY_01/. It fetches its configuration from /getApp?app_id=DE_PAY_01, which returns three encrypted fields.

The encryption adds work for an investigator without keeping the configuration secret from a visitor. The client bundle contains every step: AES-decrypt the third field using the first as a passphrase to recover an RSA private key, use that key to unwrap the AES passphrase, then decrypt the configuration. The AES mode is CBC with the key derived as SHA256(passphrase) and the IV taken from the first 16 bytes of the encrypted payload.

The configuration recovered from the researcher's session describes the German lure:

sn            DE_PAY_01
title         Elektronische Gesundheitskarte (eGK) | BMG
pay_amount    € 3,99
redirect_uri  https://www.bundesgesundheitsministerium.de
wss_server    wss://bmg-xgesundheitsversorgung.ink/wss

The lure is a €3,99 fee for a German electronic health card. The application registers six top-level routes:

/          landing and fee notice
/select    option or date selection
/card      card entry
/notice    interstitial for "verification required" steps
/fail      failure or retry
/finish    completion

The client state holds 40 fields. Those that matter are number, date, cvv, and pin, alongside username, password, birthday, and credit. Counted by reference, the bundle is a card funnel: 202 mentions of cvv, 131 of card_number, 63 of pay_amount.

One message string reveals a card-selection preference. reject_bin appears once in each of the 55 language packs, and it tells the victim that debit cards are unavailable and to pay by credit card instead. The client can steer users toward a particular card type. It does not tell us whether credit limits, resale value, or another constraint explains that preference.

Commands for steering the visitor in real time

The client code opens a WebSocket to /wss?uuid=<session>, defines a 30-second heartbeat, and encrypts messages with a per-session key from the configuration. It reports current_page and current_action and handles 25 server command types.

No WebSocket payloads were recovered. The following behavior comes from the client handlers, not an observed operator session or access to an operator panel. The handlers divide into three groups.

Move the victim.

CommandEffect
goto, action_jumpPush the victim to a named route
action_back, action_nextMove through history
refreshReload the page
clearWipe local state and restart
blockEject the visitor to the real government site

Demand another factor. Eight commands each open a prompt and set a paired waiting state. The internal names are Chinese:

sms       短信验证页      SMS code          -> 等待短信验证码
email     邮箱验证页      email code        -> 等待邮箱验证码
pin       PIN验证页       card PIN          -> 等待PIN码
app       App验证页       banking app       -> 等待App验证
balance   余额验证页      account balance   -> 等待余额验证
mobile    手机号码验证页   phone number      -> 等待手机号码验证
diy       临时验证页      temporary         -> 等待临时验证
custom    自定义验证页    custom            -> 自定义错误

Reject and retry. These commands could let an operator adapt the page to an authorisation result obtained elsewhere:

CommandInternal statusMeaning
rejectBin拒绝卡头"reject card prefix" — wrong card type
cardReject拒绝卡号the card failed; ask again
duplicateCard拒绝重复卡号this card was already used
smsReject, emailReject拒绝短信验证码the code was wrong; ask again
pass放行"release" — send the victim to the completion page

Separate commands for a card prefix, a duplicate card, and an incorrect code let the controller choose a specific retry instead of restarting the whole journey. This supports an interactive fraud workflow. It does not establish a live connection to a bank or a completed authorisation in the investigated session.

Opening developer tools sends you to the real ministry

The bundle includes disable-devtool, a public JavaScript library, with seven detectors enabled. It is wired to the destination supplied by the server:

Dg({url: t.redirect_uri})   // https://www.bundesgesundheitsministerium.de

A visitor who grows suspicious and opens developer tools is navigated to the genuine German Federal Ministry of Health website. We observed this directly: the detector fired, and the browser left for the real ministry 727 milliseconds later.

The redirect removes the suspicious page just as the visitor begins inspecting it. It also leaves a genuine government page on screen. We observed the navigation, not how a victim interpreted it.

Campaign scale

The preserved gate has a response hash useful for exact-copy searches. The broader investigation used a different pivot: its page title combined with the workerman server banner. That banner names a PHP socket framework. The query selects candidate related hosts; it does not itself test their response hashes:

page.title:"Verify you are human" AND page.server:"workerman"

That query returns 2,076 scans across 237 hostnames, from 11th May to 6th October 2026.

The themes below are inferred from hostnames. Because those scans stopped at the gate, they do not confirm the branding or payment flow on each hidden page.

Hostname themeHostnamesExample
Australia Post177auspost.addqwi.com
myGov and Medicare22my.gov.medicarecarq.com
Irish HSE10hse-renewals.com
Australian Taxation Office2ato.gov.individual-tax.com
Parking2uniparkpay.shop
Qantas1qantas.addqwf.com
Unlabelled23coreacori.click

Hostnames are mass-produced from a few registrable stems. The addq* family carries most of the corpus, with the brand word pushed into the subdomain, as in auspost.addqwi.com and auspost.addqwe.com. A second family, coerr* and core*, carries no brand word at all. The sequence addqwe.com, addqwg.com, addqwh.com, addqwp.com, addqwr.com, addqws.com, and addqwt.com is consistent with related provisioning. Similar names alone cannot distinguish one operator from several customers using a shared supplier.

Public scan volume fell in midsummer and rose again in September. Submission counts also depend on discovery and scanner activity, so this is not a measure of victim traffic:

MonthScans
May691
June670
July66
August111
September522
October (to the 6th)16

The records include Alibaba Cloud and Tencent Cloud addresses. One Alibaba address was associated with 206 hostnames from May to September; Tencent addresses also appeared, with overlapping observation windows. This supports infrastructure diversity, but does not by itself prove a coordinated migration.

AddressProviderHostnamesWindow
47.79.39.190Alibaba Cloud20611th May - 30th Sep
43.165.7.184Tencent Cloud1129th Aug - 29th Sep
43.165.68.243Tencent Cloud1028th Sep - 5th Oct
43.166.217.161Tencent Cloud22nd Jul - 28th Jul
43.157.40.45Tencent Cloud24th Sep - 26th Sep

The German host, bmg-xgesundheitsversorgung.ink, sits behind Cloudflare and does not expose the same banner. It shows why a banner-based search misses some related deployments. One such deployment does not establish a sequence of platform hardening.

One build, many lures

The German health-ministry deployment uses a multilingual build. It ships 55 complete language packs in a single file, from Albanian to Traditional Chinese, including Tajik, Sinhala, and Swahili.

It also ships lures the German lure never uses. Alongside the health-card text are complete traffic-fine and toll templates, with ready-made strings for speeding, for using a phone while driving, for an unpaid toll, and for the licence suspension that follows non-payment. There are parcel-delivery and loyalty-points templates too.

The government branding is interchangeable dressing over one payment flow. The brand names used in the corpus hostnames include Australia Post, myGov, Medicare, the Australian Taxation Office, the Irish HSE, and Qantas: high-trust services that contact large numbers of ordinary people, where an unexpected small fee is plausible. Their broad consumer use fits a reusable small-fee lure, although we have no delivery data establishing who received it.

A €3,99 pretext brings the visitor into a flow capable of collecting reusable card data and requesting additional codes. The amount shown on the page does not bound the potential misuse of that data.

Shared software, unresolved ownership

Two details suggest reusable deployment infrastructure, but neither proves that it was rented to customers.

The application uses configurable identifiers. The German case ran DE_PAY_01. A Dutch tax-authority typosquat, belastingdlienst.icu, ran NL_PAY_05 through the same getApp endpoint. Neither host appears in the 237 above, because both sit behind Cloudflare and return no workerman banner. The query therefore undercounts known deployments. Application identifiers are not customer or operator counts.

That Dutch host shared an address, 43.156.109.100, with ddbsso.com, which urlscan recorded 111 times serving a page titled RuoYi-Vue-Plus多租户管理系统, "RuoYi-Vue-Plus multi-tenant management system", with admin., api., demo., and staging. subdomains. Multi-tenancy fits a platform serving several customers. RuoYi is a widely used open-source framework with hundreds of unrelated instances in public scan data, so the framework alone proves nothing. The shared address is a lead worth recording, but shared hosting is also a plausible explanation. No recovered request or configuration links the phishing client to that management system. The panels were offline when checked.

How this compares

Darcula provides a useful comparison for what has and has not been observed. Its documented operator-side features go beyond the victim-side bundle recovered here.

Netcraft first described Darcula in March 2024 across more than 20,000 domains and 11,000 IP addresses, targeting over 100 brands in 100-plus countries. Those figures describe Netcraft's March 2024 dataset, not a directly comparable measure of this investigation's reach.

Darcula (reported)This platform (observed)
Infrastructure counted20,000+ domains reported237 hostnames in a narrower query; not a like-for-like total
Operator controlAdmin functionality documentedClient command handlers recovered; no operator panel inspected
Delivery vectorSMS, RCS, iMessagenot observed
Card-to-wallet provisioningyesno markers found

The clearest difference is what the investigations could inspect. Netcraft documented Darcula's admin panel generating an image of the stolen card so it can be loaded into a digital wallet, after which the loaded phones are sold on. That is a fencing operation: the card becomes a reusable asset.

We found no wallet-provisioning markers in the recovered client. Its pass and rejection handlers support an interactive approval-and-retry flow, but that does not reveal the eventual cash-out method. Wallet provisioning could happen in a separate operator system. The absence of that code on the victim's page cannot rule it out.

The shared traits with that ecosystem are a Chinese operator interface, Alibaba and Tencent hosting, workerman, delivery and toll lures, and client support for real-time control. Those traits are common across unrelated tools and cannot attribute this software to Darcula. The investigation found no shared code or infrastructure establishing such a link.

What it took to see any of this

Sandfish's first capture of the German page stopped at the gate, like the public scans reviewed for this investigation. The fix was to teach the capture to press the control when three conditions hold together: the page demands verification in a single phrase, the page is short enough to be an interstitial rather than real content, and it offers a control that states a verification role through its markup, its accessible name, or a checkbox shape. At the time of this investigation, that imitation-gate handler excluded pages carrying a real challenge-provider frame. This describes the capture change tested then, rather than the current handling of every challenge type.

The historical before-and-after capture comparison:

BeforeAfter
Interaction actions01
Requests212
JavaScript functions84,073
Technologies48

The same press was accepted on two further domains, tigocaqci.help and coreacori.click, each returning {"code":200,"message":"ok"}. Both then reached a Cloudflare Error 300 origin timeout. That confirmed gate acceptance, not a successful capture of their payment flows.

Indicators

Gate fingerprint

page size      22882 bytes
sha256         2b10e03cc23363ac9ebcdeb211cf98b0d1fbf5824836b17b0306209d1e108cc0
control        <button class="checkbox" id="verifyTrigger" aria-label="点击验证">
post body      __verify_submit=1&verify_code=<16 hex>
success        {"code":200,"message":"ok"}
server banner  workerman

Platform endpoints

/getApp?app_id=<APP_SN>        encrypted application configuration
/wss?uuid=<session>            AES-encrypted operator channel
/apps/<APP_SN>/assets/js/      application bundle
https://ipapi.co/json/         visitor geolocation

Observed application identifiers: DE_PAY_01, NL_PAY_05.

Hosts

bmg-xgesundheitsversorgung[.]ink
belastingdlienst[.]icu
tigocaqci[.]help
coreacori[.]click
corepeimci[.]click
hseaz[.]top
hse-renewals[.]com
healthcard-hse[.]com
services-hse[.]com
auspost[.]addqwi[.]com
qantas[.]addqwf[.]com
my[.]gov[.]medicarecarq[.]com
ato[.]gov[.]individual-tax[.]com
uniparkpay[.]shop
47[.]79[.]39[.]190     Alibaba Cloud
43[.]165[.]7[.]184     Tencent Cloud
43[.]165[.]68[.]243    Tencent Cloud
43[.]166[.]217[.]161   Tencent Cloud
43[.]157[.]40[.]45     Tencent Cloud
43[.]162[.]122[.]44    Tencent Cloud
43[.]156[.]109[.]100   Tencent Cloud

Pivot

page.title:"Verify you are human" AND page.server:"workerman"

ddbsso[.]com is an unresolved co-hosting lead discussed above, not a confirmed phishing host. Shared provider addresses also require a date and hostname before they can support a campaign link.

Method and limits

Updated on 9th October 2026: we narrowed earlier claims about a rental model, operator counts, infrastructure migration, and cash-out. The recovered client supports the capabilities described here; it does not establish those business or attribution conclusions. We also clarified that two additional gate acceptances ended in origin timeouts.

The initial capture and recapture document the change in capture coverage. Configuration and command-handler findings came from the researcher's preserved browser session. Claims about customer counts, a rental business, and a specific cash-out method are not established by those artifacts.

Sandfish preserved the captures, request records, screenshots, and WACZ archives for the German case and the two further domains. The session that reached the kit's configuration was produced in a normal browser on a researcher's machine. The gate image above was rendered from that preserved page with network access blocked. Campaign scope comes from public urlscan records.

Several limits apply to the conclusions.

We recovered no WebSocket frames. The command set is read from the client bundle, so the platform's capability is established, but its use in any specific case is not. Firefox does not retain WebSocket payloads in HAR exports.

The pivot undercounts. It finds hosts that expose a workerman banner, and both confirmed application identifiers sat behind Cloudflare instead. The 2,076 figure also counts urlscan submissions, not victims. It measures the captures selected by this query, not total campaign reach or universal scanner resistance.

Session keys, UUIDs, and verification tokens are per-session. They identify nothing beyond the session that produced them.

The Chinese interface strings, the workerman banner, and the hosting choices do not establish where the software was written. The Chinese strings suggest a Chinese-speaking development context. None of them identifies an operator, and hosting location does not establish nationality. The recovered payment flow supports a card-fraud interpretation. Government branding alone does not establish an espionage target or motive, and a client bundle cannot establish the limits of an operator's wider activity.

During the investigation the platform began refusing our analysis host and one research address. Repeated probing likely caused that, so later negative results from those addresses should not be read as the campaign ending. At the time of writing, the German lure still served its gate.

One earlier conclusion in this investigation was wrong. We briefly assessed that the kit had been withdrawn, on the basis of replaying an expired verification token, which can only fail. A full browser session corrected it. We record the error because the same mistake is easy to repeat against any kit that rotates a token.