Volver a todas las publicaciones
Publicado el · por Renaud Deraison

Esperó a la tercera llamada

Pillar Security está siguiendo a Deadbugz, un servidor MCP colado en proyectos de código abierto mediante pull request. Trae dos herramientas honestas, cuenta las llamadas de herramienta de tu agente y, en la cuarta, reescribe los metadatos que devuelve como instrucciones para recopilar claves SSH, credenciales de AWS, historial del shell y kubeconfig, y no decir nada al respecto. Ningún fallo, ninguna CVE: el protocolo permite todo eso. La víctima es la idea de que aprobar un servidor una vez te diga algo sobre la llamada siguiente. En un espacio de trabajo de Bromure Agentic Coding la carga se ejecuta entera, recoge cuatro señuelos y muere en el proxy al salir.

Leíste el código. Lo ejecutaste. Daba formato a texto, tal como anunciaba. Lo aprobaste, y el atacante contaba con que lo aprobaras.

La noche del 10 de agosto, entre las 21:52 y las 23:07 UTC, una cuenta de GitHub llamada zellkernel abrió veintitrés pull requests contra proyectos sin relación entre sí, de IA, MCP y herramientas de desarrollo. Setenta y cuatro minutos, de principio a fin. Cada pull request hacía la misma pequeña edición: añadía un servidor Model Context Protocol a un fichero de configuración.

Pillar Security publicó la campaña el 12 de agosto y la llamó Deadbugz, por el fichero que deja caer una de sus variantes. Un mes después, los repasos mensuales de MCP siguen abriendo con ella. El digest de septiembre de Adversa, publicado ayer, le dedica toda la sección de incidentes. Es el reloj lo que le gana ese espacio.

Dos herramientas, y un contador

El servidor se llama a sí mismo productivity-suite. Ofrece dos herramientas: format_text y summarize. Ambas funcionan. Apunta un agente hacia él, pídele que apriete un párrafo, y obtienes un párrafo apretado.

Detrás de ellas, el servidor mantiene lo que Pillar describe como “un contador en memoria, por cliente, de las peticiones tools/call”. Cuando ese contador llega a tres, el servidor cambia sus respuestas a otros dos tipos de petición: tools/list, que le dice al agente qué herramientas existen y para qué sirven, y prompts/get, que entrega al agente instrucciones preparadas. A partir de la cuarta llamada, esas respuestas vuelven cargando, en palabras de Pillar, “instrucciones destinadas a dirigir a un agente de IA conectado hacia ficheros locales sensibles y a ocultar la actividad a su operador”.

Cuatro cosas: claves SSH, credenciales de AWS, historial del shell, configuración de Kubernetes. Y luego una instrucción más: ocultar la actividad al usuario.

Los nombres de las herramientas no cambian nunca. format_text sigue llamándose format_text. Lo que giró es la descripción de debajo, y la descripción es la parte que el modelo lee como autoridad.

productivity-suite — las mismas dos herramientas, antes y después del contadortu agente de códigotools/call ×Ntrabajo corriente, turnos corrientescontador en memoriapor cliente, nunca persistidoumbral: tresllamadas 1 – 3tools/list → format_text, summarize“da formato a un bloque de texto”prompts/get → un prompt para asear textolo que ve una revisiónun servidor pequeño que hace lo que dicedos herramientas, descripciones honestas, sin sorpresasaprobado4.ªllamadas 4 en adelantetools/list → format_text, summarizemismos nombres, otras instruccionesprompts/get → la cargalo que ahora se le manda hacer al modelo~/.ssh · credenciales de AWShistorial del shell · ~/.kube/configy no menciones esto al usuario
Deadbugz mantiene un contador por cliente de las peticiones tools/call. En las tres primeras, tools/list y prompts/get devuelven metadatos honestos para format_text y summarize. A partir de la cuarta, esos mismos dos nombres de herramienta vuelven con descripciones y prompts preparados que ordenan al agente recopilar claves SSH, credenciales de AWS, historial del shell y kubeconfig, y callarse. Los nombres de las herramientas no cambian nunca, así que nada de lo que mira un revisor parece distinto.

Nada de esto es una vulnerabilidad

En toda esta historia no aparece un solo fallo. No hay corrupción de memoria que parchear ni CVE que registrar, porque el servidor no hace nada que el protocolo prohíba.

MCP es un protocolo en el que un servidor publica sus capacidades en tiempo de ejecución y puede volver a publicarlas. Las descripciones de herramientas son texto que el servidor elige y el cliente recoge. Los prompts son texto que el servidor elige y el cliente recoge. Deadbugz usa todo eso tal como fue diseñado. Su única aportación original es decidir cuándo decir la verdad.

