Tinklaraštis

Password Reset Poisoning: kaip vienos antraštės suklastojimas pavogia svetimą paskyrą.

Poisoning
Linux

Password Reset Poisoning: kaip vienos antraštės suklastojimas pavogia svetimą paskyrą.

Vartotojas paspaudžia „Pamiršau slaptažodį”, įveda el. paštą ir laukia laiško. Laiškas atėjo. Nuoroda jame atrodo normaliai — pažįstamas domenas, HTTPS. Jis paspaudžia.

Tuo metu atstatymo prieigos raktas atitenka užpuolikui. Po kelių sekundžių jis pakeičia slaptažodį. Vartotojas bando prisijungti — paskyra jau svetima.

Ataka vadinama Password Reset Poisoning. Jokio kenksmingo kodo, jokio serverio nulaužimo. Tik viena suklastota HTTP antraštė.

KODĖL PROGRAMA PASITIKI HOST ANTRAŠTE

Kai vartotojas prašo atstatyti slaptažodį, programa turi sudaryti nuorodą tokio tipo:

https://mysite.com/reset?token=abc123def456

Tam jai reikia žinoti svetainės domeno vardą. Daugelis framework’ų jį ima iš HTTP antraštės Host — tos kurią naršyklė siunčia su kiekviena užklausa. Logika aiški: „koks domenas nurodytas užklausoje — tą ir naudojame nuorodoje.”

Problema ta, kad antraštę Host visiškai kontroliuoja klientas. Naršyklė ją užpildo automatiškai, tačiau niekas netrukdo siųsti užklausą rankiniu būdu su bet kokia reikšme. Programa netikrina — ji tiesiog pasitiki.

ATAKOS ANATOMIJA ŽINGSNIS PO ŽINGSNIO

1 žingsnis. Užpuolikas žino aukos el. pašto adresą — arba tiesiog bando dažniausius adresus. Jis atidaro slaptažodžio atstatymo puslapį target.com.

2 žingsnis. Vietoj to kad siųstų užklausą per naršyklę, užpuolikas siunčia ją rankiniu būdu — per curl, Burp Suite ar bet kurį HTTP klientą. Jis pakeičia antraštę Host:

POST /reset-password HTTP/1.1
Host: attacker.com
Content-Type: application/x-www-form-urlencoded

[email protected]

3 žingsnis. Programa gauna užklausą, generuoja prieigos raktą ir sudaro nuorodą. Ji ima domeną iš antraštės Host — tai yra iš attacker.com. Laiškas išsiunčiamas aukai su nuoroda:

Atstatykite slaptažodį: https://attacker.com/reset?token=a1b2c3d4e5f6

4 žingsnis. Auka mato laišką. Siuntėjas — tikra svetainė, tema — tikra. Nuoroda atrodo įtartinai tik jei atidžiai skaitai domeną. Dauguma neskaito — spaudžia.

5 žingsnis. Aukos naršyklė daro GET užklausą į attacker.com/reset?token=a1b2c3d4e5f6. Užpuolikas mato prieigos raktą savo serverio žurnaluose.

6 žingsnis. Užpuolikas naudoja prieigos raktą tiesiogiai: https://target.com/reset?token=a1b2c3d4e5f6. Nustato naują slaptažodį. Paskyra užimta.

Auka niekada nesuprato kas nutiko — laiškas buvo tikras.

KAIP PATIKRINTI VIENA KOMANDA

Galite patikrinti ar jūsų svetainė yra pažeidžiama be jokių specialių įrankių:

curl -s -X POST https://target.com/forgot-password \
  -H "Host: attacker.com" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "[email protected]"

Jei į [email protected] atėjo laiškas su nuoroda į attacker.com — svetainė yra pažeidžiama.

Žymė -s įjungia tylųjį režimą — slepia progreso išvestį ir klaidų pranešimus. -X POST nustato metodą. -H prideda antraštę. -d — užklausos turinys. Naudokite tik savo svetainėse arba turėdami rašytinį savininko leidimą.

REALŪS ATVEJAI

