RO EN

Anatomia unui atac coordonat asupra unui formular de contact

Anatomia unui atac coordonat asupra unui formular de contact ✨ Imagine generată cu AI
Doru Bulubașa
12 octombrie 2026
37 vizualizări

Miercuri, 7 octombrie 2026, ora 18:15 UTC. Un email de la Microsoft 365, severitate High: „User NOREPLY@LUDOPROGRAMMING.COM has been restricted from sending messages outside the organization due to potential compromised activity."

Nimeni nu spărsese nimic. Nicio parolă nu fusese furată, niciun server nu fusese compromis. Pur și simplu, formularul de contact de pe site își făcuse treaba de 109 ori — exact cum fusese programat, și exact cum nu ne gândisem niciodată.

Trucul: inversarea câmpurilor

Formularul de contact trimitea două emailuri la fiecare mesaj valid. Unul către noi, cu conținutul mesajului. Și unul către persoana care completase formularul, o confirmare politicoasă care începea cu „Salut, {nume}!".

Al doilea email era problema, și a fost nevoie de un atacator ca să ne-o arate.

Un bot a completat formularul așa:

  • Câmpul „nume": Вам перевод 172144 руб. забрать тут https://5d58cc63.sslip.io/… — în traducere, „Ai un transfer de 172.144 de ruble, ridică-l aici", cu link.
  • Câmpul „email": adresa unei victime oarecare. În 96 din 109 cazuri, o adresă de Gmail.
  • Câmpul „mesaj": jetoane aleatorii — roY0RVe A4BJ tOXXCkW — scrise special ca să treacă de filtre.

Rezultatul: serverul nostru trimitea înșelătoria către victimă, semnată de pe domeniul nostru, cu SPF și DKIM valide. Un email de phishing cu reputație impecabilă, livrat gratuit de infrastructura noastră.

Asta nu e spam pe formularul de contact. E folosirea formularului ca releu de email. Diferența e că în primul caz deranjezi un om — în al doilea, devii tu atacatorul, din punctul de vedere al tuturor celorlalți.

Cifrele

Toate mesajele formularului se salvează în baza de date, inclusiv cele oprite de filtre. Decizia aceea, luată cu luni înainte pentru alt motiv — să putem verifica dacă filtrul aruncă din greșeală clienți reali — s-a dovedit a fi singura sursă de adevăr în timpul incidentului.

  • 167 de mesaje în trei zile, dintre care 109 au ajuns până la trimitere.
  • Niciunul nu era real. Toate 167, fără excepție, erau automate.
  • Ritm constant: aproximativ 10 mesaje pe oră, zi și noapte, fără pauză.
  • 41 de adrese IP distincte pentru cele 109 mesaje trecute.
  • 93 dintre ele aveau link în câmpul de nume — un câmp care nu conține niciodată o adresă web.

De unde a venit

Întrebarea „din ce țară" are un răspuns care deranjează: din nicio țară anume. Am căutat în registrele publice fiecare adresă IP din atac, și tiparul e uniform:

  • 3.114.220.22 — Amazon Web Services, regiunea Tokyo
  • 132.145.80.230 — Oracle Cloud, Statele Unite
  • 154.90.62.70 — Lightnode, Coreea de Sud
  • 193.29.139.132 — Greenhost, Olanda
  • 149.22.87.204 și 38.91.107.49 — Cogent Communications, Statele Unite
  • 8.218.195.61 — bloc APNIC, Asia-Pacific

Nicio adresă rezidențială. Niciun calculator infectat al vreunui pensionar. Toate sunt mașini virtuale închiriate, la furnizori de cloud respectabili, plătite probabil cu carduri furate și aruncate după câteva zile de folosință.

Aici e lecția incomodă pentru oricine se bazează pe limitarea după IP: originea unui atac modern nu e o țară, e un card de credit. Costul unei adrese IP noi a scăzut sub costul unei cafele, iar apărările care presupun că un atacator revine de la aceeași adresă au devenit decorative.

Unde duceau linkurile

