Guilgo Blog

Notas de mi quehacer diario con las técnologias.

El 9 de junio de 2026 se descubrió uno de los ataques de supply chain más significativos del ecosistema Arch Linux: más de 1600 paquetes del AUR (Arch User Repository) habían sido comprometidos con un infostealer y un rootkit eBPF. Este artículo explica cómo integré la detección automática en mi homelab usando Wazuh, de forma que si algún paquete infectado llega a mi portátil, recibo una alerta en Telegram en menos de una hora.

No sustituye revisar manualmente qué instalaste durante la ventana del ataque. Lo que aporta es vigilancia continua: la lista comunitaria sigue creciendo y tú puedes instalar un paquete sin saber que estuvo comprometido hace semanas.

Si ya tienes Wazuh en el lab, encaja con lo documentado en el blog sobre SOC proactivo, Wazuh 5 en producción y alertas por Telegram.

El ataque: atomic-lockfile y js-digest

La campaña, conocida como atomic-lockfile, funcionó así:

  1. Atacantes tomaron control de cuentas de mantenedores de paquetes AUR huérfanos (algunos mediante suplantación de identidad, con git commit --author forjado).
  2. Inyectaron npm install atomic-lockfile o bun install js-digest en los ficheros .install o .hook de los PKGBUILDs.
  3. Esos paquetes npm/bun contenían un binario ELF (Rust) que, al ejecutarse durante la instalación, robaba credenciales (tokens GitHub, Discord, SSH keys, cookies del navegador, secretos de CI/CD) y las exfiltraba vía temp.sh a un C2 por Tor.
  4. Si el usuario instalaba con privilegios root, el malware instalaba además un rootkit eBPF que ocultaba procesos, ficheros e inodos de socket.

Dos oleadas distintas:

  • Wave 1 (9–12 jun): atomic-lockfile / lockfile-js vía npm — cuentas krisztinavarga, franziskaweber, tobiaswesterburg, ellenmyklebust.
  • Wave 2 (12 jun): js-digest vía bun — cuentas custodiatovar, veramagalhaes.

El repositorio lenucksi/aur-malware-check consolidó las listas de detección comunitarias: ~1619 paquetes comprometidos y scripts de comprobación.

¿Estoy afectado?

Solo existe riesgo si se cumplen las tres condiciones:

  • Utilizas paquetes AUR (no los repos oficiales de Arch).
  • Instalaste alguno durante la ventana del ataque (9–12 junio de 2026, u oleadas posteriores documentadas).
  • Ese paquete concreto estaba comprometido en el momento de la instalación.

Los repositorios oficiales de Arch Linux no estaban afectados. Tampoco es un exploit remoto contra pacman: hace falta haber ejecutado la instalación de un PKGBUILD envenenado. Si no usas AUR, el riesgo directo es nulo; si lo usas, la pregunta operativa es si alguno de tus paquetes estuvo en la lista — y si sigues instalando AUR sin revisar el historial del mantenedor.

La arquitectura de detección

La idea es simple: cada hora, el agente Wazuh del portátil ejecuta un script que compara los paquetes AUR instalados contra la lista de comprometidos. Si hay coincidencia, el manager genera una alerta nivel 12 y la mando por Telegram.

Portátil (agente Wazuh)
  └── logcollector full_command cada 1h
        └── aur-malware-check-agent.sh
              ├── pacman -Qmq  → paquetes AUR instalados
              ├── curl → package_list.txt (caché 24h)
              └── comm -12 → intersección
                    ├── Limpio  → "AUR_MALWARE_CHECK: clean"     (nivel 0, silencio)
                    └── Infectado → "AUR_MALWARE_DETECTED: infected_packages=pkg1 pkg2"
                                          ↓
Manager Wazuh (homelab)
  ├── Decoder: extrae la lista de paquetes como extra_data
  ├── Regla 125001 nivel 12 → alerta
  └── Integración Telegram → notificación inmediata

El script del agente

El script corre como root (lo ejecuta wazuh-logcollector) y usa comm para calcular la intersección entre los paquetes AUR instalados y la lista de comprometidos, que se cachea localmente para no hacer una petición HTTP en cada ejecución:

#!/bin/bash
# /usr/local/bin/aur-malware-check-agent.sh

PACKAGE_LIST_URL="https://raw.githubusercontent.com/lenucksi/aur-malware-check/master/package_list.txt"
LIST_CACHE="/var/ossec/tmp/aur-malware-list.txt"
LIST_MAX_AGE=86400  # 24h