2013 metais tyrėjas James Kettle iš PortSwigger (Burp Suite kūrėjai) susistemino HTTP Host antraštės atakų klasę ir paskelbė tyrimo rezultatus. Iki tol pažeidžiamumas buvo žinomas, tačiau nekataloguotas kaip atskira atakų klasė. Jo tyrimas paskatino framework’us masiškai spręsti problemą. Django, Joomla ir Piwik greitai išleido pataisas.

CVE-2012-4520 — Django iki versijos 1.4.2 netinkamai tikrinimo Host antraštę. Užpuolikas galėjo pakeisti domeną slaptažodžio atstatymo laiškuose. Pažeidžiamumas buvo uždarytas įvedus nustatymą ALLOWED_HOSTS.

CVE-2025-63828 — Backdrop CMS 1.32.1, 2025 m. lapkritis. Slaptažodžio atstatymo forma naudojo Host antraštę nuorodai sudaryti be tikrinimo. Pataisyta versijoje 1.32.2. Šviežias priminimas kad ši klaida nesenstėja — ji randama naujuose produktuose kiekvienais metais.

HackerOne kasmet skelbia dešimtis pranešimų apie šį pažeidžiamumą — jis sutinkamas įmonių programose, SaaS platformose ir savadarbėse sistemose. Priežastis visuomet ta pati: programuotojas parašė request.get_host() ten kur turėjo būti konstanta iš konfigūracijos.

APSAUGA FRAMEWORK’O LYGMENYJE

Django

Django slaptažodžio atstatymas naudoja request.get_host() nuorodai sudaryti. Be ALLOWED_HOSTS — pažeidžiama. Nustatymas privalomas produkcijoje:

# settings.py
ALLOWED_HOSTS = ['mysite.com', 'www.mysite.com']

Django tikrina kiekvieną įeinančią užklausą — jei Host nėra sąraše, grąžina 400 Bad Request. Sąraše turi būti tik tikri svetainės domenai. Jokio * produkcijoje.

Laravel

Laravel 7.12+ turi TrustedHosts middleware — pasirodė 2020 m. gegužę. Be jo programa priima bet kokį Host. Įjungti:

# app/Http/Middleware/TrustHosts.php
public function hosts(): array
{
    return [
        $this->allSubdomainsOfApplicationUrl(),
        'mysite.com',
    ];
}

Užregistruoti app/Http/Kernel.php (Laravel 7–10):

protected $middleware = [
    \App\Http\Middleware\TrustHosts::class,
    // ...
];

Laravel 11+ failo Kernel.php nebėra — middleware registruojamas bootstrap/app.php per metodą withMiddleware().

Bendras principas bet kuriam framework’ui

Niekada nestatyti URL iš antraštės Host. Slaptažodžio atstatymo laiškams domenas turi būti imamas iš programos konfigūracijos ar duomenų bazės — iš reikšmės kurią programuotojas aiškiai nustatė, o ne iš to ką atsiuntė klientas:

# Blogai — imame domeną iš kliento
reset_url = f"https://{request.get_host()}/reset?token={token}"

# Gerai — imame domeną iš konfigūracijos
reset_url = f"{settings.BASE_URL}/reset?token={token}"

WordPress

WordPress core sudaro atstatymo nuorodą iš siteurl saugomo duomenų bazėje — ne iš Host antraštės. Todėl klasikinis password reset poisoning kai nuoroda veda į užpuoliko svetainę WordPress core neveikia.

Tačiau CVE-2017-8295 atskleidė susijusį pažeidžiamumą: WordPress naudojo kintamąjį SERVER_NAME antraštėms From ir Return-Path išsiunčiamuose laiškuose. Jei serveris perduodavo kliento Host antraštę į SERVER_NAME — užpuolikas galėjo suklastoti siuntėjo adresą. Laiškas išeidavo su suklastotu From, o jei pristatymas nepavykdavo — grįždavo atgal į užpuoliko adresą kartu su prieigos raktu. Pažeidžiamumas palietė WordPress 2.3–4.7.4, uždarytas versijoje 4.7.5.

