Elementrica
03Fallstudien04Referenzen05Unternehmen06Aktuelles07Kontakt
PLENDE

159 CVEs in Bibliotheken. Der Fehler, der am meisten wehtat, hat keine CVE-Nummer

Penetrationstest
159 CVEs in Bibliotheken. Der Fehler, der am meisten wehtat, hat keine CVE-Nummer

Der Kunde gab uns die Repositories zweier Anwendungen und eine Frage, die ein Scanner nicht beantwortet: Was davon tut uns wirklich weh.

Wir führten eine statische Analyse des Codes und der Abhängigkeiten durch. Sie ergab 115 Feststellungen, davon 68 mit hoher Schwere. Die gefährlichste hatte keine CVE-Nummer, leuchtete in keiner Übersicht rot auf und passte in eine einzige Zeile PHP.

Von einem Zähler bei 159 CVEs führte der Weg zu einer einzigen Methode, die md5($pass) berechnet. Dazu die Abbildung auf MITRE ATT&CK für das Blue Team.

Kontext

Wir begannen dort, wo alle beginnen: Wir ließen die Abhängigkeitsanalyse laufen und sahen zu, wie der Zähler auf 159 kletterte. Prototype Pollution, ReDoS, SSRF, Cross-Realm Object Access im Bundler. Eine hübsche Liste. Drei Stunden später lasen wir die Nutzer-Entität Zeile für Zeile und fanden eine Methode, die md5($pass) zurückgab. Ohne Salt, ohne Kostenfaktor, ohne alles.

Der Umfang umfasste zwei Teile des Systems: ein Bedienpanel in einer Angular-Version, die 2018 erschien und 2020 den Support verlor, und ein älteres Administrationspanel auf jQuery-Basis mit einem PHP-Backend auf einem MVC-Framework. Dazu ein Zahlungsmodul, das einen externen Anbieter anbindet, und eine interne Suchmaschine. Wir bekamen die vollständigen Quellen, beide Lock-Dateien und die Erlaubnis, alles zu lesen. Das kommt seltener vor, als man denkt, und es verkürzt den Weg zu brauchbaren Schlüssen erheblich.

Man muss dem Kunden gerecht werden. Die Repositories hatten konsistente Lock-Dateien, der externe Verkehr lief über TLS, die Panels waren getrennt, und das Team bestellte den Test selbst, statt zu warten, bis eine Regulierung ihn erzwingt. Auch die Klasse mit md5 entstand nicht aus Nachlässigkeit. Sie überlebte mehrere Entwicklungsteams, und niemand hatte einen Grund, zu ihr zurückzukehren, denn sie funktionierte ja.

Die Herausforderung

Die SCA-Analyse lieferte ein Bild, das jeder kennt, der einmal einen Scanner auf ein Frontend-Projekt losgelassen hat. Im Abhängigkeitsbaum 62 Pakete, davon 60 mit mindestens einer bekannten Schwachstelle, 159 Treffer, 106 eindeutige CVEs. Nähme man das wörtlich, hätte das Team bis zum Jahresende zu tun und null Gewissheit, dass irgendetwas davon das Risiko senkt.

Denn die meisten dieser Treffer steckten in Entwicklungsabhängigkeiten, denen fürs Bauen und Testen. webpack-dev-server, karma, webpack-dev-middleware, terser, loader-utils. Code, der nie in den Browser des Nutzers gelangt und nie in Produktion läuft. Die kritische Prototype Pollution in loader-utils ist für eine CI-Pipeline real, wenn diese nicht vertrauenswürdigen Code baut. In der Anwendung, die der Nutzer sieht, tut sie nichts.

Ein Scanner hat eine Stärke und eine Schwäche. Er langweilt sich nie und er denkt nie. Der Abhängigkeitsbaum enthielt drei verschiedene Versionen von semver, einer Bibliothek, deren ganze Aufgabe der Versionsvergleich ist. Sie war sich mit sich selbst nicht einig. Dazu zwei Versionen von json5, zwei von cross-spawn, drei von ws.

$ npm ls semver          # wynik przycięty
[email protected]
├─┬ <toolchain builda>
│ └── [email protected]
├─┬ <runner testów>
│ └── [email protected]
└── [email protected]