mkdir -p "$(dirname "$LIST_CACHE")"

# Refrescar caché si tiene más de 24h
if [ ! -f "$LIST_CACHE" ]; then
    NEEDS_REFRESH=true
else
    AGE=$(( $(date +%s) - $(stat -c %Y "$LIST_CACHE" 2>/dev/null || echo 0) ))
    [ "$AGE" -gt "$LIST_MAX_AGE" ] && NEEDS_REFRESH=true || NEEDS_REFRESH=false
fi

if [ "$NEEDS_REFRESH" = "true" ]; then
    curl -s --max-time 20 --fail "$PACKAGE_LIST_URL" -o "${LIST_CACHE}.tmp" 2>/dev/null \
        && mv "${LIST_CACHE}.tmp" "$LIST_CACHE" \
        || { [ ! -f "$LIST_CACHE" ] && echo "AUR_MALWARE_CHECK: ERROR cannot fetch package list" && exit 1; }
fi

AUR_PKGS=$(pacman -Qmq 2>/dev/null)
[ -z "$AUR_PKGS" ] && echo "AUR_MALWARE_CHECK: clean" && exit 0

INFECTED=$(comm -12 <(echo "$AUR_PKGS" | sort) <(sort "$LIST_CACHE"))

if [ -n "$INFECTED" ]; then
    echo "AUR_MALWARE_DETECTED: infected_packages=$(echo "$INFECTED" | tr '\n' ' ' | sed 's/ $//')"
    exit 2
else
    echo "AUR_MALWARE_CHECK: clean"
fi

La salida es siempre una sola línea con un prefijo fijo, lo que facilita el parsing en Wazuh.

El decoder en Wazuh

Aquí está el truco técnico más importante: el regex de Wazuh no funciona como el de POSIX. En el motor de reglas de Wazuh, . es un punto literal y \. es “cualquier carácter” (el equivalente al . habitual). Usar (.+) en un decoder de Wazuh no captura texto libre — captura uno o más puntos literales. El correcto es (\.+).

Además, cuando se usa full_command con un alias en el agente, Wazuh extrae ese alias como program_name en el predecodificador syslog. Por tanto el decoder padre debe usar <program_name> en lugar de <prematch>:

<!-- El alias del localfile ("aur-malware-check") llega como program_name -->
<decoder name="aur-malware-check">
    <program_name>aur-malware-check</program_name>
</decoder>

<!-- \. = cualquier carácter en regex de Wazuh (no punto literal) -->
<decoder name="aur-malware-detected">
    <parent>aur-malware-check</parent>
    <regex>AUR_MALWARE_DETECTED: infected_packages=(\.+)</regex>
    <order>extra_data</order>
</decoder>

<decoder name="aur-malware-clean">
    <parent>aur-malware-check</parent>
    <prematch>AUR_MALWARE_CHECK: clean</prematch>
</decoder>

<decoder name="aur-malware-error">
    <parent>aur-malware-check</parent>
    <regex>AUR_MALWARE_CHECK: ERROR (\.+)</regex>
    <order>extra_data</order>
</decoder>

Las reglas

Cuatro reglas cubren los casos posibles. Las importantes para la alerta son 125001 (detectado) y 125002 (primer evento por agente, FTS):

<group name="aur_malware,supply_chain,arch_linux,">

  <!-- decoded_as referencia el nombre del decoder padre (Wazuh así lo reporta) -->
  <!-- match diferencia el caso infectado del limpio dentro del mismo decoder    -->
  <rule id="125001" level="12">
    <decoded_as>aur-malware-check</decoded_as>
    <match>AUR_MALWARE_DETECTED:</match>
    <description>AUR SUPPLY CHAIN: paquetes comprometidos instalados - $(extra_data)</description>
    <group>aur_malware,supply_chain,gdpr_IV_35.7.d,pci_dss_11.5,</group>
    <mitre>
      <id>T1195.001</id>  <!-- Supply Chain Compromise -->
      <id>T1546</id>      <!-- Persistence via systemd  -->
      <id>T1056</id>      <!-- Credential Theft         -->
    </mitre>
  </rule>

  <rule id="125002" level="14">
    <if_sid>125001</if_sid>
    <if_fts />
    <description>AUR SUPPLY CHAIN (PRIMER EVENTO): nuevo host infectado - $(extra_data)</description>
    <group>aur_malware,supply_chain,</group>
  </rule>

  <rule id="125010" level="5">
    <decoded_as>aur-malware-check</decoded_as>
    <match>AUR_MALWARE_CHECK: ERROR</match>
    <description>AUR malware check: error al ejecutar la comprobación - $(extra_data)</description>
    <group>aur_malware,</group>
  </rule>

  <rule id="125020" level="0">
    <decoded_as>aur-malware-check</decoded_as>
    <match>AUR_MALWARE_CHECK: clean</match>
    <description>AUR malware check: sistema limpio</description>
    <group>aur_malware,</group>
  </rule>

