Kort gezegd: Om de database op schadelijke code te controleren, doorzoekt u een export of kopie met alleen-lezen query's op ingebrachte scripts, vreemde adressen en versluierde code in inhoud en instellingen. Controleer daarnaast triggers, geplande events, opgeslagen procedures en de databasegebruikers. Schoon pas op na de beoordeling en nooit met blind zoeken en vervangen, want geserialiseerde gegevens raken daardoor beschadigd.
De blinde vlek van veel opschoningen
Na een inbraak controleren de meeste beheerders de bestanden. Ze vervangen de CMS-kern, verwijderen verdachte PHP-bestanden en installeren uitbreidingen opnieuw. De website ziet er daarna schoon uit en een week later stuurt ze weer door naar vreemde sites. De oorzaak ligt vaak in de database, die bij het opschonen ongewijzigd is overgenomen.
Voor aanvallers is de database een aantrekkelijke schuilplaats. Een script in een widget wordt op elke pagina uitgeleverd. Een gewijzigde adresinstelling stuurt alle bezoekers om. Een extra beheerdersaccount opent de deur voor later. Niets daarvan valt op bij een bestandsscan.
Na PHP2Shell, de kwetsbaarheden CVE-2026-109618 en CVE-2026-109619 met code-uitvoering zonder aanmelding, hoort de database daarom bij elke controle. Wie code op de server kan uitvoeren, kan in de regel de toegangsgegevens van de database uit de configuratie lezen en daar naar believen schrijven. Hoe de applicatie eigenlijk met de database praat, legt PHP en databases: PDO en mysqli uit.
Waar schadelijke code in de database zit
| Vindplaats | Typische inhoud | Effect |
|---|---|---|
| Inhoud (berichten, pagina's, producten) | <script>-blokken, onzichtbare links, <iframe> |
Schadelijke code of spam voor bezoekers |
| Instellingen en opties | gewijzigd websiteadres, vreemde scripts in widgets of thema-opties | Doorverwijzingen, scripts op alle pagina's |
| Gebruikerstabellen | extra beheerders | blijvende toegang |
| Triggers | code die bij elke opslag inhoud wijzigt | Schadelijke code keert na opschoning terug |
| Geplande events | terugkerende taken in de databaseserver | Wijzigingen zonder webtoegang |
| Databasegebruikers | extra accounts met ruime rechten | Toegang buiten het CMS om |
De laatste drie regels worden bijzonder vaak over het hoofd gezien, omdat ze niet in de tabellen van het CMS staan maar in de databaseserver zelf.
Bij webwinkels verdient één vindplaats extra aandacht: instellingen waarmee extra scripts in de kassa en bij het afronden van de bestelling worden ingevoegd. Een vreemd script op die plek kan invoer van klanten meelezen voordat die versleuteld naar de betaaldienst gaat. Controleer daarom gericht alle velden voor code in kop- en voettekst, trackingintegraties en ontwerpinstellingen die op kassapagina's werken.
Denk bovendien aan andere databases op dezelfde server. Test- en stagingkopieën, oude projectversies of databases van andere websites staan er vaak naast en zijn met dezelfde toegangsgegevens bereikbaar. Wat in de live-database wordt gevonden, moet ook daar worden gecontroleerd voordat een kopie later weer als basis dient.
Database controleren op schadelijke code: inhoud en instellingen
Werk op een export. Die is tegelijk bewijsmateriaal en zoekbasis. De export is als tekstbestand te doorzoeken zonder de draaiende database te belasten:
# Consistente export maken (toegangsgegevens worden gevraagd)
mysqldump --single-transaction --routines --triggers --events -u BENUTZER -p DATENBANK > export.sql
sha256sum export.sql > export.sql.sha256
# Typische patronen in de export tellen
grep -oiE '<script[^>]{0,80}' export.sql | sort | uniq -c | sort -rn | head -30
grep -oiE '(eval\(|base64_decode|fromCharCode|document\.write|atob\()' export.sql | sort | uniq -c
grep -oiE 'https?://[a-z0-9.-]+' export.sql | sort | uniq -c | sort -rn | head -50
De laatste query somt alle domeinen op die in de database voorkomen, op frequentie. Vreemde domeinen die niemand in het team kent, zijn de belangrijkste kandidaten. Voor gerichte query's in de database zelf zijn SELECT-opdrachten geschikt. Het voorbeeld gebruikt WordPress-tabellen; het voorvoegsel en de tabelnamen van andere systemen staan in hun documentatie:
-- Instellingen met scripts of verdachte functies
SELECT option_name, LENGTH(option_value) AS laenge
FROM wp_options
WHERE option_value LIKE '%<script%'
OR option_value LIKE '%eval(%'
OR option_value LIKE '%base64_decode%'
ORDER BY laenge DESC;
Vermeldingen met een zeer grote inhoud verdienen bijzondere aandacht. Achtergronden over WordPress-specifieke schuilplaatsen biedt de gids Backdoors in WordPress vinden.
Triggers, events en databaseaccounts
Triggers en geplande events zijn in veel webapplicaties helemaal niet voorzien. Als ze bestaan, moet iemand kunnen uitleggen waarom. Deze query's lezen alleen uit de systeemtabellen van MySQL en MariaDB:
-- Triggers in alle databases
SELECT TRIGGER_SCHEMA, TRIGGER_NAME, EVENT_OBJECT_TABLE, CREATED
FROM information_schema.TRIGGERS;
-- Geplande events
SELECT EVENT_SCHEMA, EVENT_NAME, STATUS, CREATED, LAST_EXECUTED
FROM information_schema.EVENTS;
-- Opgeslagen procedures en functies
SELECT ROUTINE_SCHEMA, ROUTINE_NAME, ROUTINE_TYPE, CREATED
FROM information_schema.ROUTINES
WHERE ROUTINE_SCHEMA NOT IN ('mysql', 'sys', 'information_schema', 'performance_schema');
-- Databasegebruikers
SELECT user, host FROM mysql.user ORDER BY user;
Gebruikt uw applicatie PostgreSQL of een ander databasesysteem, dan heten systeemtabellen en opdrachten anders; het principe blijft gelijk. Voor de analyse is een eigen databasegebruiker aan te raden die alleen mag lezen. Zo kan ook een typefout in een query niets wijzigen en laat de controle geen sporen na in de gegevens.
Een trigger die bij het opslaan van een bericht code toevoegt, verklaart waarom schadelijke code na elke opschoning terugkeert. Onbekende databasegebruikers horen evenzeer bij de accountcontrole als beheerdersaccounts in het CMS; meer daarover onder Vreemde beheerdersaccounts in CMS en server vinden.
Netjes opschonen zonder gegevens te vernietigen
Het opschonen is het netelige deel. Deze volgorde heeft zich bewezen:
- Vondsten beoordelen: elke treffer wordt ingedeeld als bekend, onschuldig te verklaren of vreemd. Tracking- en kaartcodes zijn vaak legitiem.
- Toegangspoort sluiten: PHP bijwerken naar een verholpen versie (8.5.12, 8.4.27, 8.3.36, 8.2.35 of bevestigde backport), alle workers herstarten, applicatie updaten.
- Gericht verwijderen: vreemde inhoud opschonen via de applicatie of met hulpmiddelen die geserialiseerde gegevens correct behandelen. Vreemde triggers, events en accounts verwijderen.
- Toegangsgegevens wijzigen: nieuw databasewachtwoord, in de configuratie van de applicatie invoeren, rechten van de applicatiegebruiker beperken tot de eigen database.
- Natesten: de zoekquery's na enkele dagen herhalen.
Vervang niets met zoeken en vervangen in de SQL-export. Geserialiseerde PHP-gegevens slaan de lengte van elke tekenreeks mee op. Verandert de lengte, dan wordt de instelling onleesbaar en kan de website uitvallen.
Doorverwijzingen die ondanks een schone database optreden, hebben hun oorzaak in een andere laag. Het artikel De website stuurt door naar vreemde sites helpt bij het afbakenen.
Hoe helpt PHP SOS concreet?
- Wij stellen de database veilig met controlesom en doorzoeken de export op scripts, vreemde domeinen en versluierde code.
- Wij controleren triggers, events, procedures en databasegebruikers die buiten het CMS liggen.
- Wij schonen vondsten op in het kader van de verwijdering van malware, zonder geserialiseerde gegevens te beschadigen.
- Wij beperken de rechten van de applicatiegebruiker en herhalen de controle na het opschonen.
Veelgestelde vragen
Kan schadelijke code in de database zitten?
Ja. Aanvallers schrijven JavaScript in berichten, productbeschrijvingen of widgets, wijzigen adresinstellingen voor doorverwijzingen of maken beheerdersaccounts aan. In MySQL en MariaDB kunnen bovendien triggers en geplande events gegevens wijzigen. Een bestandsscan vindt daar niets van.
Hoe doorzoek ik de database zonder iets kapot te maken?
Werk op een export of een kopie en gebruik alleen SELECT-query's. Een export is bovendien als tekstbestand met grep te doorzoeken. Wijzigingen volgen pas als elke vondst is beoordeeld en er een back-up is.
Waarom moet ik vondsten niet opschonen met zoeken en vervangen in de export?
Veel CMS'en slaan instellingen op als geserialiseerde PHP-gegevens, waarbij de lengte van elke tekenreeks wordt meegeslagen. Een vervanging wijzigt de lengte maar niet de opgegeven waarde, waardoor de instelling onleesbaar wordt. Schoon op via de applicatie of met hulpmiddelen die geserialiseerde gegevens begrijpen.
Is elk script in de database verdacht?
Nee. Trackingcodes, kaarten, video's of chatwidgets worden eveneens als script opgeslagen. Beslissend is of iemand in het team de vermelding en het doeladres kent.