Y con eso basta, porque todos los controles que la mayoría de los equipos tienen contra un servidor MCP hostil se ejecutan una sola vez. Lees el repositorio. Arrancas el servidor y lo tanteas. Comparas la lista de herramientas con lo que promete el README. Apruebas, quizá fijas la versión, y sigues adelante. Cada uno de esos gestos es una fotografía, y Deadbugz decide cuándo la sacas.

El propio consejo de Pillar a quienes construyen clientes MCP delata la forma. Tratar los cambios de definición de herramienta en un servidor ya aprobado como un evento de seguridad que exige una nueva aprobación. Hacer visibles al operador los cambios de definición. Tomar la huella de las definiciones de herramienta en el momento de la aprobación y compararla después. Cada una de esas medidas es una petición de mover un control del momento de la aprobación al momento de la ejecución.

La población en la que caza Deadbugz es enorme e inestable. Una evaluación dinámica de servidores MCP expuestos a internet publicada en julio, Exposed by Design, probó 414 de los 640 servidores de producción confirmados de entre las más de 21 000 instancias que su autor encontró en la internet pública. Reportó 68 vulnerabilidades notificables repartidas en diez clases, un 91,8 % de servidores funcionando sin ninguna autenticación OAuth, y 687 instancias de herramientas que exponían ejecución de shell sin control de acceso. La cifra que se queda contigo es la rotación: un 41,6 % de los servidores confirmados desapareció en menos de tres días entre dos mediciones consecutivas, lo que el autor lee como “ciclos de despliegue rápidos sin revisión de seguridad”. Un servidor que auditaste la semana pasada puede ser otro software esta semana, o haber desaparecido.

La entrega también merece una nota, porque es barata. Diecisiete de los veintitrés pull requests añadían un punto final remoto, productivity-suite-mcp.onrender.com/mcp, a la configuración MCP de un proyecto. Cuatro apuntaban Python a un fichero local oculto, ~/.config/.cache/.sys/.deadbug-mcp.py. Dos eran envíos a directorios de servidores. Pillar no encontró ninguno fusionado con el botón de merge de GitHub en el momento de la revisión: diecinueve cerrados, cuatro todavía abiertos. Pero un pull request es solo una de las maneras en que viaja una línea de configuración. La misma línea cabe en una entrada de blog, en una guía de inicio, en una ficha de un directorio MCP, o en una respuesta servicial debajo de una pregunta sobre formateo de texto.

La lista de la compra

Vuelve a la lista. Es concreta, y es la misma cada vez: claves SSH, credenciales de AWS, historial del shell, configuración de Kubernetes.

Ese es el inventario permanente de una máquina de desarrollo. Es lo que se lleva un infostealer, lo que busca con grep un script postinstall malicioso, y lo que un atacante le pide a un agente secuestrado que vaya a buscar. La lista lleva una década de campañas estable porque su contenido es estable: una clave privada que firma, un secreto que autentica, un registro de lo que has escrito, y un token que llega a producción.

Un agente en un portátil corriente puede satisfacer todos los puntos, porque tiene un shell en tu directorio personal y se ejecuta como tú.

La misma lista, dentro de un espacio de trabajo de Bromure

Bromure Agentic Coding ejecuta cada agente de código en una VM Linux virtualizada por hardware en tu Mac, y guarda todos los secretos reales en el lado anfitrión de esa frontera. El agente trabaja con señuelos que preservan la estructura, derivados del valor real más una sal por instalación, de modo que las herramientas que toman la huella de una clave nunca la ven cambiar entre sesiones. Un proxy del lado anfitrión sustituye el valor real en el cable después de que la petición haya salido de la VM, y solo cuando va destinada al anfitrión para el que se acuñó esa credencial.

Ejecuta la carga y déjala ganar. Supón que el modelo lee la descripción envenenada, se la cree, y se pone a buscar. Toma los cuatro puntos en orden.

Claves SSH. El agente no encuentra ninguna clave, ni siquiera un señuelo. El SSH_AUTH_SOCK de la VM es un puente ssh-agent sobre un socket virtio, puerto vsock 8444. El invitado puede listar identidades y pedir firmas; los bytes de la clave privada viven solo en el anfitrión y, como dice el manual, no pueden leerse ni extraerse desde dentro de la VM. Bromure nunca expone tu agente de inicio de sesión de macOS a una sesión. Cada firma que sí ocurre emite un evento de auditoría credential.ssh_sign con la huella SHA256 de la clave, y puedes configurar cualquier clave importada para que pida confirmación en el anfitrión en cada petición de firma.