</group>

En el manager, añadí el grupo aur_malware a la integración de Telegram que ya tenía configurada para otros casos del homelab (mismo patrón que en control parental con Wazuh y Telegram): nivel mínimo 7, formato JSON, script custom que reenvía al bot.

Configuración del agente (ossec.conf en el portátil)

La clave es el bloque localfile con full_command y el alias que coincide con el program_name del decoder:

<localfile>
  <log_format>full_command</log_format>
  <command>/usr/local/bin/aur-malware-check-agent.sh</command>
  <alias>aur-malware-check</alias>
  <frequency>3600</frequency>
</localfile>

El alias es lo que Wazuh usa como program_name en el log syslog generado internamente. Sin él, el decoder no encontraría el log.

Verificación con wazuh-logtest

Para validar el pipeline completo antes de desplegar, Wazuh incluye wazuh-logtest. El truco es enviar el log en formato syslog completo (con timestamp y hostname) seguido de una línea vacía:

printf 'Jun 14 12:00:00 portatil aur-malware-check: AUR_MALWARE_DETECTED: infected_packages=foo-pkg bar-pkg\n\n' \
  | docker exec -i wazuh-manager /var/ossec/bin/wazuh-logtest

Resultado esperado:

**Phase 1: Completed pre-decoding.
    program_name: 'aur-malware-check'

**Phase 2: Completed decoding.
    name: 'aur-malware-check'
    extra_data: 'foo-pkg bar-pkg'

**Phase 3: Completed filtering (rules).
    id: '125001'
    level: '12'
    description: 'AUR SUPPLY CHAIN: paquetes comprometidos instalados - foo-pkg bar-pkg'
    mitre.id: ['T1195.001', 'T1546', 'T1056']
**Alert to be generated.

Cosas que aprendí por el camino

1. El regex de Wazuh es diferente al de POSIX

(.+) no funciona para capturar texto libre. En Wazuh, . es literal y \. es comodín. Pasé varios ciclos de wazuh-logtest hasta identificarlo.

2. decoded_as usa el nombre del decoder padre

Cuando un decoder hijo extrae los campos, Wazuh reporta en Phase 2 el nombre del decoder padre como name. Por tanto <decoded_as> en la regla debe referenciar el padre, no el hijo. Para distinguir casos (infectado vs. limpio), se añade <match> con el texto específico.

3. full_command necesita alias para que el decoder funcione

Sin alias, el program_name en el log syslog interno sería el path completo del script. Con alias, es el nombre limpio que coincide con el decoder.

4. log_format: systemd no está disponible en todas las versiones del agente

En CachyOS con el paquete AUR de wazuh-agent, el logcollector rechaza systemd como log_format. Hay que usar syslog con path explícito o simplemente omitirlo.

5. agent-auth usa -A para el nombre, no -n

En la versión del agente que uso, -n es para la interfaz de red. El flag correcto para el nombre del agente es -A.

El resultado

Mi portátil con Arch Linux / CachyOS aparece en el manager como agente activo. El check se ejecuta automáticamente cada hora. Si mañana instalo un paquete del AUR que estuviera comprometido, recibiré un Telegram con su nombre antes de que pase la siguiente hora.

En el momento de escribir este post: 0 paquetes infectados. Ninguno de mis paquetes AUR instalados aparece en la lista de comprometidos.

Este tipo de ataques demuestra que la seguridad ya no depende únicamente del sistema operativo o del antivirus. La cadena de suministro del software se ha convertido en uno de los vectores más efectivos para comprometer equipos. Con menos de 50 líneas de Bash y unas pocas reglas de Wazuh es posible añadir una capa de vigilancia continua que detecte este tipo de incidentes antes de que pasen desapercibidos.

Recursos