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:


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:
- Brand-surface specialization. The recovered build’s
cfg.projectNameis set to a regional template tag, and the kit’s belong_to_template field populated bycfg.projectNameand shipped on every exfiltration packet categorises this operator’s campaigns into a distinct sub-tree of the broader Triad catalog. - 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. - 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.
- 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.
The kit identifies itself, in every artifact we recovered, as JWR. The name is burned into:
localStoragekeys: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 asJWRCID.
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.
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_imgpairs for SSN, passport, driver’s licence, and medical ID. The_handheld_imgsuffix 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_pwdand a separate set of pages. - Telemetry & device fingerprint:
ip, device, language, timeZone, userAgent, cookie, latitude, longitude,plus anextraJSONfor 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.
Four-provider geolocation rotation (getIPInfo → requestIpAddress):

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.
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.
2. Message router:

Figure 7. Message router code snippet.
3. HTTP long-poll fallback:

Figure 8. HTTP long-poll fallback code snippet.
4. Command interface:

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.
Ships a 25-validator regex library covering 12+ jurisdictions:

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.
and reversed in decrypt:

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.
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:
- The plaintext is
JSON.stringify(body), encoded as UTF-8. - The cipher is
AES-256-CTR, key Uint8Array(32),IV Uint8Array(16), length64. - 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.
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.
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.encryptand received byWorkerCrypto.decrypt(carved out of a PCAP via theSec-WebSocket-Key/Sec-WebSocket-Accepthandshake 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.
| 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.
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.
Layer 3: Self-integrity probe with catastrophic-backtracking regex.

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.
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.
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.
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?
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?
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)?
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
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
localStoragekeys: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.htmlis 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>/khkjsahfjkwhakjlsdwdddddd88the trailing literal is the single highest-confidence IOC in the family. - HTTP
Content-Type: application/octet-streamwith 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.encryptshape (32-byte random key, 16-byte random IV, AES-CTR withlength: 64).AutoValidatorclass with the exact validator key set.instructionConfigmap with the 32 named commands.javascript-obfuscator.iostring-array shuffler immediately followed by theWorkerCryptosymbol.
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.





