Introduction: a smishing campaign at scale

A widespread smishing campaign was identified in which victims received fraudulent SMS messages impersonating official entities and were instructed to click a link inside the SMS in order to “complete a verification“, “settle an outstanding fee”, or “re-confirm delivery details”. The link resolved through a short URL into a disposable apex domain hosting the phishing kit analysed in this writeup. The kit then walked the victim through a multi-stage credential capture funnel  identity, card, OTP, sometimes a second bank, sometimes a wallet  while a human operator on the back end watched the session in near-realtime and pushed control instructions as needed.

This delivery pattern SMS-pretexted, short-link redirection, ephemeral landing domain, AES-encrypted WebSocket exfiltration, real-time operator handling  is the signature of a single criminal ecosystem that the security community has, over the past two years, come to call the Smishing Triad. Group-IB’s previous blog, “Phoenix Rising: Exposing the PhaaS Kit Behind Global Mass Phishing Campaigns”, as well as numerous public reporting (Silent Push, Palo Alto Unit 42, Resecurity, Fortra) places this collective at well over 194,000 malicious domains since 2024 spanning 121+ countries, with industry estimates of cumulative criminal revenue in the multi-billion-dollar range. The Triad is not a single team but a marketplace: kit developers sell their builds to operator crews; phone-list brokers sell target inventories; spammers run the SMS gateways; domain sellers and hosting providers rotate the infrastructure on a near-daily cadence; liveness scanners qualify the leads; blocklist scanners burn and replace whatever Cloudflare nameservers flag.

This article is the deep technical post-mortem of one specific kit deployed by one operator cluster inside that marketplace. We refer to the cluster as Outsider as a working tracking name. Where the analysis crosses from “this kit” into “this ecosystem”, we say so explicitly.

The artefacts analyzed are the kit’s full client-side bundle (recovered live during the campaign), the kit’s WebSocket worker, the kit’s auto-validation module, the lettered set of HTML phishing pages, a bundled Axios distribution, and a captured getSyncSettings encrypted response which we decrypt end-to-end.

Key Discoveries

  • A widespread smishing campaign of fraudulent SMS messages impersonating official entities with links leading to a phishing kit hosted on disposable infrastructure.
  • The landing infrastructure is a polished Vue 2 single-page application wired to a dedicated Web Worker that owns a binary WebSocket channel and a 2-second HTTP long-poll fallback, both encrypted with an AES-256-CTR envelope that prepends a random per-message key.
  • Strings inside the worker reference the trsb.top family, which is how this codebase is known in operator circles.
  • The kit identifies itself, in every artifact we recovered, as JWR, a name burned into localStorage keys, CSS class prefixes, and per-victim identifiers.
  • The kit is the front-end half of a real-time fraud cockpit: every keystroke is streamed to the operator, who can pivot the victim mid-session to any of 32 named pages or in-page mutations  SMS-OTP capture, app-confirmation stalls, QR-code swaps, “card declined, please try another” challenges, and so on.
  • Operationally, Group-IB attributes the campaign to a cluster we track as Outsider, an active customer of the broader smishing-as-a-service ecosystem documented publicly as the Smishing Triad.
  • A full reverse of one captured response: the encrypted /api/open/getSyncSettings body recovered byte-for-byte from a 32 KB octet-stream into clean JSON, and the IOCs/YARA/Suricata that fall out of the analysis.

Who may find this blog interesting:

  • Cybersecurity analysts and corporate security teams
  • Threat intelligence specialists
  • Cyber investigators
  • Computer Emergency Response Teams (CERT)
  • Law enforcement investigators
  • Cyber police forces

Group-IB Threat Intelligence Portal: Outsider

Group-IB customers can access our Threat Intelligence portal for more information about the Outsider threat actor and malware described in this blog:

Threat Actor Profile_ChenLun_Outsider

JWR_Malware Profile

The Outsider Threat Actor

Within the Smishing Triad’s PhaaS marketplace, individual operator crews are difficult to distinguish from one another at the network layer; the kits, encryption, and brand catalogs are largely shared. What separates one crew from another is the brand surface they choose to impersonate, the TLDs and short-link providers they rotate through, the operator working hours, and the handlers’ Mandarin dialect signatures visible in operator-only fields shipped to the client.

Group-IB tracks the cluster behind the campaign analysed here as Outsider, on the basis of the following observable separators:

  1. Brand-surface specialization. The recovered build’s cfg.projectName is set to a regional template tag, and the kit’s belong_to_template field  populated by cfg.projectName and shipped on every exfiltration packet categorises this operator’s campaigns into a distinct sub-tree of the broader Triad catalog.
  2. Operator-language fingerprint. Operator-only status strings in the bundle are in Simplified Chinese with consistent terminology (卡头 for BIN, 无人值守 for “unattended”), and several status strings include trailing exclamation marks (需操作!) and full-width punctuation (!) that are reproducibly used by the same hand across builds.
  3. Hosting tradecraft. Like the broader Triad, the infrastructure is registered through a Hong Kong registrar and fronted by U.S. cloud nameservers for resilience, with domains active for two days or less in the majority of cases at a churn rate consistent with public reporting on the Triad and inconsistent with one-off phishing operations.
  4. Operator workflow signature. Three operator-side toggles in the synced settings (luhn_check, unattended_switch, rejectd_card/rejectc_card) are configured to the same values across distinct Outsider builds. The configuration vector is, in itself, a soft fingerprint.