Ähnlich beim Frontend des Administrationspanels. Ein Formularvalidierungs-Plugin in einer für ReDoS anfälligen Version wurde zehnmal gezählt, weil neben der Quelle minifizierte Builds und Übersetzungsdateien lagen: Niederländisch, Finnisch, chilenisches Spanisch und einige andere. Jede Datei als eigene Feststellung. Ein regulärer Ausdruck wird nicht gefährlicher, nur weil ein Verzeichnis sechs Lokalisierungsdateien enthält.

Die eigentliche Frage lautete also anders als die nach der Zahl der CVEs. Sie lautete: Was davon ist erreichbar für jemanden, der nur einen Browser und ein Konto in der Anwendung hat.

Was wir taten

Wir teilten das SCA-Ergebnis in drei Gruppen: Code, der beim Nutzer läuft, Code, der auf dem Server läuft, und Code, der nur zur Build-Zeit lebt. Erst nach diesem Schnitt begann die Liste etwas zu bedeuten. Von den 159 Treffern blieben in der Gruppe Nutzer oder Server rund ein Dutzend übrig.

Dann gingen wir von Hand durch alles, was die Regel zum schwachen Hash-Algorithmus markiert hatte. Elf Treffer. Zehn davon steckten in der Integrationsbibliothek des Zahlungsanbieters, in den Funktionen, die die Signatur der Anfrage berechnen. Dieser Anbieter verlangt MD5 in seiner älteren API, und dort gibt es keine Entwurfsentscheidung zu treffen, nur die Konformität mit fremder Spezifikation. Wir beschrieben das als akzeptiertes Risiko, mit der Empfehlung, auf die neuere Schnittstellenversion zu migrieren, in der die Signatur mit SHA-384 berechnet wird.

Der elfte Treffer sah so aus:

class User extends Entity
{
    protected $attributes = [
        'email' => null,
        'password' => null,
        // ... trzydzieści kilka pól adresowych i rozliczeniowych
    ];

    public static function getHashedPassword($pass)
    {
        return md5($pass);
    }
}

Die ganze Klasse verarbeitet die personenbezogenen Daten des Kunden: Adresse, Rechnungsdaten, Abrechnungsdaten. Am Ende steht eine Methode, die ein Passwort ohne Salt und ohne jeden Rechenaufwand in einen MD5-Hash verwandelt. Das ist CWE-916, die Verwendung einer Hash-Funktion mit unzureichendem Rechenaufwand, und A02:2021 in den OWASP Top 10. Öffentlich verfügbare Hashcat-Ergebnisse für Modus 0 auf einer einzelnen modernen Grafikkarte erreichen mehrere zehn Milliarden Hashes pro Sekunde. Eine Datenbank mit solchen Passwörtern ist nach einem Leak nicht verschlüsselt, sie ist um ein paar Stunden verzögert.

Fehlendes Salt hat noch eine Folge, über die seltener gesprochen wird. Gleiche Passwörter ergeben gleiche Hashes, also sieht man ohne irgendetwas zu knacken, welche Nutzer dasselbe Passwort haben, und welches Passwort in der Datenbank am häufigsten ist. Ein GROUP BY genügt.

Beim Lesen des Codes fanden wir außerdem ein konstantes, in den Quellen fest verdrahtetes Salt, das zum Markieren von Inhaltsfragmenten dient (CWE-798), Kommunikation mit der internen Suchmaschine über einfaches HTTP, mit in der Konfiguration fest verdrahteter Adresse und Port, sowie ein Backend auf einer für CVE-2023-46240 anfälligen Framework-Version. In dieser Version kann selbst in Produktion ein vollständiger Fehlerbericht samt Stack Trace erscheinen. Letzterer ist gerade deshalb tückisch, weil er dem Angreifer für sich genommen nichts gibt, aber beim ersten 500er-Fehler Dateipfade, Versionen und die Struktur der Anwendung preisgibt.

Wir bildeten die Feststellungen auf MITRE ATT&CK ab, damit das Team aufseiten des Kunden sie in derselben Sprache hat, in der es Erkennungsregeln schreibt. Ein Vorbehalt: Das war eine statische Analyse, die Abbildung zeigt also, was sich damit tun lässt, nicht was wir getan haben.