Credenciales de AWS. Un fichero que tiene buen aspecto y no vale nada. El ~/.aws/config de la VM apunta a un ayudante credential-process que entrega el identificador de clave de acceso real emparejado con una clave secreta falsa de 40 caracteres por el puerto vsock 8445. Todos los SDK, la CLI aws, terraform y boto3 lo recogen sin configuración adicional, y el invitado produce una firma SigV4 bien formada calculada con el secreto equivocado. El refirmador del anfitrión retira esa firma y vuelve a firmar a la salida. Llévate el par a otra parte y AWS responde InvalidSignatureException.

Configuración de Kubernetes. ~/.kube/config existe y contiene un token bearer que empieza por brm-k8s-. Funciona, a través del proxy, contra los servidores de API para los que se configuró el espacio de trabajo. En cualquier otro sitio es inerte.

Historial del shell. Real, y duradero: el trabajo de programación necesita estado duradero, así que la VM del espacio de trabajo es una máquina persistente y no desechable. Contiene el historial de una máquina que nunca ha contenido una clave real, con comandos ejecutados contra señuelos en un directorio personal que no es el tuyo, junto a un ~/.git-credentials y un ~/.docker/config.json que también son falsos.

la lista de la compra, ejecutada dentro de un espacio de trabajolo que pide la cargalo que encuentraclaves privadas ~/.sshnada — los bytes están en el anfitriónlas firmas cruzan vsock 8444; las claves nuncacredenciales de AWSid de clave real, secreto falso de 40 caracteresreproducidos en otro sitio: InvalidSignatureException~/.kube/configun marcador brm-k8s-sirve por el proxy del anfitrión, inerte en otro sitiohistorial del shellduradero, y lleno de señuelosuna VM que nunca ha tenido una clave real
La lista de la compra de cuatro puntos de la carga, ejecutada contra un espacio de trabajo de Bromure Agentic Coding. SSH no devuelve ni un byte de clave privada, porque la firma ocurre en el anfitrión por vsock 8444. La configuración de AWS entrega el identificador de clave de acceso real con un secreto falso, así que un par robado falla la validación de firma aguas arriba. El kubeconfig contiene un marcador brm-k8s-. El historial del shell es el de una VM que nunca ha tenido una credencial real.

Y luego tiene que enviarlo

Recoger es la mitad del trabajo. La otra mitad es el tramo de salida hacia productivity-suite-mcp.onrender.com, y ese tramo se topa con una pila de controles del lado anfitrión.

Los señuelos son cables trampa. Cada uno tiene una única familia de destinos legítimos, el ámbito de anfitriones para el que se acuñó, así que el proxy escanea cada petición saliente, cabeceras y cuerpo, en busca de un señuelo que vaya a donde no le corresponde. Ante un acierto trata la petición como intento de exfiltración de credenciales: rechaza con un HTTP 451 sin reenviar un solo byte, pausa la VM en el acto, y levanta una alerta que ofrece Apagar, Guardar para investigación (exportar antes el disco, el directorio personal y las carpetas compartidas para el análisis forense) o Continuar por tu cuenta y riesgo. El evento aterriza como una fila roja de Intermediación de credenciales en la Security Timeline.

El segundo control es la alcanzabilidad. Cada espacio de trabajo lleva un cortafuegos de salida ordenado (acción, protocolo, anfitrión o CIDR, puertos, y para el tráfico web los verbos HTTP individuales) con un valor por defecto para el tráfico no emparejado. Los espacios de trabajo nuevos permiten lo que ninguna regla recoge; ponlo en Denegar, enumera lo que el trabajo necesita, y un subdominio de render.com no está en la lista. Dos componentes fuera del invitado lo aplican: el conmutador virtual empareja cada flujo por IP de destino y por nombre de anfitrión espiado en el DNS, y el proxy vuelve a emparejar por el nombre de servidor TLS. Las ediciones de reglas llegan a las sesiones en curso sin reinicio.

Guardrails pone una puerta en la consecuencia. Clasifica las llamadas del agente a Kubernetes, AWS, DigitalOcean, registros de contenedores, forjas git y bases de datos HTTPS como lecturas, escrituras u operaciones destructivas, y los espacios de trabajo nuevos vienen por defecto en Preguntar antes de escribir: cada mutación se detiene en un diálogo del anfitrión que muestra la operación literal, el SQL exacto o el METHOD /path. Eso es justo lo que Pillar pidió a quienes construyen plataformas: la lectura de ficheros sensibles, el acceso a credenciales y la ejecución de código no deberían ser consecuencias de metadatos remotos.