Outsider is therefore best understood as a sub-cluster of the Smishing Triad ecosystem rather than an independent operator. The kit they ship is the JWR family discussed in detail below. You can read more about a connected Outsider PhaaS operation in a previous report.

Attack Chain and Kit Family Identification

Figure 1. End-to-end attack chain of the Outsider smishing operation from SMS pretext through operator-driven exfiltration.

Figure 1. End-to-end attack chain of the Outsider smishing operation from SMS pretext through operator-driven exfiltration.

The kit identifies itself, in every artifact we recovered, as JWR. The name is burned into:

  • localStorage keys: JwrCvvForm, JwrCustomCvvForm, JwrIpInfo, JwrSubmittedCardNumbers, JwrSelectedPaymentMethod, JwrControlInstruction, JwrExtraJSON, JwrAjaxUrl, JwrNonce, JwrCustomEmailorPhone, JWR_IFRAME_ACTIVE, jwrisThankPage, JwrIsShopify, JwrIsWordPress.
  • CSS class and DOM id prefixes: jwrGlobalErrorPopup, jwrErrorAnimationStyle, jwrErrorSlideIn.
  • Per-victim id format: JWRCVV-{Date.now()}-{rand36}-{rand36}, persisted as JWRCID.

Strings inside the worker refer to the codebase as the trsb.top family, which is how this build line is known on operator forums. The two names refer to the same code in different audiences (JWR in source, trsb.top in deployment notes).

The kit is brand-agnostic: the storage keys, REST surface, encryption envelope, project-name conventions, and operator-side status strings remained consistent across multiple unrelated impersonation skins observed in the wild. For this reason, Group-IB treats JWR as the kit family, not as a single campaign.

High-level Architecture

Figure 2. Component-level architecture of the JWR kit.

Figure 2. Component-level architecture of the JWR kit.

The design separates concerns sharply: the Vue layer handles UI, validation, and page transitions; the Worker exclusively owns the AES-256-CTR encryption envelope (48-byte header shown explicitly above), and the three transport channel–the WebSocket, heartbeat, and long-poll–that converge on the operator’s C2 cockpit. They communicate only via postMessage. Keys never leave the worker scope, which provides modest defence against naive console-based incident response and makes the transport substitutable without touching the Vue code.

The cvvform Data Model: Credential Harvester

The single object that drives the entire kit is cvvform, declared verbatim at the top of main.js. It has ~70 fields. The declaration is the cleanest single artefact in the observed bundle:

const _0x1570c0 = {};
_0x1570c0.id = '';
_0x1570c0.firstname = '';
_0x1570c0.lastname = '';
_0x1570c0.fullname = '';
_0x1570c0.country = '';
_0x1570c0.state = '';
_0x1570c0.city = '';
_0x1570c0.postcode = '';
_0x1570c0.address1 = '';
_0x1570c0.address2 = '';
_0x1570c0.phoneNumber = '';
_0x1570c0.email = '';
_0x1570c0.email_pwd = '';
_0x1570c0.two_factor_authentication = '';
_0x1570c0.gender = '';
_0x1570c0.ssn = '';
_0x1570c0.dob = '';
_0x1570c0.passport_number = '';
_0x1570c0.license_number = '';
_0x1570c0.medical_number = '';
_0x1570c0.ssn_img = '';
_0x1570c0.ssn_handheld_img = '';
_0x1570c0.passport_img = '';
_0x1570c0.passport_handheld_img = '';
_0x1570c0.license_img = '';
_0x1570c0.license_handheld_img = '';
_0x1570c0.medical_img = '';
_0x1570c0.medical_handheld_img = '';
_0x1570c0.cvv_fullName = '';
_0x1570c0.cvv_cardnumber = '';
_0x1570c0.cvv_expiry = '';
_0x1570c0.cvv_cvv = '';
_0x1570c0.cvv_frontImg = '';
_0x1570c0.cvv_backImg = '';
_0x1570c0.cvv_handheld_img = '';
_0x1570c0.cvv_brand = '';
_0x1570c0.cvv_type = '';
_0x1570c0.cvv_category = '';
_0x1570c0.cvv_issuer = '';
_0x1570c0.cvv_country = '';
_0x1570c0.cvv_pin = '';
_0x1570c0.ip = '';
_0x1570c0.device = '';
_0x1570c0.language = '';
_0x1570c0.timeZone = '';
_0x1570c0.userAgent = '';
_0x1570c0.cookie = '';
_0x1570c0.source = '';
_0x1570c0.extraJSON = '';
_0x1570c0.web_login_account1 = '';
_0x1570c0.web_login_pwd1 = '';
_0x1570c0.web_login_type1 = '';
_0x1570c0.web_login_account2 = '';
_0x1570c0.web_login_pwd2 = '';
_0x1570c0.web_login_type2 = '';
_0x1570c0.web_login_account3 = '';
_0x1570c0.web_login_pwd3 = '';
_0x1570c0.web_login_type3 = '';
_0x1570c0.paypal_login_account = '';
_0x1570c0.paypal_login_pwd = '';
_0x1570c0.operator_by = '';
_0x1570c0.update_time = '';
_0x1570c0.state1 = '';
_0x1570c0.state2 = '';
_0x1570c0.state3 = '';
_0x1570c0.operational_status = '';
_0x1570c0.current_page = '';
_0x1570c0.online_status = '';
_0x1570c0.custom_phone = '';
_0x1570c0.custom_email = '';
_0x1570c0.custom_news = '';
_0x1570c0.operation_code = '';
_0x1570c0.release_code = '';
_0x1570c0.latitude = '';
_0x1570c0.longitude = '';
_0x1570c0.card_submission_type = '';
_0x1570c0.belong_to_template = '';
_0x1570c0.custom_form_data = '';