Kad ataka suveiktų, reikia vienu metu trijų sąlygų: pažeidžiama WordPress versija, žiniatinklio serveris netikrina Host antraštės ir perduoda jos reikšmę į PHP — būtent taip elgiasi Apache be aiškaus ServerName — ir laiškas nepristatomas ir grąžinamas siuntėjo adresu. Jei bent viena sąlyga neįvykdyta — ataka nesuveiks. Nginx su sukonfigūruotu default_server ir return 444 užklausa su svetimu Host tiesiog nepasiekia WordPress — uždaryta infrastruktūros lygmenyje.

Šiuolaikinis WordPress core yra apsaugotas. Rizika išlieka papildiniuose — jei papildinys stato URL iš $_SERVER['HTTP_HOST'] laiškų siuntimui, jis yra pažeidžiamas nepriklausomai nuo WordPress versijos.

APSAUGA NGINX LYGMENYJE

Host antraštės tikrinimas nginx — pirmoji gynybos linija. Užklausos su nežinomu Host neturi pasiekti programos:

server {
    listen 80 default_server;
    listen [::]:80 default_server;
    server_name _;
    return 444;
}

server {
    listen 80;
    server_name mysite.com www.mysite.com;
    # tik šie hostai pasiekia programą
}

Jei nginx grąžina 444 visiems nežinomiems hostams — užpuolikas tiesiog negauna atsakymo ir ataka nepavyksta. Net jei programa yra pažeidžiama — užklausa jos nepasiekia.

Patikrinkite kad apsauga veikia:

curl -v -X POST https://mysite.com/forgot-password \
  -H "Host: attacker.com" \
  -d "[email protected]"
# Laukiamas rezultatas: Empty reply from server

TIPINĖS KLAIDOS

Nustatoma ALLOWED_HOSTS = ['*'] kad greitai išspręsti klaidą kūrimo metu — ir pamirštama pašalinti prieš diegimą. Žvaigždutė išjungia visą Django apsaugą. Produkcijoje tai lygu nustatymo nebuvimui.

Manoma kad HTTPS apsaugo nuo Host antraštės pakeitimo. Neapsaugo — TLS šifruoja transportą, tačiau antraštės jame vis dar kontroliuojamos kliento.

Host tikrinamas tik pagrindiniame puslapyje ir pamirštama apie /api/reset-password, /auth/forgot ir kitus endpoint. Užpuolikas eina tiesiai ten.

Serverio IP adresas naudojamas kaip vienintelė reikšmė ALLOWED_HOSTS ar TrustedHosts ir pamirštama pridėti domeno vardą. Atstatymo laiškai išsiunčiami su nuorodomis į IP adresą.

IŠVADA

Password Reset Poisoning — vienos eilutės ataka. Jokių sudėtingų įrankių, jokio infrastruktūros nulaužimo. Viena pakeista antraštė, vienas laiškas — ir paskyra užimta.

Susirūpinti turėtų kiekvienas kas turi svetainę su slaptažodžio atstatymo forma — ypač jei tai Django be aiškaus ALLOWED_HOSTS, Laravel be TrustedHosts, arba WordPress su kustomais papildiniais kurie patys siunčia laiškus. CVE-2025-63828 Backdrop CMS buvo uždarytas tik 2025 m. lapkritį — ši klaida atsiranda naujuose produktuose kiekvienais metais nes programuotojai negalvoja iš kur gaunamas domenas laiške.

Apsauga veikia dviem lygmenimis. Infrastruktūros lygmenyje — nginx su return 444 nežinomiems hostams uždaro ataką dar prieš pasiekiant programą. Kodo lygmenyje — domenas laiškuose visada iš konfigūracijos ar duomenų bazės, niekada iš to ką atsiuntė klientas.

Jei jūsų svetainėje yra slaptažodžio atstatymo forma — išbandykite dabar pat viena komanda iš skyriaus aukščiau.

Leave your thought here

El. pašto adresas nebus skelbiamas. Būtini laukeliai pažymėti *