IDTechnikWas das in diesem Code bedeutet
T1190Exploit Public-Facing ApplicationVeraltete, von außen erreichbare Komponenten, samt Fehleroffenlegung durch das Framework
T1110.002Brute Force: Password CrackingUngesalzene MD5-Hashes, offline knackbar nach jedem Datenbank-Leak
T1078Valid AccountsGeknackte Passwörter funktionieren in der Anwendung, bei Passwort-Wiederverwendung auch in anderen Diensten
T1552.001Unsecured Credentials: Credentials In FilesSalt und Verbindungsparameter fest im Repository verdrahtet
T1040Network SniffingAnfragen an die Suchmaschine über HTTP innerhalb des Netzes
T1499.003Endpoint DoS: Application Exhaustion FloodReDoS in Validierungsbibliotheken und im eigenen Code

Das Ergebnis

Im Bericht landeten 115 Feststellungen, davon 68 hoch, 37 mittel und 10 niedrig. Für die erste Welle der Behebung benannten wir vier Dinge.

Die Umstellung der Passwortspeicherung auf Argon2id, gemäß dem OWASP Password Storage Cheat Sheet und NIST SP 800-63B, ohne einen Reset für alle Nutzer auf einmal zu erzwingen. Ein Rehash beim ersten korrekten Login erledigt das still:

public function verifyAndUpgrade(string $plain, string $stored, int $userId): bool
{
    if (str_starts_with($stored, '$argon2id$')) {
        return password_verify($plain, $stored);
    }

    if (!hash_equals($stored, md5($plain))) {
        return false;
    }

    $this->users->update($userId, [
        'password' => password_hash($plain, PASSWORD_ARGON2ID),
    ]);

    return true;
}

Die Aktualisierung des Backend-Frameworks auf eine Version mit dem Fix für die Fehleroffenlegung sowie das harte Erzwingen von display_errors auf Aus in der Produktionskonfiguration, unabhängig von der Version.

Das Herausziehen des Salts und der Verbindungsparameter aus dem Repository in Umgebungsvariablen oder einen Secrets-Manager, mit Rotation des Werts, der bereits in der Git-Historie gelandet ist. Die Datei zu ändern genügt nicht, denn der alte Wert bleibt in den Commits.

Die Aktualisierung der für den Browser erreichbaren Frontend-Bibliotheken, also der Formularvalidierung und der UI-Bibliothek, sowie das Entfernen der Lokalisierungsdateien aus dem Verzeichnis assets, die die Anwendung nicht lädt. Der Rest der SCA-Treffer ging in die Gruppe Build-Prozess, fällig bei der nächsten größeren Aktualisierung und unter einer Bedingung: Baut derselbe Runner nicht vertrauenswürdigen Code, rücken sie wieder an die Spitze der Liste.

Ehrlich: Eine statische Analyse sagt nicht, ob das Administrationspanel eine korrekte Zugriffskontrolle auf Funktionsebene hat, ob Sitzungstokens bei einer Passwortänderung ungültig werden und ob sich das Zahlungsmodul auf Ebene der Bestelllogik austricksen lässt. Das sind Fragen für einen dynamischen Test, und so hielten wir sie in den Empfehlungen für die nächste Stufe fest.

Wenn Sie im Code eine Funktion haben, die Passwort-Hashes berechnet, und niemand sie seit Jahren angefasst hat, prüfen Sie sie jetzt, bevor Sie den Scanner-Bericht nach CVSS sortieren. Die gefährlichste Schwachstelle trägt womöglich gar keine CVE-Nummer.

Wie viele Treffer aus Ihrem letzten SCA-Bericht hat wirklich jemand gelesen?

Vereinbaren Sie ein Gespräch mit einem Berater. Gemeinsam stecken wir den Umfang einer Code- und Abhängigkeitsanalyse (SAST/SCA) ab, die realen Risiken und wie eine solche Prüfung bei Ihnen aussehen würde.

Vorheriger
Wir haben ein Paket hochgeladen. Wir bekamen Root.

Alle Fallstudien sind nach Sektor anonymisiert, ohne Namen, Daten oder Angaben, die den Kunden identifizieren könnten, gemäß der Vertraulichkeit. Wir veröffentlichen niemals echte Schwachstellen oder technische Kundendaten.