Egy megosztott VPS-t kaptunk kézbe, amelyen több különálló Laravel-alkalmazás futott egymás mellett, egy publikus marketingoldal, néhány API és egy admin-felület fájlkezelővel. A gépről az üzemeltető büszkén mondta, hogy SSH-zni „csak VPN-en át lehet”. Ez igaz is volt, és pontosan ezért volt megtévesztő. A támadó ugyanis egyetlen SSH-kapcsolatot sem használt, mert végig a nyilvános HTTPS-en jött be, a webalkalmazáson keresztül.
Ez az eset azért tanulságos, mert semmi egzotikus nincs benne. Minden lépése egy ismert, régóta dokumentált hiba, és minden lépése megelőzhető lett volna egy-egy sornyi konfigurációval.
A belépési pont: egy fájlfeltöltő, ami PHP-t is kiszolgált
Az admin-alkalmazásban volt egy fájlfeltöltő. A feltöltött fájlok a Laravel szokásos helyére kerültek, a storage/app/public/files/… alá, amit a public/storage symlink tesz kívülről elérhetővé. A validáció a kiterjesztést és a kép fejlécét nézte, a tartalmat nem.
A támadó egy polyglot fájlt töltött fel, aminek az elején egy érvényes JPEG-fejléc állt, utána pedig PHP-kód. A böngészőnek kép, a PHP-nek program. Ez önmagában még nem lett volna baj, mert egy feltöltött .php fájl csak akkor veszélyes, ha a szerver le is futtatja. Itt pedig lefuttatta, mert az nginx vhost minden .php-t a PHP-FPM-nek adott, függetlenül attól, hol van a fájlrendszerben:
# A sebezhető minta: BÁRMELY .php lefut, a feltöltési mappában is
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
include fastcgi_params;
}
Egy Laravel-alkalmazásban egyetlen PHP-fájlnak van dolga lefutni, a public/index.php front controllernek. Minden más .php, pláne egy feltöltési mappában, vagy hiba, vagy támadás. A helyes minta ezért nem azt mondja meg, mit tiltunk, hanem azt, mi az egyetlen, amit engedünk:
# A javítás: csak a front controller futhat, minden más .php tilos
location = /index.php {
fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
include fastcgi_params;
}
location ~ \.php$ {
deny all;
return 403;
}
Ez az egy blokk az egész támadási osztályt kizárja, mert hiába kerül fel egy webshell bárhová, nem indul el.
A négy nap, amíg látszólag semmi sem történt
A logokban a feltöltés és a tényleges támadás között négy nap telt el. A nulladik napon került fel a webshell, és aznap egy Telegram-bot lekérte egyszer. Ez a tipikus „megjelöltem, később visszajövök” minta. A negyedik napon hajnalban jött vissza a támadó, és ekkor kezdte el POST-kérésekkel vezérelni a shellt.
Ez a csúszás fontos tanulság. A betörés nem egy pillanat, hanem egy folyamat, és a legnagyobb esély az észlelésre pont ebben a csendes ablakban van. Egy egyszerű riasztás arra, hogy „új .php fájl jelent meg egy feltöltési vagy asset-mappában”, négy napig ott állt volna kihasználatlanul.
deployerből root
A webshell a PHP-FPM felhasználójaként futott; nevezzük deployer-nek. Ez a fiók nem volt sudoer, tehát a jelszó nem segített volna. A támadó ehelyett egy néhány megabájtos eszközkészletet töltött le a shellen át, és egy helyi kernel-exploittal lett rootból.
Rootként az első dolga egy rejtett kapu volt, vagyis egy új sor a /etc/passwd-ben, 0 felhasználó- és csoportazonosítóval, a legitim rendszerfiókokra hajazó névvel. Egy ilyen sor a felületes szemlének normálisnak tűnik, közben teljes jogú root-hozzáférés.
A tanulság itt kettős. Egyrészt a PHP-FPM felhasználója fusson a lehető legkevesebb joggal, elkülönítve. Másrészt, és ez a kényelmetlen rész: root-szintű kompromittálás után egyetlen takarítás sem ad teljes bizonyosságot. Egy root-jogú támadó módosíthat bináris fájlt, hamisíthat időbélyeget, törölhet naplót. A takarítás csökkenti a kárt; a biztos megoldás az újratelepítés.
A backdoor, ami kifelé tárcsázott
A perzisztencia legérdekesebb darabja egy gsocket (reverse-relay) backdoor volt, két lábon állva. Egy óránként futó felhasználói cron és egy root systemd-szolgáltatás, mindkettő ártalmatlan kernelfolyamatnak álcázott néven futtatta a binárist.
Itt kapcsolódik vissza a VPN-es kényelemérzet. A gsocket nem nyit bejövő portot, amit egy tűzfal blokkolhatna. Ehelyett kifelé épít kapcsolatot egy köztes relay-hálózaton át. Ehhez semmilyen bejövő szabály nem kell, sem SSH, sem nyitott port. A „csak VPN-en át lehet SSH-zni” szabály teljesült végig, és teljesen érdektelen volt, mert a támadónak nem kellett bejönnie, hiszen a szerver ment ki hozzá.
A gyakorlati következmény pedig az, hogy a bejövő szűrés fontos, de a kimenő forgalom szűrése legalább annyira az. Egy production szervernek ritkán van dolga tetszőleges kimenő kapcsolatot építeni.
Amit a támadó akart: SEO-spam
Nem adatlopás, nem zsarolóvírus. A publikus oldal index.php-jába egy néhány soros betoldás került, amely a keresőrobotoknak (Googlebot, Bingbot és társai a User-Agent alapján) egy másik, spam-tartalmat szolgált ki egy külső CDN-ről, a valódi látogatóknak viszont a rendes oldalt. Ez a „cloaking”, aminek a célja a fertőzött, jó reputációjú domain kihasználása idegen tartalom kereső-rangsorolására.
Ennek van egy alattomos utóhatása is. A támadó a feltöltött igazolófájllal be tudja jelölni magát a Google Search Console-ban a domain tulajdonosaként, és a helyi fájl törlése ezt nem szünteti meg, mert a Search Console-ban külön kell eltávolítani.
Amit tanultunk
Egyetlen soros hibák láncolata volt, tehát a védekezés is soronként áll össze:
- Csak a front controller futhat. Az nginx
index.php-only mintája az egész webshell-osztályt kizárja, függetlenül attól, hová kerül a fájl. - A feltöltés ne a webroot alá menjen, vagy ha muszáj, ott a PHP-futtatás legyen explicit tiltva. A feltöltést tartalom szerint validáld, ne csak kiterjesztés és fejléc alapján.
- A PHP-FPM felhasználója fusson minimális joggal, hogy egy webshell ne legyen egyenlő a teljes gép elvesztésével.
- Szűrd a kimenő forgalmat. A modern backdoorok kifelé tárcsáznak; a bejövő tűzfal önmagában hamis biztonságérzet.
- Figyeld a változást. Új
.phpegy feltöltési mappában, új sor a/etc/passwd-ben, új systemd-egység, mind olcsó és magas jelértékű riasztás. - A naplómegőrzés legyen elég hosszú. Nálunk a rendszernapló három napra, a webkiszolgáló-log két hétre nyúlt vissza. Egy több napig lappangó behatolásnál ez dönti el, hogy rekonstruálható-e a belépési pont, vagy sem.
- Root-kompromittálás után újratelepítés. Ez a kényelmetlen, de egyetlen igazán megbízható válasz.
Egyik pont sem új, és éppen ez a lényeg. A betöréshez nem kellett nulladik napi sérülékenység, csak néhány kihagyott alapbeállítás. A védelemhez sem kell több annál, mint hogy azt a néhány sort tényleg megírja valaki, mielőtt szükség lenne rá.