Grouped by purpose:

  • Identity & PII (full KYC coverage): firstname, lastname, fullname, country, state, city, postcode, address1, address2, phoneNumber, email, gender, dob, ssn, passport_number, license_number, medical_number.
  • Document imagery: *_img / *_handheld_img pairs for SSN, passport, driver’s licence, and medical ID.  The _handheld_img suffix is the threat-actor’s term for the “selfie holding ID” image used to defeat KYC re-verification.
  • Card data: cvv_* group of 13 fields, including front/back/handheld images and a server-side enrichment block (cvv_brand, cvv_type, cvv_category, cvv_issuer, cvv_country).
  • Web-account credentials, three slots: web_login_account{1,2,3} / web_login_pwd{1,2,3} / web_login_type{1,2,3}. The slot design supports primary bank → secondary bank → brokerage chaining in a single session, consistent with public Fortra reporting on a fivefold YoY rise in brokerage-targeting smishing attacks.
  • PayPal sub-track: a dedicated mini-funnel with paypal_login_account / paypal_login_pwd and a separate set of pages.
  • Telemetry & device fingerprint: ip, device, language, timeZone, userAgent, cookie, latitude, longitude, plus an extraJSON for plugin-mode product/cart data.
  • Operator metadata: operator_by, update_time, state{1,2,3}, operational_status, current_page, online_status, operation_code, release_code, card_submission_type, belong_to_template, custom_form_data.

The belong_to_template field populated from cfg.projectName is the kit’s self-tag for operator-side classification, and is the single best attribution signal in the family.

Source Code Analysis

The Vue layer (main.js)

The application bundle is shipped through javascript-obfuscator.io with string-array shuffling and dictionary indirection enabled. After resolving the string array (4,169 lookups in the recovered build), the structure becomes legible. The created() / mounted() hooks of the root Vue instance bootstrap the kit in a fixed order.

Per-victim id assignment (initCreateId):

Figure 3. Per-victim id assignment code snippet.

Figure 3. Per-victim id assignment code snippet.

Four-provider geolocation rotation (getIPInfo → requestIpAddress):

Figure 4. Four-provider geolocation rotation code snippet.

Figure 4. Four-provider geolocation rotation code snippet.

The Chinese error string 所有IP API请求均失败 (“all IP API requests failed”) is a recurring family marker that survives across builds.

WebSocket URL assembly with hardcoded access-token suffix (initWebSocket):

Figure 5. WebSocket URL assembly code snippet.

Figure 5. WebSocket URL assembly code snippet.

Analysis of the WebSocket initialization routine reveals a significant family marker: the trailing literal /khkjsahfjkwhakjlsdwdddddd88 is hardcoded across every recovered build. This string serves as an obscurity-as-security access token required to gate the WebSocket endpoint, making it a high-confidence Indicator of Compromise (IOC) for identifying JWR-family infrastructure.

Plugin-mode awareness. The kit inspects sessionStorage for JwrIsShopify and JwrIsWordPress markers, and when present, switches into a host-platform integration path: for WordPress/WooCommerce it actually calls wc_gateway_complete_order via the host’s AJAX endpoint and nonce; for Shopify it harvests cart data from URL parameters (?cart_data=…). The JWR engine therefore also ships as a hostile plugin or theme injection, not just as standalone phishing pages a TTP that significantly broadens its blast radius.

The WebSocket worker (ws-worker.js)

The worker is an 18 KB obfuscated file whose deobfuscated form is roughly 815 lines. It contains four logical units.

1. Timing constants:

Figure 6. Timing constants code snippet.