Partea cu adevărat elegantă a atacului — și o spun fără ironie — era felul în care evitau înregistrarea unui domeniu. Linkurile foloseau sslip.io și nip.io, două servicii publice de DNS wildcard care transformă o adresă IP într-un nume de gazdă. Numele 5d58cc63.sslip.io nu e aleatoriu: 5d58cc63 e chiar adresa IP scrisă în hexazecimal, care se traduce în 93.88.204.99.

Niciun domeniu de înregistrat, niciun registrar la care să depui o plângere, niciun certificat de cumpărat. Serverul de destinație era găzduit într-un bloc olandez, iar un al doilea link din aceeași campanie ducea la o mașină din Microsoft Azure. Atacatorii folosesc aceleași platforme ca noi, pentru aceleași motive: sunt ieftine, rapide și de încredere.

O notă metodologică, pentru că era să greșesc: prima rezolvare DNS pe care am făcut-o a dat o adresă din România. Nu era adevărat — era furnizorul meu de internet, care redirecționează automat numele inexistente către o pagină proprie. Cu un resolver public, răspunsul real a apărut imediat. Dacă publici o atribuire, verifică de pe două drumuri diferite, altfel acuzi un nevinovat.

De ce n-au funcționat apărările existente

Formularul nu era neprotejat. Avea patru straturi, și e instructiv de văzut cum a trecut botul pe lângă fiecare.

Câmpul-capcană — un câmp invizibil pe care doar un robot îl completează. A prins 2 mesaje din 167. Boții serioși nu mai cad în el de ani de zile.

Timpul minim de completare — trei secunde, pentru că un om nu scrie mai repede. A prins 50 de mesaje. Apoi botul a început să aștepte, și a încetat să mai prindă ceva.

Limita de rată per IP — maximum trei mesaje pe oră de la aceeași adresă. Inutilă în fața a 41 de adrese care se roteau. Mai mult, contorul trăia în memoria aplicației, deci dispărea la fiecare repornire și nu era comun între instanțe.

Filtrul structural — partea care judecă forma textului, nu vocabularul: cuvinte imposibile lingvistic, cifre îngropate în cuvinte, același număr repetat în câmpuri diferite. Prinsese 6 mesaje. Pe restul le-a ratat din două motive pe care le-am înțeles abia citind sarcinile reale.

Primul: analiza lucra doar pe alfabetul latin. Textul chirilic era, pentru filtru, text gol. Al doilea: jetoanele de tipul roY0RVe erau rupte în bucăți scurte la fiecare cifră, iar bucățile rămase erau prea scurte ca să pară suspecte.

Cum am contracarat

1. Am scos amplificatorul

Emailul de confirmare către adresa din formular a fost eliminat complet. A fost prima decizie și cea mai importantă, pentru că nicio îmbunătățire a filtrului nu schimbă o realitate structurală: un formular care trimite email către orice adresă necunoscută este, prin definiție, un releu deschis. Diferența e doar cât de greu e de găsit.

Dacă vrei confirmare pentru oameni reali, singura formă sigură e dublul opt-in — adresa trebuie să-și dovedească existența înainte să primească al doilea mesaj. Până atunci, tăcerea e mai bună.

2. Am scris reguli din sarcinile reale, nu din imaginație

Având toate cele 109 mesaje salvate, regulile noi n-au fost ghicite, ci citite direct din atac:

  • Link în câmpul de nume — blochează singur. Un nume nu conține niciodată o adresă web, iar regula asta acoperea 93 din 109 mesaje.
  • Nume mai lung de 70 de caractere — un nume nu e o propoziție.
  • Alfabet chirilic — deliberat sub pragul de blocare. Singur, nu oprește nimic: un client bulgar sau rus e un client. Împreună cu orice alt semnal, devine decisiv.
  • Jetoane aleatorii cu litere și cifre amestecate — patru sau mai multe într-un formular n-au explicație inocentă.
  • Cuvinte cu registrul comutat de patru ori — KsWGvdjBWBGYcDmOAQ. Pragul e la patru alternanțe tocmai ca McDonald și DeLuca să treacă nestingherite.

