Vier dagen te laat met één update: hoe één vergeten site vijf andere meenam
Eén WordPress-site draaide nog een kwetsbare plugin terwijl de patch al vier dagen klaarstond. Door een gedeeld hostingpakket werden het vijf besmette sites en 129 kwaadaardige bestanden. De reconstructie, en waarom herstel uit een back-up de deur opnieuw openzette.
Drie requests. Vier seconden. Van "geen account" naar een volledig beheerdersdashboard. De patch die dit had voorkomen, stond op dat moment al vier dagen klaar — op één site was hij alleen nog niet geïnstalleerd. Wat daarna volgde, kostte drie dagen ongestoorde toegang, 129 kwaadaardige bestanden over vijf websites en een storing die de eigenaar pas op het spoor bracht van wat er werkelijk aan de hand was.
Dit is de reconstructie van een incident dat wij eind augustus onderzochten bij een Nederlandse organisatie. de opzet van de omgeving komt bij honderden Nederlandse bedrijven precies zo voor. Eén hostingpakket, meerdere WordPress-sites, één beheerder die "het er ooit nog eens bij moet pakken".
Wat er gebeurde
De ingang was een populaire beheerplugin voor WordPress, de WPMU DEV Dashboard-plugin (wpmudev-updates). Daarin werden in augustus twee ernstige kwetsbaarheden bekend:
| CVE (CVSS) | Kwetsbaar | Wat het oplevert |
|---|---|---|
| CVE-2026-76581 (9.8) | t/m 5.0.1, opgelost in 5.0.2 | Beheerderstoegang via de Hub SSO-flow, als SSO aan een beheerdersaccount is gekoppeld — dit is de route die is gebruikt |
| CVE-2026-15459 (8.1) | t/m 5.0.0, opgelost in 5.0.1 | Installatie van willekeurige plugins en daarmee code-uitvoering, op sites die niet met de WPMU DEV Hub zijn verbonden |
De aanval bestond letterlijk uit drie requests:
admin-ajax.php?action=wdpsso_step1 302
admin-ajax.php?action=wdpsso_step2&outgoing_hmac=… 302
/wp-admin/ 200 ← ingelogd
De fout zit in hoe de twee stappen hun handtekening opbouwen. Stap 1 ondertekent een aaneenschakeling van token, state, redirect en domein, en geeft die HMAC terug. Stap 2 controleert een aaneenschakeling zonder het domein. Wie in stap 1 een geldige HMAC opvraagt en in stap 2 de domeinwaarde naar het redirectveld verschuift, komt door de controle en krijgt een beheerderssessie. Versie 5.0.2 trekt de opbouw van die twee berichten gelijk, waarmee een hergebruikte HMAC waardeloos wordt.
De patch was er al vier dagen
Dit is het detail dat blijft hangen. De hoofdsite van deze organisatie draaide de gepatchte versie en was beschermd. Eén kleinere, half vergeten site draaide nog de oude versie. De aanvaller koos niet willekeurig: dat was de enige kwetsbare ingang op de hele server.
Vier dagen. Zo groot was het venster tussen "de fix bestaat" en "de fix staat erop". Voor een site die niemand meer echt gebruikt, voelt dat als een detail. Voor een aanvaller die het internet afscant op precies deze plugin, is het een uitnodiging.
Waarom één site vijf sites werden
Hier zit de duurste ontwerpfout, en die staat los van WordPress.
Alle sites stonden in hetzelfde hostingpakket (in Plesk: dezelfde subscription). De PHP-beveiligingsgrens open_basedir staat dan op het niveau van dat hele pakket, niet per site. Concreet: PHP-code die op de zwakste site draait, mag bestanden schrijven in álle andere sites in dat pakket. De hoofdsite, de dealerportal, de campagnesite — allemaal.
Zo werd één kwetsbare plugin op een vergeten site het startpunt voor 129 kwaadaardige bestanden over vijf sites.
Het tegenbewijs stond in dezelfde omgeving: één site was ooit in een éigen subscription gezet. Die bleef volledig schoon. Isolatie is geen theorie — hier is precies te zien wat het scheelt.
Wat de malware deed
Geen los shell-scriptje, maar negen onafhankelijke families over vier verschillende commandokanalen. De opzet gaat er expliciet van uit dat een deel gevonden wordt.
| Component (aantal) | Kanaal | Functie |
|---|---|---|
Zelfherstellende engine, verstopt als .png en .css (2) |
— | Houdt index.php intact en read-only, zet gewiste shells terug, herinjecteert kernbestanden op een willekeurige regel |
| Filemanagers vermomd als GIF-afbeeldingen (8) | $_GET |
Bestandsbeheer achter wachtwoord en vervaltijd |
| Eval-backdoor (37) | Cookie | Commando's reizen in cookies — die worden niet gelogd |
| Ongeauthenticeerde shells (6) | $_GET + upload |
shell_exec() en bestandsupload zonder inloggen |
| OS-commandoshell (1) | $_POST |
Functienamen opgeknipt tegen scanners |
| Thema-injectie (3) | modulair | 185 regels toegevoegd aan het echte functions.php |
| Remote loader (1) | remote fetch | Haalt payload op als "afbeelding" van githubusercontent |
| Versleutelde loader (5) | versleuteld | Verstopt in nagemaakte submappen (Http/Http/) |
.htaccess-lockdown (63) |
— | Blokkeert alle PHP behalve index.php |
Die laatste regel is de reden dat het incident überhaupt opviel. De aanvaller sloot met een eigen .htaccess álle andere PHP af — bedoeld om concurrerende aanvallers buiten te houden, met als bijwerking dat ook de beheerders eruit vlogen. De organisatie zag een 403-storing op zaterdagavond. Die storing wás de inbraak.
Waarom de logs bijna niets lieten zien
Twee technieken, allebei simpel en allebei effectief.
Commando's via cookies. De belangrijkste backdoor haalde zijn instructies niet uit de URL of de POST-body, maar uit cookies. Apache logt standaard geen cookies. In het access-log staan dus normale requests naar een normaal ogend bestand.
Tijdstempels terugzetten. De engine gebruikte touch() om wijzigingsdatums te vervalsen. Zoeken op "welke bestanden zijn deze week gewijzigd" levert dan niets op.
Wat een aanvaller zonder rootrechten níet kan vervalsen, is ctime — het moment waarop de inode zelf wijzigde. Daar zat het bewijs: index.php had een vervalste mtime, maar een ctime van negentien uur ná het laatste bezoek van de aanvaller. Met andere woorden: het ding herschreef zichzelf, autonoom, terwijl er niemand meekeek.
De valkuil bij herstel uit back-up
Het herstel verliep in twee stappen, en de tussenstap is de belangrijkste les van dit hele incident.
Eerst is een back-up van vóór de inbraak teruggezet. De malware verdween, de storing was opgelost, de sites stonden weer online. Klaar, zou je denken.
Maar een back-up van vóór de inbraak bevat per definitie ook de kwetsbare pluginversie. Binnen een uur na de restore was diezelfde kwetsbaarheid opnieuw misbruikt. Pas daarna zijn de updates doorgevoerd en was de ingang echt dicht.
Restore en patch horen bij elkaar. Zet een omgeving na een compromittering nooit online tussen de restore en de update in. Haal de site offline of achter een IP-restrictie, patch, verifieer, en pas dán open. Wie geen manier heeft om offline te updaten, ontwikkelt die het beste vóór hij hem nodig heeft.
Het is de spiegelbeeldige les van de Citrix-zerodays: daar liet een update de achterdeur staan, hier bracht een restore het lek terug. Wie na een inbraak maar één van de twee doet, is niet klaar.
Er is niets mee verdiend — en dat is juist het punt
Bij het onderzoek is geen enkele vorm van uitbating gevonden. Geen spam, geen SEO-injectie, geen cryptominers, geen voorbereide datadiefstal. Alle investering zat in het behouden van toegang.
Eén logregel geeft een aanwijzing waarom. Een dag na de inbraak haalde Telegram-infrastructuur automatisch een preview op van de URL van een van de webshells. Dat gebeurt als iemand zo'n link in een Telegram-gesprek plakt. De toegang werd op dat moment dus met iemand gedeeld — of dat een koper was, valt uit de logs niet op te maken.
Het past wel in een bekend businessmodel: de ene partij breekt in en houdt de deur open, een andere partij koopt de sleutel en bepaalt wat er gebeurt. Ransomware, datadiefstal, of gewoon doorverkopen. Wie een compromittering ontdekt vóórdat er "iets gebeurt", heeft geen mazzel gehad — die zit mogelijk in de periode tussen inbraak en verkoop.
De gebruikte toolkit is trouwens geen maatwerk. In de code staat een hardgecodeerde Chinese tijdzone en staan interne variabelenamen in pinyin — namen die een slachtoffer nooit ziet en die er geen reden is om te vervalsen. Bovendien scanden bots al ruim een jaar vóór de inbraak op de bestandsnaam van de belangrijkste backdoor. Standaardgereedschap, gebruikt door operators die via gehuurde servers in vijf landen werkten. Deze organisatie was geen doelwit, maar een kwetsbare installatie die een scanner tegenkwam.
De les voor uw organisatie
Vijf dingen die dit incident hadden voorkomen of sterk beperkt, in volgorde van rendement:
- Zet elke website in een eigen hostingpakket. Dit is de maatregel die het verschil maakte tussen "één site besmet" en "vijf sites besmet". Gedeelde subscriptions besparen een paar tientjes per maand en kosten u de hele omgeving. Vraag uw hoster expliciet om isolatie per site — het is bijna altijd mogelijk.
- Inventariseer de sites die niemand meer beheert. Elke organisatie heeft ze: een oude campagnesite, een testomgeving, een merk dat is uitgefaseerd. Ze staan niet in de onderhoudsafspraak, niemand kijkt ernaar, en ze staan wél op dezelfde server. Zet ze offline of geef ze dezelfde patchdiscipline als productie.
- Let op premium- en custom plugins. In deze omgeving liepen vier betaalde plugins jaren achter, simpelweg omdat er geen geldige licentiekoppeling was. Zonder licentie meldt WordPress geen updates — en dan ziet uw beheerder een groen dashboard bij een verouderde installatie. Controleer handmatig, of gebruik een virtuele-patchingdienst.
- Meet uw patchvenster in uren, niet in weken. Bij CVSS 9.8 is vier dagen al te lang, zoals dit incident laat zien. Spreek intern (of met uw bureau) een harde termijn af voor kritieke kwetsbaarheden en leg vast wie hem bewaakt. Hoe u dat inricht, beschreven we in ons artikel over een patchproces dat bijhoudt met aanvallers.
- Zorg dat detectie ook meldt. De malwarescanner van de hosting had de bestanden gevonden, maar stuurde geen alarm en ruimde niets op. Twee dagen lang stond het antwoord in een dashboard waar niemand keek. Zet notificaties aan, met een adres dat gelezen wordt, en controleer of een scanner alleen detecteert of ook opruimt.
En onder de Cyberbeveiligingswet?
Valt uw organisatie onder de Cyberbeveiligingswet (de Nederlandse implementatie van NIS2), dan is dit precies het scenario waarop de zorgplicht ziet. Patchmanagement, segmentatie van systemen, monitoring en incidentafhandeling zijn geen losse best practices meer maar onderdelen van uw zorgplicht — en een significant incident kent een meldtermijn van 24 uur voor de eerste melding.
Let daarbij op één ding dat in dit onderzoek niet met zekerheid vast te stellen was: of er gegevens zijn weggehaald. Er zijn geen dumps of grote uitgaande responses gevonden, maar bij een commandokanaal via cookies is het uitblijven van sporen zwak bewijs. Voor de AVG-afweging ("is dit een datalek?") betekent dat: u kunt niet aantonen dat er niets is gebeurd. Neem dat mee in uw beoordeling in plaats van het als "geen bewijs, dus geen lek" af te doen.
Indicators of compromise
Beheert u WordPress-omgevingen? Deze indicatoren zijn breder bruikbaar dan dit ene incident.
Bestandsnamen: filefuns.php (md5 0986f5a069e4ac1f4e70a41b7950228d), Nx*.php, cache-qmfsb.php, egl.php, wdg*.php, cache.php in dubbele submappen (Http/Http/), plugins met het voorvoegsel starter- die u niet zelf heeft geïnstalleerd, en elke .htaccess die de string suspected bevat.
In de code: date_default_timezone_set('PRC'), variabelenamen jue_jiang, ruzhu, eindmarkering BiaoJiOk.
IP-adressen: 62.60.130.128 (initiële toegang), 116.204.143.227 (interactief gebruik), 187.40.232.58, 187.40.232.168, 193.148.56.78, 167.99.233.101.
Praktische controle: draai wp core verify-checksums en wp plugin verify-checksums --all, en vergelijk ctime in plaats van mtime. Een aanvaller kan mtime vervalsen, ctime zonder rootrechten niet.
Bronnen
- Wordfence – WPMU DEV Dashboard <= 5.0.1: authentication bypass via Hub SSO HMAC (CVE-2026-76581)
- OpenCVE – CVE-2026-76581
- cvefeed.io – CVE-2026-15459: WPMU DEV Dashboard <= 5.0.0, authentication bypass to arbitrary plugin installation
- Severity Daily – WPMU DEV Dashboard's second unauthenticated admin bypass this month hits the sites the first one missed
- WPMU DEV – WPMU DEV Dashboard, changelog