Figure 6. Timing constants code snippet.

2. Message router:

Figure 7. Message router code snippet.

Figure 7. Message router code snippet.

3. HTTP long-poll fallback:

Figure 8. HTTP long-poll fallback code snippet.

Figure 8. HTTP long-poll fallback code snippet.

4. Command interface:

Figure 9. Command interface code snippet.

Figure 9. Command interface code snippet.

The visibility command forwards document.visibilityState over the wire the operator knows whether the victim has tabbed away, useful for timing the manual push of a verification challenge.

The AutoValidator module (check.js)

The auto-validator is, in our view, the single most useful artefact for clustering JWR deployments across campaigns. It is a self-contained class of approximately 800 lines that:

Auto-discovers fields by DOM-id suffix:

Figure 10. Auto-validator code snippet.

Figure 10. Auto-validator code snippet.

Ships a 25-validator regex library covering 12+ jurisdictions:

Figure 11. Auto-validator regex library code snippet.

Figure 11. Auto-validator regex library code snippet.

The validator dictionary’s exact composition is, to our knowledge, unique to this family. A grep against any captured client-side bundle for the co-occurrence of personnummer, fodselsnummer, codicefiscale, and germanid is high-confidence JWR fingerprinting.

The module also implements live formatting (auto-spaces in card numbers, / in MM/YY expiries, hyphenation in identity numbers) and submit-button enable/disable based on a 100 ms setInterval poll over all watched fields the buttons themselves are matched by the -button suffix on their DOM id. Pages with id="*-button" controls and that exact polling cadence are JWR-shaped.

The HTML funnel

File Phishing Stage
a_index.html, a_login.html, a_shop.html Landing / initial login / plugin storefront
b_password.html, b_info.html, b_qrverify.html Password / PII / QR-verification
c_pay.html Card data capture
d_sms.html, d_sms_login.html, d_sms_bank.html, d_2fa.html, d_verify.html, d_thank.html SMS/2FA/verification variants
e_email.html Email-OTP capture
f_pin.html Card PIN capture
g_app.html, g_login_app.html “App-based confirmation” stalling page
h_bank_login{1,2,3}.html, h_loading.html, h_paypal_login.html Bank-credential capture + loading + PayPal entry
i_paypal_select_verify.html PayPal verification method picker
j_paypal_verify.html, k_paypal_card.html, l_paypal_pin.html, m_paypal_app.html PayPal sub-funnel
z_thank.html Funnel-termination redirect

A lettered set of pages was observed in the recovered deployment. The single-letter prefix acts as a stage code, and the operator panel pushes navigation by this stage code rather than by URL. The misspelling payal in i_payal_select_verify.html and the _payal_* instruction keys is a durable fingerprint that survives across campaigns and is baked into the panel’s instruction tables.

The Encrypted Format: AES-256-CTR With a Prepended Key

