Letos na přelomu července a srpna mi přišla zpráva od jednoho klienta, kterému jsem před pár týdny nasazoval na web nějaké úpravy: někdo nám hacknul web. Nešlo se přihlásit do administrace a přes FTP zjistil, že tam jsou nějaké divné soubory.
A ve mně hrklo. Správu webu jim sice nedělám, ale co když jsem to při tom nasazování zavařil zrovna já? Přitom na tom webu nebylo za co zaškobrtnout: šablona na míru, pluginů jen pár a všechny aktualizované, WordPress ve verzi, která vypadala úplně čerstvě. Když nesedí žádné z obvyklých podezření, zbývá nepříjemně krátký seznam.
Předběhnu: moje úprava to nebyla. Ale cesta k té odpovědi byla dost poučná na to, abych o ní napsal.
Ten web totiž v tu chvíli fungoval úplně normálně. Přesto v něm sedělo devět kopií jednoho backdooru, dva falešné pluginy a tři cizí administrátorské účty. Do administrace se nešlo přihlásit jen proto, že si útočník přepsal přihlašovací stránku vlastní verzí – čímž si sám shodil krytí. Jinak by si toho asi dlouho nikdo nevšiml.
Kudy se dostali dovnitř?
Nebyl to plugin ani slabé heslo. Byla to chyba v samotném WordPressu – v tom jádře, které rozhodně nebylo nějak zastaralé.
Web běžel na verzi 6.9.4, opravená byla až 6.9.5. Celý rozdíl mezi děravým a záplatovaným webem byla jedna číslice na konci, na kterou se při pohledu do administrace nikdo nedívá.
Ta chyba nese označení CVE-2026-63030, přezdívá se jí wp2shell a umožňovala spustit na serveru cizí kód bez přihlášení. Žádné hádání hesel, žádný phishing – stačilo poslat na web správně poskládaný požadavek.
Že to bylo opravdu tudy, ukázaly logy: byla v nich série požadavků s charakteristickým vzorcem toho útoku a bezprostředně po nich se v databázi objevily ty tři cizí administrátorské účty.
Opravné verze vyšly 17. července – a tím se ta chyba stala veřejně známou. Během několika hodin ji útočníci začali automatizovaně zkoušet na všem, co našli – a tady uspěli.
Co všechno na tom webu bylo
- devět kopií jednoho souborového správce, rozesetých po systémových složkách WordPressu, šabloně, nahraných souborech i pluginech a pojmenovaných tak, aby vypadaly jako součást WordPressu
- dva falešné pluginy – jeden se tvářil jako známý diskusní plugin a uměl si dotáhnout další kód z internetu
- soubor maskovaný jako
.gitignore, tedy nudná technikálie, kterou nikdo neotevírá; uvnitř čtyřicet kilobajtů cizího kódu - tři administrátorské účty, které nikdo nezaložil.
Co udělat po čištění
I když smažete všechny podstrčené soubory a vyhodíte z databáze cizí administrátory, pořád nemáte hotovo. Ta zranitelnost útočníkovi umožnila spouštět na serveru libovolný kód – mohl si tedy v klidu otevřít konfigurační soubor a odejít s heslem k databázi, nebo si někam uložit něco, na co jsem vůbec nepřišel.
Je proto dobré jádro i pluginy vyměnit za čistou verzi z oficiálního zdroje a změnit všechna hesla – nejen do administrace, ale i k databázi, na FTP a SSH, bezpečnostní klíče WordPressu, odesílání e-mailů a klíče ke všemu, co je na web napojené. Zůstane databáze, kterou vyměnit nejde a musí se také zkontrolovat.
A hlavně: tohle za vás neudělá žádný plugin na jedno kliknutí. Jsou to hodiny práce a zkoumání. Čas, který jde celý jen do toho, aby byl web tam, kde byl předtím.
Proč tam ta stará verze vůbec zůstala
Zpětně mě na celém případu zarazilo něco jiného než ten malware. Web běžel na 6.9.4 – jenže já jsem na něm pár týdnů předtím pracoval a lokálně jsem měl sedmičkovou řadu. Jak to, že na ostrém webu pořád seděla šestka?
Odpověď je jednoduchá: automatické aktualizace na tom webu zapnuté nebyly. WordPress si je totiž umí nainstalovat sám a většina webů je opravená dřív, než si toho majitel vůbec všimne. Kdyby běžely i tady, web by se spravil sám během pár dní po vydání záplaty a nikdo by se nic nedozvěděl.
A jak to, že když jsem úpravy dělal na novější verzi, tahle se po nasazení na produkci neobjevila?
Použil jsem k tomu All-in-One WP Migration: práci uděláte na kopii webu, hotový výsledek zabalíte a nahrajete na ostrý web. V tom balíčku je databáze a složka wp-content – tedy šablona, pluginy a nahrané soubory, ale ne samotné jádro WordPressu.
A popravdě – ani kdyby se jádro přenášelo, nepomohlo by to. Lokálně jsem tehdy pracoval na novější řadě, jenže ta chyba byla v obou: zranitelné bylo všechno od 6.9.0 po 6.9.4 a od 7.0.0 po 7.0.1. Jediné, co ten web mohlo zachránit, byla ta bezpečnostní aktualizace, která na něm neběžela.
Proto doporučuji – automatické bezpečnostní aktualizace nechte na WordPressu zapnuté. Nejsou to velké verze, co něco překopou – jsou to ta malá čísla na konci, která jen zavírají díry.
Jak se ta automatika zapne
Ve výchozím stavu WordPress bezpečnostní aktualizace instaluje sám a nemusíte pro to dělat nic. Když je někdo vypnul, zapínají se zpátky v souboru wp-config.php:
define( 'WP_AUTO_UPDATE_CORE', 'minor' );
Hodnota minor dělá přesně to, co chcete: bezpečnostní a drobné verze se instalují samy, velké verze zůstanou na vás. (true zapne i ty velké, false vypne všechno.)
Zároveň se vyplatí zkontrolovat, jestli v tom souboru někde není tohle:
define( 'AUTOMATIC_UPDATER_DISABLED', true );
To vypíná všechny automatické aktualizace bez ohledu na cokoli dalšího a bývá to nejčastější důvod, proč se web sám neopraví.
A pozor na cache
Pak je tu jedna méně známá past. WordPress nemá vlastní hodiny – naplánované úlohy spouští teprve ve chvíli, kdy někdo otevře stránku. A kontrola aktualizací je přesně taková úloha. Jenže na webu s plnou stránkovou cache dostane návštěvník rovnou hotové HTML a k PHP se vůbec nedojde. Takže neproběhne ani ta kontrola.
Čím líp je web nacachovaný, tím vzácněji k tomu dojde – a na dobře odladěném webu nemusí kontrola proběhnout klidně několik dní.
Řešení je nespoléhat na návštěvníky a dát tomu pravidelný impulz ze serveru. V administraci hostingu (nebo přes crontab) se založí úloha, která si o to řekne sama:
*/5 * * * * wget -q -O - "https://vasweb.cz/wp-cron.php?doing_wp_cron" >/dev/null 2>&1
Tím je to vyřešené. WordPress teď dostane pobídku každých pět minut bez ohledu na to, jestli někdo web navštívil.
Volitelně se k tomu dá do wp-config.php přidat ještě tohle:
define( 'DISABLE_WP_CRON', true );
To vypne spouštění úloh při načtení stránky, které je od téhle chvíle zbytečné a jen ubírá výkon. Podmínka to není, serverový cron funguje i bez toho. Důležité je jen pořadí: samotné DISABLE_WP_CRON bez cronu na serveru by naplánované úlohy vypnulo úplně, tedy pravý opak toho, o co jde.
Co si z toho odnést
- Ověřte si, že automatické bezpečnostní aktualizace opravdu běží. Ne že by měly – že běží. Umí být vypnuté, aniž o tom kdokoli ví.
- Aktualizované pluginy neznamenají aktualizované jádro – a nasazení z aktuálního systému taky ne.
- Napadení nepoznáte podle toho, jak web vypadá. Tady prasklo náhodou, protože si útočník rozbil přihlašovací stránku. Jinak se to pozná jedině tak, že se někdo dívá.
O weby, které mám ve správě, se starám právě proto, aby tohle jejich majitele nemuselo zajímat. Kontrola verzí, aktualizace, zálohy – běží to na pozadí.