TL;DR: Das Frontend war wirklich sauber: keine Analytics, keine Pixel, keine Dritt-Fonts, null Cookies vor Consent, blockierte Dotfiles, nur HTTPS. Das CMS dahinter lebte auf einer eigenen Subdomain — und dieser Host hatte keinen Clickjacking-Schutz am Admin-Login, einen Staging-Zwilling ohne die Basic Auth des Staging-Frontends, eine API, die Datensätze auslieferte, die die Oberfläche bewusst ausblendet, Formulare, deren einzige Prüfung im Browser lief, und eine Mail-Domain mit SPF-Hardfail, aber ohne DMARC und ohne DKIM. Nichts davon ist Remote Code Execution. Zusammen ist es ein gezielter Phishing-Baukasten: namentliche Mitarbeiter, Durchwahlen, Mobilnummern, ein Login, das sich auf dem echten Origin framen lässt, und eine Domain, deren Name sich ohne Policy fälschen lässt.
Die Ausgangslage
Ein mittelständischer Zulieferer bat mich, seine Website anzusehen. Das Unternehmen verkauft an Kliniken und Pflegeeinrichtungen — seine Kunden sind Einkauf, technische Leitung und Architekturbüros. Menschen also, die Angebote, Rechnungen und Projektdokumente per Mail bekommen und darauf trainiert sind, auf den Absender zu achten.
Die öffentliche Seite ist ein vorgerendertes JavaScript-Build, ausgeliefert als statische Dateien hinter einem normalen Webserver. Eine getrennte Anwendung auf einer Subdomain hält die Inhalte und die Admin-Oberfläche. Im Zertifikatsregister tauchten zwei weitere Hostnamen auf: ein Staging-Frontend und eine Staging-Version derselben Anwendung.
Ich habe zuerst das Frontend angesehen. Dann habe ich aufgehört, das Frontend anzusehen, weil alles Interessante auf dem anderen Host lag.
Dies ist ein Muster-Essay, kein Fallbericht. Es gibt darin kein Land, kein Produkt, keinen Hostnamen, keine Version, kein Datum und keine Zahl aus dem Engagement. Die Mechanik bleibt gleich, das Ziel ist nicht rekonstruierbar.
Was das Frontend richtig macht
Das gehört klar gesagt, denn der Rest des Artikels handelt davon, was schiefging:
- Keine Dritt-Requests außer dem Consent-Tool. Kein Tag Manager, keine Analytics, keine Werbe-Pixel, keine Karten-Einbettung, kein Video-Embed, keine Web-Fonts von einem CDN. Schriften liegen lokal. Der einzige externe Origin auf der Seite ist das CMP selbst.
- Null Cookies vor Consent. In einem frischen Browser-Profil gemessen: Startseite, Blog und Kontaktseite setzen nichts. Kein Banner, das „blockiert" — es gab nichts zu blockieren.
- HTTPS überall. HTTP antwortet auf jedem Host mit einem permanenten Redirect, HSTS schließt Subdomains ein, die Zertifikatskette ist gültig, TLS 1.0 und 1.1 werden abgelehnt.
- Langweilige Härtung, die funktioniert.
nosniff, Referrer-Policy, TRACE abgelehnt, Dotfiles und Environment-Dateien liefern 403, keine Verzeichnis-Listings, keine Source Maps, keine Framework-Debug-Seiten, keine Stacktraces. - Eine kleine API. Drei Lese-Endpunkte und zwei Formular-Endpunkte. Keine User-Liste, kein Admin-Endpunkt, keine halbfertigen internen Routen.
Das ist besser als die meisten Seiten, die ich mir ansehe. Und genau deshalb ist der Rest wichtig: Jemand hat sich um dieses Frontend gekümmert. Dieselbe Sorgfalt hat den Host nebenan nicht erreicht.
Der zweite Host ist die eigentliche Eingangstür
Das CMS lebte auf api. — ein anderer Origin, ein anderer Stack, ein anderer Satz Header. Das ist eine normale Architektur: statische Seite für Geschwindigkeit, Anwendung für die Redaktion. Das Problem ist, dass Teams die Website prüfen und bei der Website aufhören. Die Anwendung ist in keiner Checkliste „die Website".
Was dem Anwendungs-Host fehlte — in Produktion und in Staging:
- Kein
X-Frame-Options, keinframe-ancestors. Die Admin-Login-Seite ließ sich in jede beliebige Seite einbetten. Ein geframtes Login auf dem echten Origin, mit echtem Zertifikat und echter URL-Zeile, ist die überzeugendste Zugangsdaten-Phishing-Seite, die es gibt — und sie braucht keine nachgebaute Domain. - Keine Content-Security-Policy. Bei einer SPA-Admin mit Session-Cookie fällt damit die letzte Mitigation gegen eingeschleustes JavaScript weg.
- Eine Versionsangabe in der Login-Seite. Für sich keine Schwachstelle; nützlich, um auszuwählen, welches öffentliche Thema man zuerst probiert.
Das Login selbst war CSRF-geschützt — sechs POSTs ohne Token wurden abgelehnt — und das Session-Cookie war Secure, HttpOnly, SameSite=Lax. Jemand hat die Framework-Defaults gesetzt. Nur die zwei Header, die einen Browser davon abhalten, die Seite zu framen, fehlten.
Der Staging-Zwilling, den niemand schützt
Das Staging-Frontend lag hinter HTTP Basic Auth. Ein neugieriger Besucher bekam eine Passwortabfrage und ging weiter.
Die Staging-Anwendung hatte diesen Schutz nicht. Gleiche Login-Seite, gleiche API, gleiche Versionsangabe, gleiches Framework — erreichbar für jeden, der das Zertifikatsregister liest. Die robots.txt dort verbot auch nichts.
Das ist die häufigste Aufteilung, die ich in mittelständischen Unternehmen finde: Das Environment mit der hübschen URL und der sichtbaren Seite bekommt ein Passwort, und das Environment mit der Datenbank, der Admin-Session und der echten Angriffsfläche nicht. Staging ist außerdem der Ort, an dem Zugangsdaten am schwächsten sind — wiederverwendete Passwörter, Testkonten, eine Datenbank, die „nur kurz" aus Produktion zurückgespielt wurde — und an dem niemand die Logs ansieht.
Ein ungeschütztes Staging-Backend ist kein Datenleck. Es ist ein kostenloser Trainingsplatz für den Angriff auf Produktion.
Die API weiß nichts von deinen Filtern
Die Kontaktseite wurde von einem öffentlichen Endpunkt gespeist, der Namen, Rollen, Durchwahlen und Mobilnummern der Mitarbeiter lieferte. Das ist eine bewusste Veröffentlichung: Der Vertrieb will erreichbare Menschen. In Ordnung.
Zwei Datensätze waren mit showInContacts: false markiert. Die Oberfläche respektierte das Flag und zeigte sie nicht. Die API lieferte sie trotzdem — in der Liste und über die Einzelabfrage — inklusive eines Seed-Platzhalters mit Testadresse, einer einzeiligen Telefonnummer und Alternativtext, in dem wörtlich „Alternativtext Deutsch" stand.
Es sind keine Kundendaten geleakt. Geleakt ist die Annahme: Das Team hielt ein Boolean in der Datenbank für eine Zugriffskontrolle. Es war eine Anzeigeeinstellung. Jedes Feld, das du zurückgibst, ist veröffentlicht; jeder Filter, der im Browser läuft, ist ein Vorschlag.
Das ist dasselbe Muster wie eine Mediathek, in der unveröffentlichte Bilder herunterladbar bleiben, oder ein Shop-API, das Entwürfe auflistet. Das Objekt ist öffentlich, sobald ein Endpunkt es benennen kann.
Formulare, die dem Browser vertrauen
Das Bewerbungsformular nahm einen Lebenslauf und Anhänge an. Der Client-Code war sorgfältig: erlaubte Endungen, Größenlimit pro Datei, Gesamtlimit, ein Honeypot-Feld, eine Datenschutz-Checkbox. Alles davon lief im Browser — und alles davon ist optional für jeden, der direkt postet.
So sah der Endpunkt von außen aus:
- Kein CSRF-Token. Die API ist zustandslos und nahm den POST ohne Token an.
Access-Control-Allow-Origin: *. Jede Website konnte dorthin posten. Es gibt keine Credentials zu stehlen, das ist also kein „CORS mit Credentials" — es ist Cross-Site-Schreibzugriff auf eine Geschäfts-Inbox.- Dateien als Base64 in JSON, in der Form
{Name, Type, Content}. Der Browser liest jede Datei, entfernt das Data-URL-Präfix und schickt den Rest als Text. Typ und Name kommen vom Client. Eine Validierung müsste serverseitig alles dekodieren und neu prüfen — und das clientseitige Größenlimit ist genau die Regel, die in diesem Pfad gern übersprungen wird. 500 Server Errorbei leerem Body, nicht422. Ein Validierung-first-Design liefert Feldfehler. Ein Serverfehler bedeutet, dass der Request Code erreicht hat, der Daten erwartete — und dass jeder kaputte Request eine Exception ins Log schreibt.
Ich habe keine Datei hochgeladen. Ein Live-Upload schreibt in das System des Kunden, und das Mandat war nicht-intrusiv. Der ehrliche Befund ist deshalb das Design, nicht ein bewiesener Bypass: Die einzigen Prüfungen, die ich verifizieren konnte, waren die im Browser, und der Endpunkt akzeptierte unauthentifizierte Cross-Origin-Posts.
Für ein Unternehmen, das einstellt, ist eine gefälschte Bewerbung mit Anhang ein Zustellvehikel, das über die eine Inbox kommt, in der das Öffnen von Dateien zum Job gehört.
Mail sind drei Records, und zwei fehlten
SPF war korrekt und hart: -all, mit dem Mail-Provider und einem Signaturdienst autorisiert. Das blockiert Envelope-Spoofing.
Dann:
- kein DMARC-Record — also keine Policy und kein Reporting;
- kein DKIM-Selektor — nichts signiert die Domain;
- kein MTA-STS, kein TLS-Reporting, kein CAA.
Ein gefälschtes From: mit einem vom Angreifer kontrollierten Envelope besteht SPF für diesen Envelope. Ohne DMARC gibt es keine Alignment-Prüfung, also sagt nichts dem empfangenden Server, dass der sichtbare Absender eine Lüge ist. Die Gegenseite sieht eine plausible Mail von der echten Domain des Zulieferers, mit dem echten Logo, adressiert an eine namentlich bekannte Person, deren Durchwahl auf der Website steht.
Das ist der Geldpfad: eine Angebotskorrektur, eine Rechnung, eine geänderte Bankverbindung. Kein technischer Exploit — ein Angriff auf einen Geschäftsprozess. Und in einer Lieferantenbeziehung, in der täglich Dokumente wandern, ist die Mail das Produkt.
Zwischenstand: die Laufzeit wurde zum Ziel
Während dieses Engagement geschrieben wurde, veröffentlichte das BSI einen Hinweis, der genau auf den oben beschriebenen Host-Typ fällt: den zweiten Host, den niemand prüft.
Am 21. Oktober 2026 veröffentlichte das PHP-Projekt Notfall-Patches für zwei Remote-Code-Execution-Schwachstellen in der PHP-Engine selbst — CVE-2026-109618 (OOM-Korruption) und CVE-2026-109619 (verbliebener globaler Zustand / Use-after-free), beide CVSS v4.0 9,2, beide ohne Anmeldung ausnutzbar. Korrigierte Releases: PHP 8.5.12, 8.4.27, 8.3.36 und 8.2.35; ältere Zweige erhalten keine offizielle Korrektur. Der BSI-Hinweis ist BITS-H Nr. 2026-297981-1132 (Version 1.1, 21. Oktober 2026, Kritikalität 3/Orange, TLP:CLEAR).
Zwei Details machen daraus mehr als eine Patch-Notiz:
- Es greift, bevor dein Code läuft. Der demonstrierte Pfad ist Apache mit
mod_php, wo die verwundbare Sequenz während des Request-Starts auftritt — die Anwendung kommt nie dazu, etwas zu validieren, zu loggen oder zu begrenzen. - Es wurde ausgenutzt, bevor es öffentlich war. Ein Ausnutzungsversuch wurde vier Tage vor der Offenlegung beobachtet; nach dem Patch-Release stiegen die Versuche stark an.
Für dieses Muster ist die Lehre unangenehm: Ein statisches Frontend kann perfekt sein, und die PHP-Laufzeit auf dem CMS-Host ist trotzdem die schwächste Komponente im Bestand. Der Host, der nur zum Bearbeiten von Inhalten existiert, ist der, der den Interpreter bei jedem Request ausführt — auch für die Login-Seite, die API und die Formular-Endpunkte aus den vorherigen Abschnitten.
Dieselbe Logik gilt für jedes CMS im Bestand: Die Version der Laufzeit ist eine Eigenschaft des Hosts, nicht des Repositories — und niemand besitzt sie, solange sie niemandem zugewiesen ist.
Das Zertifikatsregister ist die Landkarte
Keiner der Hostnamen oben wurde geraten. Sie standen in öffentlichen Zertifikatslogs — denselben Logs, die es gibt, damit TLS prüfbar ist. Gruppiert man die Einträge nach Namen, erscheint die Landschaft: Produktion, Staging, API, ein alter Alias. Erst Struktur, dann Ziele.
Die Kette
Befunde
| Schwere | Befund | Beleg |
|---|---|---|
| HOCH | Mail-Domain mit SPF-Hardfail, aber ohne DMARC und ohne DKIM — ein gefälschtes From ist zustellbar und niemand meldet es | Autoritatives DNS: kein _dmarc, acht DKIM-Selektoren leer, kein MTA-STS, kein CAA |
| MITTEL | Der Staging-Zwilling von Admin und API ist ohne die Basic Auth erreichbar, die das Staging-Frontend schützt | 401 am Staging-Frontend, 302 → /login an der Staging-API |
| MITTEL | Der Admin-Login lässt sich nicht gegen Framing schützen: kein X-Frame-Options, kein CSP-frame-ancestors | Response-Header in Produktion und Staging |
| MITTEL | Keine CSP und keine Permissions-Policy auf Marketing-Site und API-Host; die CMS-Inhalte selbst sind sauber | Header-Capture; 40 Datensätze geprüft, 0 Script-Tags, 0 Inline-Handler, 0 Embeds |
| MITTEL | Die öffentliche API liefert Datensätze, die die Oberfläche bewusst ausblendet — in der Liste und über die Einzelabfrage | Kontakt-Endpunkt vs. Einzelabfrage eines markierten Platzhalters |
| MITTEL | Formular-Endpunkte nehmen unauthentisierte Cross-Origin-Posts an, prüfen nur im Browser und antworten mit 500 statt 422 | Access-Control-Allow-Origin: *; Leer-Body-POST; Client-Bundle |
| MITTEL | Die Datenschutzerklärung nennt einen Hoster, der nicht das Netz ist, aus dem die Seite kommt; CMP und Mail-Processor unbenannt | Erklärungstext vs. RIPE-WHOIS der öffentlichen IP |
| NIEDRIG | Das einzige Dritt-Skript (Consent-Tool) lädt ohne Integritäts-Hash | DOM-Inventar: null integrity-Attribute |
| NIEDRIG | Canonical und Sitemap zeigen auf den Apex, der per 301 auf www leitet; ein veröffentlichter Artikel fehlt in der Sitemap | Apex-Redirect; Sitemap-Diff |
| NIEDRIG | Staging-API indexierbar; kein security.txt; kein CAA-Record | Leeres Disallow; 404 auf /.well-known/; DNS |
| GOOD | Frontend: null Dritt-Requests außer dem CMP, null Cookies vor Consent, selbst gehostete Schriften, nur HTTPS mit HSTS, Dotfiles und Environment-Dateien 403, keine Source Maps; Admin-Session-Cookie Secure/HttpOnly/SameSite=Lax, Login CSRF-geschützt, Admin-API 401 | Frisches Profil, Pfad-Probes, Header-Capture |
So siehst du es selbst
Alles oben ist aus öffentlichen Daten sichtbar:
# die Landschaft, aus öffentlichen Zertifikatslogs
curl -s "https://crt.sh/?q=%25.example.tld&output=json" | jq -r '.[].name_value' | sort -u
# Eingangstür versus zweiter Host
curl -sI https://www.example.tld/ | grep -iE 'strict-transport|content-security|x-frame'
curl -sI https://api.example.tld/ | grep -iE 'strict-transport|content-security|x-frame'
# der Staging-Zwilling: ein Host geschützt, der andere nicht
curl -s -o /dev/null -w '%{http_code}\n' https://stage.example.tld/
curl -s -o /dev/null -w '%{http_code}\n' https://api.stage.example.tld/
# das Organigramm, das die Website veröffentlicht
curl -s https://api.example.tld/api/contacts | jq '.[] | {name, email, phone, mobile, showInContacts}'
# Mail-Policy
dig +short TXT _dmarc.example.tld
dig +short TXT selector1._domainkey.example.tld
Zwei Regeln für das Lesen des Ergebnisses:
- Hosts vergleichen. Eine Seite kann exzellent sein und ihr API-Host mittelmäßig. Prüfe die Landschaft, nicht die Seite.
- Prüfe, was der Endpunkt zurückgibt, nicht was die Seite rendert.
showInContacts,published,visible,status— jedes dieser Felder ist ein UI-Konzept, bis eine serverseitige Query es durchsetzt.
Detection: was das gefangen hätte
- Asset-Inventar inklusive Subdomains und Environments. Jeder Host im Zertifikatsregister mit Owner, Environment-Label und Kontrollsatz.
api.stagewäre als „Staging, keine Basic Auth" aufgetaucht. - Header-Baseline pro Host, nicht pro Site. Eine Policy für den Marketing-Host und den Admin-Host, automatisch geprüft. Ein fehlendes
frame-ancestorsauf einer Login-Seite ist eine Ein-Zeilen-Assertion. - Mail-Policy als überwachter Record. DMARC, DKIM und SPF wöchentlich prüfen; Alarm, wenn ein Record verschwindet (er verschwindet oft während einer Mail-Migration).
- Ein API-Contract-Test, der Filter behauptet. „Datensätze mit
showInContacts=falsedürfen nicht in/contactserscheinen" ist ein Test, kein Code-Review. - Formular-Endpunkte in der Negativ-Test-Suite. Leerer Body muss 422 liefern; ein Cross-Origin-POST muss abgelehnt oder rate-limitiert werden; ein zu großer Anhang muss vor dem Dekodieren scheitern.
- Laufzeit-Inventar pro Host. Nicht pro Repository — pro Host, inklusive CMS und API-Subdomain. Der Interpreter, der jeden Request parst, ist die Komponente, die dein Anwendungscode nicht schützen kann.
Was der Verteidiger mitnehmen sollte
- Prüfe die Landschaft, nicht die Website. CMS-Host, API-Host und Staging-Zwilling gehören zum Perimeter, auch wenn sie nicht im Footer verlinkt sind.
frame-ancestorsauf jede Login-Seite. Kostet einen Header und entfernt ein ganzes Phishing-Genre.- Staging bekommt dieselbe Netzkontrolle wie Produktion — oder geht offline. Ein Basic-Auth-Prompt auf dem hübschen Host und eine offene Tür auf dem Datenhost ist die schlechteste Kombination.
- Sichtbarkeit in der Query erzwingen. Ein Boolean, den nur das Frontend respektiert, ist Dokumentation, keine Autorisierung.
- Serverseitig validieren — und es beweisen. 422 zurückgeben, Body vor dem Dekodieren begrenzen, die dekodierte Datei sniffen, den Base64-in-JSON-Transport ersetzen.
- DMARC veröffentlichen und Mail signieren. SPF allein sagt Empfängern, welche Server senden dürfen; DMARC sagt ihnen, was zu tun ist, wenn der sichtbare Absender lügt.
- Das Mitarbeiterverzeichnis ist eine Zielliste. Durchwahlen zu veröffentlichen ist eine Vertriebsentscheidung; sie für eine fälschbare Domain zu veröffentlichen ist eine Risikoentscheidung.
Fazit
Die Website war nicht das Problem. Die Website war der Teil, den alle schon repariert hatten — keine Tracker, keine Cookies, keine Dritt-Skripte, ein sauberer Header-Satz und ein gültiges Zertifikat.
Das Problem war die Anwendung dahinter: ein Login, das sich framen ließ, ein Staging-Zwilling, der sich durchsuchen ließ, eine API, die veröffentlichte, was die Oberfläche verbarg, Formulare, die dem Browser vertrauten, und eine Mail-Domain, die sich ohne Widerspruch fälschen ließ. Nichts davon ist exotisch. Alles davon ist Routine — und alles davon reicht, um eine überzeugende Nachricht an eine namentlich bekannte Person zu bauen.
Das Frontend war sauber. Der Perimeter war es nicht.
Once an attacker gains access, they can perform the following actions:
Alle Identifikatoren entfernt — Branchendetail, Land, Größe, Hostnamen, Versionen und Daten sind bewusst generisch, und keine Zahl aus dem Engagement erscheint hier. Es wurden keine Kundensysteme über öffentliche Lese-Requests hinaus berührt, keine Zugangsdaten verwendet, keine Datei hochgeladen und keine Ausnutzung durchgeführt.