Both the WebSocket binary frames and the HTTP application/octet-stream request/response bodies on /api/open/* use a single envelope:

Offset Length Field
0x00 32 B AES-256 key (random per message, in clear)
0x20 16 B AES-CTR initial counter / IV
0x30 N B AES-256-CTR ciphertext of UTF-8 JSON

The encryption is performed in the worker’s WorkerCrypto.encrypt:

Figure 12. AES-256-CTR encryption code snippet.

Figure 12. AES-256-CTR encryption code snippet.

and reversed in decrypt:

Figure 13. Decryption code snippet.

Figure 13. Decryption code snippet.

The transport-routing decision lives in WorkerCrypto.post, which also reveals an artefact of development:

Figure 14. Transport-routing decision code snippet.

Figure 14. Transport-routing decision code snippet.

The defining feature of this design is that the AES key is shipped in clear, prepended to every ciphertext. There is no key agreement, no asymmetric wrap, no shared secret; the “encryption” provides no real confidentiality against any party who can read the wire. The author appears to have selected this format to defeat naive logging and traffic-mirroring: the ciphertext does not contain JSON-shaped strings, the volume is binary, and a casual tcpdump or proxy log will not show any of the harvested data in plaintext. For defenders this is a gift: the 48-byte header is fixed-shape and trivially detectable, and any captured envelope can be decrypted with three lines of Python.

Cracking getSyncSettings: A Decryption Walkthrough

This is the section most useful to incident responders. We walk end-to-end from a captured opaque binary response to a clean JSON document containing the operator’s per-deployment configuration. The same procedure works against any /api/open/* body and against every binary frame on the /webSocket/QT/... channel.

What we captured

A POST is fired from the kit during page bootstrap:

POST /api/open/getSyncSettings HTTP/1.1
Host: 
Content-Type: application/octet-stream
Content-Length: 32809
Origin: https://
<32,809 bytes of binary>

The response body (getSyncSettings in our case bundle) is ~32 KB of pure binary. there is no UTF-8 substring of any length, and strings return nothing. A first-time analyst would correctly conclude the body is either compressed, encrypted, or both.

Hexdumping the first 64 bytes:

$ xxd -l 64 getSyncSettings.txt
00000000: 9852 3cf6 7337 cf98 c532 e4ee 6b63 90ef  .R<.s7...2..kc..
00000010: a3ad 8842 3eda 5dac 01ff 0a0d 0d51 7b88  ...B>.]......Q{.
00000020: 8af8 80cd ba99 c12d 4eaf 0b75 5d0e 0c0d  .......-N..u]...
00000030: 4e92 4676 c233 87df 4f95 77fa e3c1 257e  N.Fv.3..O.w...%~

There is no recognisable file magic. Entropy is uniformly high across the whole file. There is no compression header. The body is therefore either an opaque blob keyed off the URL (uncommon), or  far more likely encrypted under a scheme that ships its key in-band.

Identifying the wire format from the client

The pivot is the client-side WorkerCrypto.encrypt function. Reading it tells us the kit’s transport format in one paragraph:

  1. The plaintext is JSON.stringify(body), encoded as UTF-8.
  2. The cipher is AES-256-CTR, key Uint8Array(32), IV Uint8Array(16), length 64.
  3. The wire shape is key (32) || iv (16) || ciphertext.

Since WorkerCrypto.decrypt is symmetric and reads the same wire shape, we have everything we need to decrypt. Crucially, the key is prepended to the ciphertext in clear; there is no out-of-band key material.

Writing the decryptor

A minimal Python implementation using the cryptography library:

#!/usr/bin/env python3

from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
import json, sys
from pathlib import Path

ENVELOPE_HEADER = 48   # 32-byte key + 16-byte IV

def decrypt_envelope(blob: bytes):
    if len(blob) < ENVELOPE_HEADER + 1:
        raise ValueError(f"input too small ({len(blob)} B)")

    key, iv, ct = blob[:32], blob[32:48], blob[48:]
    
    pt = Cipher(algorithms.AES(key), modes.CTR(iv)) \
        .decryptor().update(ct)
    
    return key, iv, pt

if __name__ == "__main__":
    for path in sys.argv[1:]:
        blob = Path(path).read_bytes()
        key, iv, pt = decrypt_envelope(blob)
        
        print(f"[+] {path}", file=sys.stderr)
        print(f"    key (hex)  : {key.hex()}", file=sys.stderr)
        print(f"    iv  (hex)  : {iv.hex()}",  file=sys.stderr)
        print(f"    plaintext  : {len(pt)} B", file=sys.stderr)
        
        print(json.dumps(json.loads(pt), indent=2, ensure_ascii=False))

Running it against the captured response:

$ python3 jwr_decrypt.py getSyncSettings.txt > settings.json
[+] getSyncSettings.txt
    total bytes  : 32809
    key (hex)    : 9852 3cf6 … (32 bytes)
    iv  (hex)    : 8af8 80cd … (16 bytes)
    plaintext    : 32761 B

The 32,761-byte plaintext is valid UTF-8 JSON. We have unwrapped the operator’s per-deployment configuration.

What the decrypted plaintext contains

The recovered JSON has the shape (field names verbatim from how main.js references this.syncSettings.*; values redacted/abbreviated):

Figure 15. The decrypted JSON output.

Figure 15. The decrypted JSON output.

The structural fields above (luhn_check, unattended_switch, rejectd_card, rejectc_card, the BIN lists, the custom-error block) are confirmed from main.js call sites. The remaining metadata is operator-side and varies by build.

The behavioral switches are the most interesting to defenders. The kit’s card-handling logic in jwrCvvFormSubmit is gated entirely on these flags:

Figure 16. Card handling logic code snippet.

Figure 16. Card handling logic code snippet.

The sync-settings response therefore acts as a server-driven feature flag set for the kit. Operators can change behavior live without redeploying the front-end. A defender who decrypts one such response gains visibility into the operator’s economic preferences: which BINs they have already tested and found profitable (whitelist), which BINs they have learned not to touch (blacklist likely either fraud-monitored issuers or BIN ranges with reliable 3-D-Secure step-up), whether the campaign is running in autonomous or human-driven mode at that moment, and whether the operator is currently chasing debit, credit, or both.

The same jwr_decrypt.py script also decrypts:

  • The WebSocket binary frames sent by WorkerCrypto.encrypt and received by WorkerCrypto.decrypt (carved out of a PCAP via the Sec-WebSocket-Key/Sec-WebSocket-Accept handshake and binary opcodes).
  • The HTTP request bodies on /api/open/addCvv, /api/open/the_final_interface, /api/open/updateStatus, /api/open/getPendingInstruction, and /api/open/binLookup.
  • The HTTP response bodies on those same endpoints (responses are encrypted with a different random key per message and shipped in the same wire shape).

C2 Endpoint Surface

The kit’s REST surface is declared verbatim in the bundle:

Figure 17. REST surface declaration.

Figure 17. REST surface declaration.

Endpoint Direction Purpose
POST /api/open/addClick Client → C2 Victim arrival beacon
POST /api/open/getSyncSettings Client → C2 Pull operator configuration
POST /api/open/addCvv Client → C2 Primary exfiltration sink
POST /api/open/the_final_interface Client → C2 Funnel completion notification
POST /api/open/updateStatus Client → C2 Operator-driven status updates
POST /api/open/getPendingInstruction Client → C2 HTTP long-poll command target
POST /api/open/binLookup Client → C2 Real-time BIN verification
WSS /webSocket/QT/<JWRCID>/… Bidirectional Real-time C2 channel

The /api/open/ prefix is a strong family signal; it is consistent across every recovered JWR deployment and is carved into the kit’s compiled source. The /webSocket/QT path, combined with the trailing /khkjsahfjkwhakjlsdwdddddd88 access-token suffix, is similarly consistent. Together with the encryption envelope, these URL fragments alone are sufficient for confident family attribution from network telemetry.

The addCvv endpoint deserves special note: the kit calls it not only on form submissions, but also on every keystroke-level change to a watched field (synchronousDataInputHandle() pushes a fresh cvvform to the panel as the victim types). Combined with the long-poll, this creates a near-realtime shared state where the panel sees what the victim is typing before they hit submit, and can choose to inject a verification prompt at any time.

Real-time Operator Control Plane

The Vue layer maintains an instructionConfig map binding 32 operator command names to handler behavior. The structure of each entry is {defaultPage, needs*, persistent} for navigation, or {handler, persistent} for in-page mutations:

Figure 18. Mapping operator command names to handler behavior.

Figure 18. Mapping operator command names to handler behavior.

What this map tells you about the intended workflow is precise: a human handler watches the victim’s stream in near-real-time and dispatches one of these commands as the situation demands. The victim is captured at c_pay; the operator sees the BIN lookup, decides which OTP flow to mimic, dispatches to_sms_bank; sees the OTP arrive in their panel; completes the fraudulent transaction; dispatches to_success and the victim is redirected away, believing the transaction completed normally.

When syncSettings.unattended_switch === '1', the kit makes its own decisions: whitelist BIN + Luhn pass → push into the OTP flow; blacklist BIN or Luhn fail → silently kill the session and redirect. This is the “spray and pray” mode used during high-volume campaigns when manual handling is the bottleneck.

Anti-analysis Layer

The kit’s anti-analysis defences are layered but not heavy. In ascending order of nuisance:

Layer 1:  obfuscator.io string-array shuffle. Standard output of the named tool. The string array is shuffled at runtime via a constant rotation count and is resolvable with a one-pass AST walker. We resolved 4,169 references in the bundle automatically.

Layer 2:  obfuscator.io debugger trap. The _0x232d28 self-test class triggers an infinite recursion if Function.prototype.toString reports a wrapped function in the lookup chain:

Figure 19. Anti-analysis layer 2 code snippet.

Figure 19. Anti-analysis layer 2 code snippet.

Layer 3: Self-integrity probe with catastrophic-backtracking regex.

Figure 20. Anti-analysis layer 3 code snippet.

Figure 20. Anti-analysis layer 3 code snippet.

Layer 4: WorkerCrypto.DEV_MODE flag. Hardcoded false in production; if flipped at runtime, swaps the encrypted transport for plaintext JSON. The presence of the DEV_MODE branch in production binaries is itself a development-pattern fingerprint.

Layer 5: Plugin-mode sandboxing. When running inside a hostile WordPress or Shopify plugin, the kit checks window !== window.top and uses postMessage to communicate with a parent popup window (closePopupWindowz, closePopupAndRedirect). Naive iframe-busting analyses miss the full flow.

There is no server-side gating that we could observe (no anti-bot challenge before serving the kit, no IP allow/deny at the static-content layer beyond standard CDN noise), no VM detection, and no client-side packing beyond the JS obfuscator. The operators rely on volume and on ephemeral domain rotation rather than on technical evasion entirely consistent with public Triad reporting on a 2-day median domain lifetime.

Development Patterns & Operator Fingerprints

Chinese operator-side status strings. The operational_status field is populated with natural-language descriptions of the victim’s current state visible to operators and baked into the client-side bundle, so they ship to the victim as well:

Figure 21. Chinese-language operator-side status strings.

Figure 21. Chinese-language operator-side status strings.

The terminology 卡头 (literally “card head”, the local term for BIN) and the consistent use of 需操作! (“action required!”) are strong dialectal signals.

The JWR prefix and JWRCVV- id format. Every storage key, every CSS class, every id generator uses this prefix. We have seen no campaign in this family that drops it.

The cvvform field name. A holdover from an earlier, simpler version of the kit that only harvested cards; now used for everything including SSN and email passwords. The naming has fossilised.

The payal typo. Preserved across builds in i_payal_select_verify.html and the operator instruction keys to_paypal_sms/to_paypal_email. The page-name typo doesn’t match the command-name spelling, strong evidence the typo entered the codebase early and was never fixed because doing so would require renaming the panel-side instruction tables.

Bundled axios distribution shipped as a static asset rather than from a CDN. The bundle’s metadata provides a developer-fingerprint anchor.

Project-name templating:

Figure 22. Project-name templating code snippet.

Figure 22. Project-name templating code snippet.

projectName is then attached to every exfiltration packet via the belong_to_template field, set during initCvvForm():

Figure 23. Project name is attached to every exfiltration packet.

Figure 23. Project name is attached to every exfiltration packet.

Defenders fingerprinting belong_to_template values across captured exfiltration traffic can cluster individual operator crews including the Outsider cluster within the broader JWR/Triad ecosystem.

No build-tooling artefacts, no webpack runtime banner, no source-map references. The kit ships hand-curated, minified files. Consistent with a developer who maintains the codebase in a single repo and ships by FTP/rsync rather than through CI/CD.

Conclusion

The JWR kit is an example of a quiet trend in the phishing-as-a-service market: kits that look more like product engineering than malware. The architectural choices–a Vue SPA, a dedicated Web Worker, a binary real-time C2 channel with an HTTP fallback, an international identity-validator library, plugin-mode integration with WordPress and Shopify, server-driven feature flags–are not innovations in any individual sense, but their composition into a single coherent kit is the kind of thing one normally sees behind a B2B SaaS dashboard, not behind a credit-card phish.

That coherence is also the kit’s weakness. Because the developer reuses the same engine across every campaign, every campaign carries the same fingerprints. The storage keys, the URL structure, the encryption envelope, the validator library, the typo, the WebSocket access-token suffix, the belong_to_template self-tag  all of these survive even when the brand skin, the hosting infrastructure, and the operator territory rotate. A defender who fingerprints the engine once detects the family forever.

The Outsider cluster is one of many customers of this kit inside the broader Smishing Triad PhaaS ecosystem. The campaign analysed here is one of many. The technical indicators and the decryption walkthrough are intended to make the next campaign and the next operator cluster measurably easier to track, decrypt, and disrupt.

Recommendations

For Enterprises:

  • Track new pages utilizing the unique file name signatures to initiate takedown of phishing sites.
  • Integrate an advanced Threat Intelligence solution to identify emerging phishing kits early.
  • Implement continuous monitoring for SMS-linked brand abuse with a comprehensive Digital Risk Protection solution.

For individual users:

  • Be cautious of unsolicited SMS messages creating urgency.
  • Avoid clicking links in SMS notifications.
  • Verify alerts through official apps or websites.
  • Never provide sensitive or payment information via SMS links.

Frequently Asked Questions (FAQ)

What is the Smishing Triad?

arrow_drop_down

The Smishing Triad is best described as not a single team, but a Phishing-as-a-Service (PhaaS) criminal marketplace made up of kit developers, phone-list brokers, spammers, and hosting providers who collaborate to deploy mass SMS phishing campaigns.

How is Outsider linked to the Smishing Triad?

arrow_drop_down

Outsider is a Chinese-language operator sub-cluster and active customer within the broader Smishing Triad marketplace that leverages shared PhaaS infrastructure and phishing frameworks — such as the JWR kit. Outsider is also known to operate its own PhaaS platform; which you can read about in a separate blog here.

What is Phishing-as-a-Service (PhaaS)?

arrow_drop_down

Phishing-as-a-Service (PhaaS) is a scalable, subscription-based cybercrime model that lowers the technical barrier to entry for threat actors. By using a PhaaS, cybercriminals can rapidly deploy fraudulent campaigns and replicate proven attack workflows with minimal technical overhead. It is a similar operating model to Ransomware-as-a-Service (RaaS), which you can read more about on the Group-IB Knowledge Hub.

Differences between and phishing and smishing

arrow_drop_down

Phishing is a deceptive form of cyberattack in which criminals impersonate trusted entities to trick victims into revealing confidential information or installing malware. While Smishing is a subset of phishing where impersonation is done through SMS message. You can read more about phishing and smishing on the Group-IB Knowledge Hub.

MITRE ATT&CK

Tactic Technique Notes
Initial Access T1660  Phishing SMS pretext with short-link redirection
Initial Access T1566.002  Spearphishing Link Smishing variant; victim clicks SMS-delivered URL
Execution T1204.001  User Execution: Malicious Link Victim opens the disposable landing domain
Defense Evasion T1027  Obfuscated Files or Information javascript-obfuscator.io string-array shuffle
Defense Evasion T1027.013  Encrypted/Encoded File AES-256-CTR envelope on all C2 traffic
Defense Evasion T1140  Deobfuscate/Decode Files or Information Key prepended in-band
Defense Evasion T1622  Debugger Evasion Anti-debug + integrity probe in worker
Defense Evasion T1497  Virtualization/Sandbox Evasion visibility command tracks victim focus state
Discovery T1217  Browser Information Discovery getDeviceInfo() UA-based browser/OS detection
Discovery T1614  System Location Discovery 4-provider IP geolocation rotation
Collection T1056.003  Input Capture: Web Portal Capture Vue form harvest, per-keystroke streaming
Collection T1119  Automated Collection synchronousDataInputHandle on every keystroke
Command and Control T1071.001  Web Protocols /api/open/* REST channel
Command and Control T1095  Non-Application Layer Protocol Binary WebSocket frames
Command and Control T1573.001  Symmetric Cryptography AES-256-CTR with prepended key
Command and Control T1571  Non-Standard Port Cloudflare-fronted ports vary by deployment
Exfiltration T1041  Exfiltration Over C2 Channel All exfil over the same WS/HTTP transport

Indicators of Compromise

Static-content fingerprints

  • localStorage keys: JwrCvvForm, JwrCustomCvvForm, JWRCID, JwrIpInfo, JwrSubmittedCardNumbers, JwrSelectedPaymentMethod, JwrControlInstruction, JwrExtraJSON, JwrAjaxUrl, JwrNonce, JWR_IFRAME_ACTIVE, jwrisThankPage, JwrIsShopify, JwrIsWordPress, JwrCustomEmailorPhone.
  • Per-victim id regex: ^JWRCVV-\d{13}-[a-z0-9]{1,13}-[a-z0-9]{1,13}$
  • DOM ids/classes: jwrGlobalErrorPopup, jwrErrorAnimationStyle, jwrErrorSlideIn, paypalLoadingdiv, paymentError1, paymentError2, paypalCardError1, paypalCardError2, loginVerifyError, codeVerifyError.
  • HTML page-name set co-occurrence of c_pay.html, d_sms_bank.html, i_payal_select_verify.html, h_paypal_login.html is family-distinctive.
  • Input-id convention: id="<arbitrary>-cardnumber", -expiry, -cvv, -cardholder, -smscode, -emailcode, -pincode, –paypaypwd, -paypayaccount, -button.

Network-traffic fingerprints

  • URL path prefix /api/open/ on a suspect host.
  • Co-occurrence of the seven endpoint suffixes on a single host.
  • WebSocket path /webSocket/QT/<JWRCID>/khkjsahfjkwhakjlsdwdddddd88  the trailing literal is the single highest-confidence IOC in the family.
  • HTTP Content-Type: application/octet-stream with body shape [48-byte header][16-aligned ciphertext].
  • HTTP long-poll cadence at exactly 2 s on /api/open/getPendingInstruction.
  • Sequential outbound calls to ipinfo.io, ipapi.co, ip-api.com, httpbin.org/ip (the 4-provider geolocation rotation).

Suricata-style hunting rule for the WebSocket access-token suffix:

alert tls $HOME_NET any -> $EXTERNAL_NET any
   ( msg:"JWR phishing kit  WebSocket access token in URL";
     content:"/webSocket/QT/"; http_uri;
     content:"/khkjsahfjkwhakjlsdwdddddd88"; http_uri;
     classtype:trojan-activity; sid:99000001; rev:1; )

YARA rule covering the JS bundle:

rule jwr_phishing_kit_main_js {
    meta:
        description = "JWR / trsb.top phishing kit - main bundle"
        author      = "threat-intel"
    strings:
        $a = "/api/open/addCvv"
        $b = "/api/open/getPendingInstruction"
        $c = "/webSocket/QT"
        $d = "JWRCVV-"
        $e = "belong_to_template"
        $f = "khkjsahfjkwhakjlsdwdddddd88"
        $g = "i_payal_select_verify.html"
    condition:
        4 of them
}

Code-level fingerprints

  • WorkerCrypto.encrypt shape (32-byte random key, 16-byte random IV, AES-CTR with length: 64).
  • AutoValidator class with the exact validator key set.
  • instructionConfig map with the 32 named commands.
  • javascript-obfuscator.io string-array shuffler immediately followed by the WorkerCrypto symbol.

DISCLAIMER: All technical information, including malware analysis, indicators of compromise and infrastructure details provided in this publication, is shared solely for defensive cybersecurity and research purposes. Group-IB does not endorse or permit any unauthorized or offensive use of the information contained herein. The data and conclusions represent Group-IB’s analytical assessment based on available evidence and are intended to help organizations detect, prevent, and respond to cyber threats.

Group-IB expressly disclaims liability for any misuse of the information provided. Organizations and readers are encouraged to apply this intelligence responsibly and in compliance with all applicable laws and regulations.

This blog may reference legitimate third-party services such as Telegram and others, solely to illustrate cases where threat actors have abused or misused these platforms.

This material is provided for informational purposes, prepared by Group-IB as part of its own analytical investigation, and reflects recently identified threat activity.

All trademarks referenced herein are the property of their respective owners and are used solely for informational purposes, without any implication of affiliation or sponsorship.