Fiecare regulă are o pondere, iar blocarea se face pe scor cumulat. Un singur semnal nu condamnă pe nimeni — exact ca înainte, pentru că riscul real al oricărui filtru nu e să lase spamul să treacă, ci să taie clientul.

3. Am măsurat pe atacul real

Partea pe care o sar cei mai mulți. Am rulat filtrul nou peste toate cele 109 mesaje care scăpaseră: 108 blocate. Singurul rămas e o ofertă SEO scrisă de un om, în engleză curată, structural identică cu o cerere legitimă — și pe care, sincer, nici nu vrem s-o blocăm.

Fără corpusul salvat, „am strâns filtrul" ar fi rămas o impresie. Cu el, e un număr.

4. Am verificat în producție, nu în teste

Publicarea s-a încheiat la 06:41:40 UTC. Botul lovea din opt în opt minute, deci dovada a venit singură, patru minute mai târziu:

06:45:47  filtrat  spam (scor 17: digits_in_name, link_in_name,
                   name_too_long, cyrillic, random_tokens)

Scor 17 la un prag de 5. De atunci, niciun mesaj al campaniei n-a mai ajuns la trimitere.

Partea pe care nimeni n-o povestește: urmările

Repararea cauzei n-a repornit emailurile. Contul rămăsese restricționat, iar fiecare încercare se întorcea cu 550 5.1.8 Access denied, bad outbound sender. Trei lucruri învățate pe viu:

„200 OK" nu înseamnă „livrat". Serviciul nostru de email răspundea cu succes, iar aplicația marca mesajul ca trimis. Microsoft îl respingea câteva secunde mai târziu, în altă parte a lanțului. Monitorizarea care se oprește la codul HTTP e oarbă exact acolo unde contează.

Restricția se aplică expeditorului, nu destinatarului. Am presupus că mesajele interne vor trece. Nu trec: un cont restricționat nu mai trimite nicăieri, nici măcar colegului de la biroul alăturat.

Notificările proprii plecau în afara organizației. Mesajele formularului ajungeau la o adresă de Gmail, nu la o cutie din domeniu. Deci în timpul atacului nu s-au trimis 109 emailuri externe, ci 218 — confirmările către victime și notificările către noi au hrănit împreună detectorul Microsoft.

Ce să verifici la tine, azi

Dacă ai un formular de contact pe un site, întrebările care contează sunt astea:

  1. Trimite formularul vreun email către adresa completată de vizitator? Dacă da, ai un releu deschis. Oprește-l sau pune dublu opt-in.
  2. Unde ajung notificările tale? Dacă destinatarul e în afara domeniului, fiecare mesaj de spam îți consumă din reputația de expeditor.
  3. Salvezi ce blochează filtrul? Fără corpus, nu poți nici măsura, nici îmbunătăți — și nici nu afli dacă arunci clienți.
  4. Limita de rată e per IP? Atunci nu te apără de nimic serios. Mută-o pe altceva, sau acceptă că e doar un capac împotriva accidentelor.
  5. Ce se întâmplă dacă furnizorul tău de email te blochează mâine? Confirmările de comandă, resetările de parolă, facturile — toate pleacă pe același drum.

Ce am fi făcut altfel

Codul nu era neglijent. Avea filtre, limite, câmp-capcană, salvare pentru audit și comentarii care explicau de ce fiecare prag e acolo unde e. A pierdut în fața unui atacator care a citit comportamentul aplicației mai atent decât am citit noi propriile presupuneri.

Greșeala n-a fost un rând de cod, ci o presupunere nescrisă: că oamenii completează formularul pentru ei înșiși. Din momentul în care un câmp controlat de vizitator ajunge în corpul unui email trimis către altă adresă controlată de același vizitator, nu mai ai un formular de contact. Ai un serviciu de trimitere a mesajelor, oferit gratuit, oricui.

Incidentul a durat o zi. Repararea a durat trei ore. Recâștigarea reputației de expeditor durează mai mult decât amândouă la un loc — și ăsta e singurul cost care contează cu adevărat.