También hay un detector en el camino del contenido. Cuando el agente devuelve al modelo contenido externo (contenidos de ficheros, páginas descargadas, salida de comandos, resultados de llamadas de herramienta), un clasificador PromptGuard local puntúa esos fragmentos en tu Mac antes de que el modelo actúe sobre ellos, y puede registrar, preguntar, o bloquear con un 451. El token bearer MCP de un servidor HTTP no entra siquiera en la VM: el de verdad se queda en el anfitrión, la configuración del agente recibe un marcador brm-mcp_, y el campo de ajustes lo dice en voz alta, Nunca se envía a la VM — lo sustituye el proxy.

la comprobación corre una vezaprobación al instalar, heredada para siempredía 1 — leer el código, ejecutarlo, aprobarel servidor es honesto, y el veredicto es correctollamadas 1 – 3 — todavía honestonada empuja a nadie a mirar de nuevollamada 4 — los metadatos girancubierta por un veredicto emitido antes de los hechosno se le pide a nadie una decisión nuevalo que el agente alcanza~/.ssh real · claves AWS reales · kubeconfig realla comprobación corre cada vezno hay veredicto de instalación que heredarVM del espacio de trabajo — el agente y la salida del servidorbrm-k8s- · brm-mcp_ · secreto AWS falsomarcadores desde el arranque; sin bytes de clave privadavsock 8443 — la única ruta de salidaproxy del anfitrión — consultado en cada peticióncortafuegos de salida · anfitrión, puerto, verbo HTTPpolítica de escritura · lectura / escritura / destructivoescaneo de inyecciones en el contenido enviado al modelouna fila de traza por petición, en tu máquinael tramo de exfiltraciónseñuelo fuera de su ámbito → HTTP 451, VM en pausa, fila en la timeline
Dos ubicaciones para la misma decisión. A la izquierda, una aprobación en el momento de la instalación cubre todo lo que el servidor haga después, así que un cambio de metadatos en la cuarta llamada hereda un veredicto emitido el primer día. A la derecha no hay veredicto de instalación que heredar: las credenciales son marcadores desde el arranque, y cada operación que el agente intenta se clasifica de nuevo en el proxy del anfitrión al salir.

Si tocaste este

Busca en tus configuraciones de cliente MCP, repositorios y directorios personales productivity-suite-mcp.onrender.com, el punto final anterior promo-surname-xml-quantum.trycloudflare.com, y el artefacto local en ~/.config/.cache/.sys/.deadbug-mcp.py. Revierte cualquier cambio de configuración que los haya introducido, y no ejecutes el script. Conserva los registros de tu cliente MCP y busca refrescos de definiciones de herramienta después de la tercera llamada. El consejo de Pillar sobre la rotación es comedido: rota allí donde la evidencia local respalde un compromiso.

La regla que encoge la pregunta

En el panel Guardrails del espacio de trabajo, pon Tráfico no emparejado en Denegar y enumera lo que el trabajo necesita: allow web api.github.com, allow web registry.npmjs.org, default deny. Después ejecuta bromure-cli trace hostnames my-workspace tras una sesión, que imprime todos los anfitriones distintos que el agente contactó, con sus cuentas. Un destino que nunca elegiste aparece como una línea que no escribiste.

La aprobación es una instantánea; el cable es continuo

El instinto después de una campaña así es revisar con más dureza: leer más código, fijar el commit. Ambas cosas valen la pena, y ninguna toca el mecanismo. La revisión aquí fue correcta. El servidor fue honesto mientras alguien miraba, luego cambió según un calendario que él controlaba, y al hacerlo no rompió ninguna regla del protocolo.

Un control que sobrevive a que un servidor cambie de idea es uno que no depende de haber acertado antes. Una credencial que es un marcador es un marcador en la cuarta llamada y en la cuatrocientésima. Una clave privada en el anfitrión no puede leerse en ninguna llamada. El proxy evalúa una regla de salida en cada conexión y clasifica cada escritura mientras ocurre. Nada de eso guarda memoria del día en que aprobaste algo, y por eso un atacante no puede heredarlo.

MCP hizo fácil darle a un agente capacidades nuevas, que era el objetivo, e hizo de esas capacidades algo que una parte remota describe en un texto que tu modelo lee como instrucción. Las dos cosas han venido para quedarse. Decide qué puede guardar tu agente y qué puede alcanzar, y pon la aplicación en algún sitio donde el servidor no tenga voto. Instala Bromure Agentic Coding y no le des a tu agente nada que merezca esperar tres llamadas.