Caso forense · Seguridad WordPress

Limpiar el malware no es cerrar la brecha: anatomía forense de un WordPress comprometido

El barrido del antimalware terminó. La pantalla mostró cero amenazas activas. El equipo respiró. Tres semanas después, un análisis rutinario de la carpeta de subidas reveló cinco archivos de respaldo —volcados de base de datos incluidos— descargables desde el servidor de producción sin ninguna autenticación.

Respuesta breve. Limpiar el malware y cerrar la brecha son dos trabajos distintos. El primero puede terminar mientras el segundo permanece completamente abierto. Este artículo documenta ambos, incluyendo los cinco errores de método del equipo que los mantuvo separados durante semanas.

Captura de pantalla del navegador mostrando el dominio del cliente con contenido del atacante AYAMJP: portal de apuestas en línea con menú de juegos de azar, logo del atacante y botones de registro. La URL en la barra del navegador pertenece al cliente.
Captura 1. El sitio del cliente al momento del compromiso: el atacante había reemplazado el contenido visible del dominio con su propio portal de apuestas. El dominio corporativo de la empresa servía contenido completamente ajeno a su giro. Fuente: documentación interna del caso, Transgenia, julio 2026.

¿Qué hace un sitio comprometido mientras el equipo lo limpia?

La base de datos miente; los mtime no

Cualquier análisis forense de un sitio WordPress comprometido empieza con una pregunta: ¿cuándo ocurrió esto? La respuesta que ofrece la base de datos es, en este caso, inutilizable como cronología.

Ocho entradas publicadas declaraban fechas anteriores a otras con un identificador de base de datos mayor: fueron creadas más tarde pero fechadas como si fueran más antiguas. El mismo patrón aparece en otra tabla del sitio, con cuatro comentarios de identificadores consecutivos distribuidos en dos años distintos. No fue un error de zona horaria: el campo fecha fue modificado manualmente.

Diagrama con dos filas paralelas. Fila superior: entradas A, B, C con IDs secuenciales N, N+1, N+2. Fila inferior: fechas declaradas 2025, 2019, 2026. La entrada B tiene el ID mayor pero declara la fecha más antigua, lo que prueba modificación manual del campo.
Ver fuente Mermaid
%%{init: {'theme': 'dark', 'themeVariables': {'primaryColor': '#1e293b', 'primaryTextColor': '#f8fafc', 'lineColor': '#94a3b8'}}}%%
flowchart LR
    subgraph real["Orden real de creacion (ID de base de datos, secuencial)"]
        direction LR
        id_a["Entrada A\nID N"] --> id_b["Entrada B\nID N+1"] --> id_c["Entrada C\nID N+2"]
    end
    subgraph decl["Fecha declarada por quien publico el contenido"]
        direction LR
        f_a["2025-XX-XX"] --> f_b["2019-XX-XX\n(ID mayor,\nfecha mas antigua)"] --> f_c["2026-XX-XX"]
    end

id_a -.-> f_a id_b -.-> f_b id_c -.-> f_c

style f_b fill:#7f1d1d,stroke:#ef4444,color:#fca5a5 style id_b fill:#1e3a5f,stroke:#3b82f6,color:#bfdbfe

Figura 4. El orden real de creación (ID de base de datos) contradice las fechas declaradas. La Entrada B tiene un ID mayor —fue creada después—, pero declara una fecha anterior a la Entrada A. Fuente: expediente forense interno, Transgenia, 2026.

La cronología válida para este sitio es la de los tiempos de modificación del sistema de archivos (mtime). Son coherentes entre sí y con el único registro de ejecución de código documentado. No se probó que el atacante pudiera alterarlos; afirmar que son incorruptibles excedería la evidencia. Lo que sí puede afirmarse: son la única fuente consistente disponible.

La ventana de actividad reconstruida por mtime va del 25 de junio al 26 de agosto de 2026.

La cadena de ejecución del 3 de julio

El único momento en que puede demostrarse ejecución de código ocurrió el 3 de julio de 2026. Un directorio creado menos de diez minutos antes contenía cuatro elementos: la copia íntegra de un plugin legítimo, un archivo oculto, un cargador PHP y un archivo de configuración de 16 bytes. La elección de un directorio de plugins como contenedor no fue casual: cualquier archivo PHP en esa ubicación se carga automáticamente con cada petición de WordPress.

La cadena: el cargador incluyó el archivo oculto, que invocó una función interna que intentó abrir un proceso del sistema operativo con popen(). El hosting tenía esa función deshabilitada. El intento falló con el error registrado literalmente:

[03-Jul-2026 HH:MM:SS UTC] PHP Fatal error:  Call to undefined function popen() in
/home/USUARIO/public_html/wp-content/plugins/NOMBRE-FALSO-DEL-ATACANTE/cargador.php on line 7

Stack trace: #0 /home/USUARIO/public_html/wp-content/plugins/NOMBRE-FALSO-DEL-ATACANTE/oculto.php(38): funcion_interna() #1 /home/USUARIO/public_html/wp-settings.php(422): include_once('...') #2 /home/USUARIO/public_html/wp-load.php(50): require_once('...') #3 {main}

Figura 1. Registro de error de PHP del 3 de julio de 2026. Las rutas absolutas y los nombres de archivo reales han sido sustituidos. La función que falla (popen), el tipo de error y la estructura de la pila son los del registro original. La marca de tiempo exacta está pendiente de autorización para publicar. Fuente: expediente forense interno, Transgenia, 2026.

Este registro es la única prueba de ejecución de código en todo el incidente. La lectura canónica del bloqueo es la lista de funciones deshabilitadas del hosting; no hay otra evidencia que contradiga esa interpretación.

Flowchart con cinco nodos en cadena: directorio de plugin falso creado menos de diez minutos antes, cargador PHP, archivo oculto, función interna intentando popen(), y nodo final marcado como BLOQUEADO por disable_functions del hosting.
Ver fuente Mermaid
%%{init: {'theme': 'dark', 'themeVariables': {'primaryColor': '#1e293b', 'primaryTextColor': '#f8fafc', 'primaryBorderColor': '#fb6c25', 'lineColor': '#94a3b8', 'edgeLabelBackground': '#1e293b'}}}%%
flowchart TD
    A["Directorio de plugin falso\ncreado 9 minutos antes\n(timestamp Unix en el nombre)"]
    B["cargador.php\nIncluye el segundo archivo"]
    C["Archivo oculto\nLlama a la funcion interna"]
    D["Funcion interna\nIntenta abrir proceso del sistema\npopen()"]
    E["BLOQUEADO\nCall to undefined function popen\ndisable_functions del hosting"]

A --> B --> C --> D --> E

style A fill:#0f172a,stroke:#fb6c25,color:#f8fafc style B fill:#0f172a,stroke:#fb6c25,color:#f8fafc style C fill:#0f172a,stroke:#fb6c25,color:#f8fafc style D fill:#0f172a,stroke:#fb6c25,color:#f8fafc style E fill:#7f1d1d,stroke:#ef4444,color:#fca5a5,font-weight:bold

Figura 2. Cadena de ejecución documentada el 3 de julio de 2026. El intento de abrir un proceso del sistema fue bloqueado por la lista de funciones deshabilitadas del hosting. Fuente: expediente forense interno, Transgenia, 2026.

El contenido en números

Al momento del análisis, el sitio tenía 604 entradas inventariadas: 115 publicadas, 483 en la papelera, 4 borradores y 2 autodrafts de WordPress. De las 115 publicadas, cuatro correspondían al contenido legítimo del cliente: el 3.5 por ciento del total. El resto: 102 entradas de apuestas deportivas en nueve idiomas, cinco de casino y cuatro de plantilla por defecto de WordPress.

Gráfico de barras horizontal. De las 115 entradas publicadas: 102 son de apuestas en 9 idiomas, 5 de casino, 4 por defecto de WordPress y 4 del cliente (3.5%).
Ver fuente Mermaid
%%{init: {'theme': 'dark'}}%%
xychart-beta horizontal
    title "115 entradas publicadas: 4 son del cliente (3.5%)"
    x-axis ["Del cliente", "Casino", "Por defecto", "Apuestas en 9 idiomas"]
    y-axis 0 --> 110
    bar [4, 5, 4, 102]
Figura 3. Las 115 entradas publicadas del sitio. El 96.5% corresponde a contenido inyectado por el atacante, concentrado en apuestas deportivas en nueve idiomas. Fuente: expediente forense interno, Transgenia, 2026.
Diagrama del sitio WordPress comprometido. La cuenta administradora (comprometida) publicó contenido spam y creó el plugin falso. El directorio mu-plugins carga ese plugin en cada petición y es invisible al panel. El directorio de uploads tiene archivos de respaldo descargables sin autenticación.
Ver fuente Mermaid
%%{init: {'theme': 'dark', 'themeVariables': {'primaryColor': '#1e293b', 'primaryTextColor': '#f8fafc', 'lineColor': '#94a3b8'}}}%%
flowchart TB
    subgraph EXT["Exterior (internet)"]
        direction LR
        visitante["Visitante\nlegitimo"]
        atacante["Atacante\n(origen desconocido)"]
    end

subgraph WP["WordPress (sitio comprometido)"] direction TB admin["Cuenta administradora\ncomprometida"] mu["mu-plugins/\nCarga automatica\nInvisible al panel"] plugins["plugins/\nPlugin falso del atacante"] uploads["wp-content/uploads/\nArchivos de respaldo\nDescargables sin autenticacion"] core["WordPress Core\nActualizado post-incidente"] content["604 entradas:\n4 del cliente (3.5%)\n600 spam inyectado"] end

visitante -->|"HTTP 200"| content atacante -->|"Credencial comprometida\n(hipotesis de trabajo)"| admin admin -->|"Publico contenido spam"| content admin -->|"Instalo plugin falso"| plugins plugins --> mu mu -->|"Se ejecuta en cada peticion"| core

style mu fill:#7f1d1d,stroke:#ef4444,color:#fca5a5,font-weight:bold style uploads fill:#7f1d1d,stroke:#ef4444,color:#fca5a5 style admin fill:#78350f,stroke:#f59e0b,color:#fde68a style content fill:#1e3a5f,stroke:#3b82f6,color:#bfdbfe style atacante fill:#450a0a,stroke:#ef4444,color:#fca5a5

Figura 7. Capas comprometidas del sitio y puntos de exposición activos. Los nodos en rojo permanecían abiertos al corte del análisis. Fuente: expediente forense interno, Transgenia, 2026.

El patrón de autoría confirma que la cuenta legítima de administración fue usada para publicar spam: 10 de 18 entradas con autor asignado salieron con esa identidad. De los 431 usuarios registrados, 428 eran suscriptores falsos sin ninguna entrada asociada. Las 38 categorías del sitio incluían 29 de apuestas deportivas. De los 108 comentarios, ninguno provenía de un visitante real: cuatro eran de plantilla por defecto de WordPress y ocho eran notas internas de pedidos de plataformas de comercio electrónico de años anteriores.


Limpiar es necesario. No es suficiente.

Diagrama de dos columnas. Izquierda en verde: acciones de limpieza completadas (plugin falso eliminado, segunda cuenta borrada, WordPress actualizado, artefactos eliminados, página spam cerrada). Derecha en rojo: brecha todavía abierta (archivos de respaldo descargables, hashes phpass expuestos, gestor de archivos activo, notificación LFPDPPP pendiente, barrido incompleto).
Ver fuente Mermaid
%%{init: {'theme': 'dark', 'themeVariables': {'primaryColor': '#1e293b', 'primaryTextColor': '#f8fafc', 'lineColor': '#94a3b8'}}}%%
flowchart LR
    subgraph LIMPIO["Limpiado al 2026-09-18 (76%)"]
        direction TB
        l1["Plugin falso eliminado\nde mu-plugins"]
        l2["Segunda cuenta\nadministradora eliminada"]
        l3["WordPress Core\nactualizado"]
        l4["15 artefactos de malware\neliminados del sistema de archivos"]
        l5["Pagina spam\ncerrada (404)"]
    end

subgraph BRECHA["Brecha abierta (24% pendiente)"] direction TB b1["Archivos de respaldo\nDescargables sin autenticacion"] b2["Hashes phpass\nExpuestos en volcado SQL"] b3["Gestor de archivos\ndel panel activo"] b4["Notificacion LFPDPPP\npendiente de confirmacion"] b5["Barrido del directorio\nde subidas: solo ~33% revisado"] end

LIMPIO -. "No implica" .-> BRECHA

style LIMPIO fill:#052e16,stroke:#22c55e,color:#86efac style BRECHA fill:#7f1d1d,stroke:#ef4444,color:#fca5a5 style b1 fill:#450a0a,stroke:#ef4444,color:#fca5a5,font-weight:bold style b2 fill:#450a0a,stroke:#ef4444,color:#fca5a5,font-weight:bold

Figura 8. Al 2026-09-18, el 76% de las acciones estaba cerrado y el 24% seguía abierto. La columna derecha es el trabajo que permanece activo. Fuente: expediente forense interno, Transgenia, 2026.

Al corte del 2026-09-18, 25 de las 33 acciones documentadas habían sido cerradas: el 76 por ciento. El directorio de plugins de carga obligatoria estaba limpio. La segunda cuenta administradora había sido eliminada. El núcleo de WordPress había sido actualizado. Quince artefactos de malware ya no existían en el sistema de archivos.

Lo que seguía abierto no era menor.

Captura de pantalla del navegador mostrando el sitio del cliente durante la fase de recuperación. El logo y el menú de navegación son visibles, pero la página aparece en fondo oscuro sin estilos completos cargados.
Captura 2. El sitio durante la remediación parcial: el contenido del cliente ya estaba presente en el servidor, pero la capa CSS aún no se había estabilizado. Estado transitorio habitual cuando se restauran archivos desde respaldo sin limpiar primero los artefactos de malware que modifican la pila de carga de WordPress.

Los huecos que siguen abiertos

Archivos de respaldo descargables sin autenticación. Cinco archivos de respaldo eran descargables directamente desde el servidor de producción sin ninguna credencial. Llevaban documentados veinte días desde que se identificaron. El de mayor tamaño supera los 200 MB y data de 2017; su contenido no fue abierto. Por tamaño y formato, el expediente infiere sistema de archivos más base de datos.

El volcado SQL de menor tamaño, de aproximadamente 12 MB, contenía los hashes de contraseña de al menos dos cuentas de usuario en formato phpass: MD5 con iteraciones, crackeable con GPU moderno en horas. También contenía datos personales de terceros: clientes de la empresa afectada registrados en las plataformas de comercio electrónico que usaban el sitio.

La base de datos actual, con un tamaño aproximado de 118 MB, devuelve HTTP 404 por petición directa: está en disco pero no es accesible desde el exterior. No todos los archivos sensibles tienen el mismo nivel de exposición.

Página de contenido spam activa. Al momento del análisis, la página puerta del spam devolvía HTTP 200 y servía aproximadamente 800 KB de contenido. Al 2026-09-19 fue verificada como HTTP 404: ese hueco específico ya se cerró.

Gestor de archivos por panel activo. El panel de administración seguía teniendo habilitado el gestor de archivos, lo que permite editar cualquier archivo PHP del sitio desde el navegador.

El .htaccess que oculta sin proteger

La carpeta de subidas tenía un archivo .htaccess con la directiva IndexIgnore *. Esta directiva oculta el listado del directorio cuando alguien navega a esa URL. No niega el acceso. Un atacante que conozca el nombre del archivo puede descargarlo directamente.

Diagrama de dos columnas. Izquierda en rojo: configuración existente con IndexIgnore * que oculta el listado pero no niega acceso; resultado: archivos .sql y .zip descargables. Derecha en verde: configuración con Options -Indexes más RegexMatch 403 para .sql, .zip, .gz, .tar y .bak; resultado: 403 Forbidden.
Ver fuente Mermaid
%%{init: {'theme': 'dark', 'themeVariables': {'primaryColor': '#1e293b', 'primaryTextColor': '#f8fafc', 'lineColor': '#94a3b8'}}}%%
flowchart LR
    subgraph L["Configuracion existente"]
        direction TB
        la["IndexIgnore *"]
        lb["Efecto real:\nOCULTA el listado del directorio\nNO niega el acceso"]
        lc["Resultado:\n.sql y .zip siguen\ndescargables sin autenticacion"]
        la --> lb --> lc
    end

subgraph R["Configuracion que habria negado el acceso"] direction TB ra["Options -Indexes"] rb["RedirectMatch 403 \.sql$"] rc["RedirectMatch 403 \.zip$"] rd["(y .gz, .tar, .bak)"] re["Resultado:\n403 Forbidden para archivos\nde base de datos y respaldo"] ra --> rb --> rc --> rd --> re end

L -. "vs." .-> R

style L fill:#7f1d1d,stroke:#ef4444,color:#fca5a5 style lc fill:#450a0a,stroke:#ef4444,color:#fca5a5,font-weight:bold style R fill:#14532d,stroke:#22c55e,color:#86efac style re fill:#052e16,stroke:#22c55e,color:#86efac,font-weight:bold

Figura 5. IndexIgnore * oculta el listado pero no niega acceso a archivos individuales. La protección real del sitio cubría archivos .php y no alcanzaba a .sql ni .zip. La columna derecha muestra un ejemplo de configuración que sí habría negado el acceso; no es una receta universal. Fuente: expediente forense interno, Transgenia, 2026.

La protección real que tenía el directorio cubría archivos .php —una directiva Deny para PHP. No cubría .sql ni .zip. La configuración de seguridad estaba parcialmente correcta, y esa parcialidad la hacía ineficaz exactamente donde importaba.


El cargador latente: cómo no titular un hallazgo

El ciclo de persistencia

La razón por la que un archivo en mu-plugins es más peligroso que en cualquier otro directorio no es su contenido: es su posición. WordPress carga todos los archivos PHP de ese directorio antes de procesar cualquier petición, sin excepción y sin que el panel de administración los muestre.

Flowchart: petición HTTP llega → WordPress inicializa → mu-plugins carga todos los PHP automáticamente → cargador.php se ejecuta → archivo oculto en posición → intento de popen bloqueado. El panel de administración queda fuera del ciclo: no ve nada.
Ver fuente Mermaid
%%{init: {'theme': 'dark', 'themeVariables': {'primaryColor': '#1e293b', 'primaryTextColor': '#f8fafc', 'lineColor': '#94a3b8'}}}%%
flowchart LR
    req["Peticion HTTP\ncualquier visitante"]
    wp["WordPress\ninicializa"]
    mu["mu-plugins/\nCarga TODOS los .php\nSIN excepcion ni VoBo del admin"]
    cargador["cargador.php\n56 bytes\nIncluye archivo oculto"]
    oculto["Archivo oculto\n522 bytes\nFunciones en hex"]
    panel["Panel de administracion\nNO muestra nada\nInvisible para el operador"]
    bloqueo["BLOQUEADO\npopen() deshabilitada\npor el hosting"]

req --> wp --> mu --> cargador --> oculto --> bloqueo mu -.->|"No aparece en"| panel

style mu fill:#7f1d1d,stroke:#ef4444,color:#fca5a5,font-weight:bold style cargador fill:#450a0a,stroke:#ef4444,color:#fca5a5 style oculto fill:#450a0a,stroke:#ef4444,color:#fca5a5 style bloqueo fill:#052e16,stroke:#22c55e,color:#86efac,font-weight:bold style panel fill:#1e293b,stroke:#64748b,color:#94a3b8,font-style:italic

Figura 9. Cada petición al sitio pasaba por el cargador del atacante. El bloqueo por popen() frenó la ejecución en este caso; sin él, el ciclo habría completado. Fuente: expediente forense interno, Transgenia, 2026.

El archivo de 56 bytes

En el directorio de plugins de carga obligatoria se encontró un archivo de 56 bytes. Su contenido era una sola instrucción: un envoltorio phar:// dentro de trim().

trim() no dispara ese envoltorio. El wrapper phar:// en PHP se activa cuando una función que toca el sistema de archivos —include, require, fopen, entre otras— recibe una ruta phar://. trim() procesa una cadena y la devuelve; no toca el sistema de archivos. Esto fue verificado en un contenedor aislado antes de incluir el hallazgo en el informe.

El archivo se saca del sitio de todas formas, por dos razones: carga en cada petición de WordPress por estar en el directorio de carga obligatoria, y no aparece en el listado de plugins del panel de administración. La razón exacta de esa invisibilidad en el panel no quedó establecida en el expediente.

Existe un segundo artefacto de 522 bytes con tres funciones de ejecución codificadas en escapes hexadecimales. La codificación hexadecimal es una técnica de evasión: una búsqueda de texto plano sobre el archivo no encontrará las palabras clave de las funciones.

El análisis antimalware

Antes de sellar el contenedor de evidencias, 210 archivos fueron sometidos a análisis antimalware. El resultado fue cero detecciones. El contenedor fue sellado con cifrado AES-256; los intentos posteriores de análisis sobre el contenedor sellado retornaron errores de protección por contraseña para las 288 entradas registradas. El resultado de cero detecciones corresponde a los 210 archivos revisados antes del sellado.

La explicación no es la ausencia de malware: los fragmentos de 16, 56 y 522 bytes están por debajo del umbral de longitud de cualquier firma antimalware conocida. Un motor de firmas no puede identificar lo que no tiene mapeado. Nadie en el equipo emitió el diagnóstico de "no hay malware porque el antivirus dice que no hay"; el expediente registra esto explícitamente porque es el razonamiento que el resultado invita a hacer.

Cómo titular correctamente

El titular tentador era: Se encontró un cargador PHAR activo en el directorio de plugins obligatorios.

El titular correcto es: Se encontró un cargador PHAR a una palabra de armarse, en un directorio que lo carga en cada petición y que lo hace invisible al panel.

La diferencia no es semántica: uno describe una amenaza activa que no existía; el otro describe correctamente la amenaza real: latente, no activa, pero en posición privilegiada.


Un conector de IA concedió más alcance que la credencial equivalente por REST

Durante el análisis, la REST API de WordPress estaba bloqueada en modo edición: devolvía error 401 incluso con una credencial de administrador válida. El bloqueo es una medida de endurecimiento estándar para sitios que no necesitan exponer esa interfaz.

Un plugin de conector de inteligencia artificial instalado en el sitio no respetaba ese endurecimiento. Por ese canal, el equipo pudo leer los 431 usuarios con sus roles sin ningún problema, usando las mismas credenciales que la REST API rechazaba.

La lección tiene dos caras. La inmediata: un vector de lectura estaba abierto donde el operador creía que estaba cerrado. La estructural: instalar un segundo conector para ganar acceso habría sumado código de terceros con pocas valoraciones a un sitio ya comprometido. Sumar superficie de ataque para ahorrar cinco clics es una mala operación de seguridad.


Cinco errores más una corrección hermana

E1 — Conteo parcial reportado como conteo final. El barrido del directorio de subidas produjo un manifiesto de exactamente 4,000 líneas. La última línea del manifiesto es el marcador de corte de la herramienta, no un nombre de archivo real. El barrido cubrió aproximadamente un tercio del directorio. La conclusión honesta: se encontró una fuga en la parte revisada; se desconoce qué hay en el resto.

E1-hermana — Instalado no equivale a activo. La API del panel devuelve plugins instalados en el servidor. La fuente que responde qué está activo es la tabla de opciones en la base de datos. Son dos preguntas distintas con fuentes distintas.

E2 — Capacidad del atacante sobreestimada. La descripción inicial del archivo de 56 bytes lo calificaba como una deserialización PHAR activa. El experimento en contenedor aislado demostró que no lo era tal como estaba escrito. El informe conserva la afirmación original junto a su corrección, con fecha. Eso lo hace auditable: quien lea el expediente puede seguir el razonamiento que llevó al error y la evidencia que lo refutó.

E3 — Artefacto del atacante confundido con ruido propio. Dos registros de error PHP, en el mismo sitio. Uno documenta la cadena de ejecución del atacante del 3 de julio. El otro fue generado por la propia sonda HTTP de la auditoría en un turno anterior. Lo único que los distingue: haber registrado qué hizo uno mismo y cuándo lo hizo. Sin ese registro, el segundo error habría parecido parte del ataque y habría sesgado el análisis.

E4 — La auditoría bloqueó la IP de Transgenia. Durante diecinueve días, el equipo no pudo acceder al sitio desde la oficina. Tardó seis días en darse cuenta de que el problema era el acceso propio, no el sitio. "El sitio está caído" significaba "nuestro acceso está bloqueado."

Cuatro causas suficientes por separado en una sola sesión de trabajo: autenticaciones fallidas repetidas, recorrido recursivo de directorios, sondeo de rutas con firma de escáner de vulnerabilidades, y peticiones por rango sobre archivos grandes. El operador del hosting compartía la misma dirección IP pública que el equipo de auditoría. La confirmación de que "yo tampoco lo veo" ratificaba el bloqueo, no lo descartaba. La solución fue medir desde un punto externo sin relación con el cliente.

E5 — Portada congelada atribuida al plugin de caché. La portada mostraba una versión desactualizada del sitio. Un GET y un POST a la misma URL, en el mismo segundo, devolvieron respuestas distintas: el origen estaba sano. La copia desactualizada venía de la capa intermedia del hosting, alimentada por una cabecera de caché de un año que un plugin había dejado escrita. Una particularidad: el escudo web del antimalware del hosting intercepta URLs que contienen query strings y devuelve un reto de JavaScript de aproximadamente 12 KB. Esto invalida cualquier medición con cache-buster en la línea de comandos y rompe peticiones automatizadas. Si la herramienta de medición está siendo interceptada, lo que se mide es la herramienta, no el sitio.

Tres columnas paralelas. Error E4: síntoma de sitio inaccesible, realidad de IP de la auditoría bloqueada por el hosting, corrección de medir desde punto externo. Error E5: síntoma de portada antigua, realidad de caché del hosting, corrección de GET vs POST al mismo segundo. Error E3: síntoma de fatal error que parece del atacante, realidad generado por la sonda propia, corrección de atribución retirada por escrito.
Ver fuente Mermaid
%%{init: {'theme': 'dark', 'themeVariables': {'primaryColor': '#1e293b', 'primaryTextColor': '#f8fafc', 'lineColor': '#94a3b8'}}}%%
flowchart LR
    subgraph E4["Error E4"]
        direction TB
        e4s["Sintoma:\nel sitio no responde\ndesde la oficina"]
        e4r["Realidad:\nIP de la auditoria bloqueada\npor el hosting"]
        e4c["Correccion:\nmedir desde un punto externo\nsin relacion con el cliente"]
        e4s --> e4r --> e4c
    end

subgraph E5["Error E5"] direction TB e5s["Sintoma:\nla portada muestra\ncontenido antiguo"] e5r["Realidad:\ncopia en cache de la\ncapa intermedia del hosting"] e5c["Correccion:\nGET vs POST al mismo segundo\ndesde la consola del navegador"] e5s --> e5r --> e5c end

subgraph E3["Error E3"] direction TB e3s["Sintoma:\nun fatal error coincide\ncon la firma del ataque"] e3r["Realidad:\nlo genero la propia\nsonda HTTP de la auditoria"] e3c["Correccion:\natribucion retirada\npor escrito con fecha"] e3s --> e3r --> e3c end

E4 -.-> E5 E5 -.-> E3

style e4r fill:#7f1d1d,stroke:#ef4444,color:#fca5a5 style e5r fill:#7f1d1d,stroke:#ef4444,color:#fca5a5 style e3r fill:#7f1d1d,stroke:#ef4444,color:#fca5a5 style e4c fill:#052e16,stroke:#22c55e,color:#86efac style e5c fill:#052e16,stroke:#22c55e,color:#86efac style e3c fill:#052e16,stroke:#22c55e,color:#86efac

Figura 6. Tres diagnósticos incorrectos en el mismo expediente, presentados por síntoma, realidad documentada y corrección aplicada. Sin registro propio de las acciones del equipo, E3 y E4 habrían sido invisibles. Fuente: expediente forense interno, Transgenia, 2026.

Por qué contar los errores suma autoridad. Un informe donde nada salió mal no es un informe sin errores: es un informe sin control de calidad. La ronda adversarial que refutó E2, el contenedor aislado que lo decidió, la nota de alcance que acotó E1, la atribución retirada por escrito en E3: eso es el método funcionando. El cliente tiene un expediente auditable, no una narrativa pulida.

Regla exportable. Antes de tocar un sitio comprometido, registra tu propia huella: dirección IP de salida, hora, tipo y volumen de peticiones. Avisa al hosting antes de comenzar. Si no lo haces, pasarás parte del tiempo investigando tus propias marcas.


¿Qué no prueba este expediente?

Flowchart bifurcado: el vector de entrada no está probado (nodo central en amarillo) se divide en H1 (credencial comprometida → inicio de sesión admin → instalación de plugin falso) y H2 (plugin vulnerable → ejecución remota de código → credenciales obtenidas).
Ver fuente Mermaid
%%{init: {'theme': 'dark', 'themeVariables': {'primaryColor': '#1e293b', 'primaryTextColor': '#f8fafc', 'lineColor': '#94a3b8'}}}%%
flowchart TB
    inicio["Vector de entrada\nNO PROBADO\n(hipotesis de trabajo)"]

subgraph H1["Hipotesis 1: Credencial comprometida"] direction TB h1a["Contrasena de administrador\nobtenida por phishing,\nfuerza bruta o reutilizacion"] h1b["Inicio de sesion\nen wp-admin"] h1c["Instalacion del plugin falso\nPublicacion del contenido spam"] h1a --> h1b --> h1c end

subgraph H2["Hipotesis 2: Plugin con vulnerabilidad"] direction TB h2a["Plugin legitimo instalado\ncon vulnerabilidad sin parchear"] h2b["Ejecucion remota de codigo\no carga de archivo malicioso"] h2c["Credenciales obtenidas\nPlugin falso instalado"] h2a --> h2b --> h2c end

inicio --> H1 inicio --> H2

style inicio fill:#78350f,stroke:#f59e0b,color:#fde68a,font-weight:bold style H1 fill:#1e293b,stroke:#64748b style H2 fill:#1e293b,stroke:#64748b

Figura 10. Las dos hipótesis de trabajo sobre el vector de entrada. El expediente documenta ambas como no probadas; el informe no elige entre ellas sin evidencia que las discrimine. Fuente: expediente forense interno, Transgenia, 2026.
  1. El vector de entrada no está probado. La hipótesis de trabajo más plausible es credencial comprometida o plugin vulnerable sin actualizar. Es una hipótesis, no una certeza documentada.
  2. La ejecución exitosa de código no está probada. El único registro disponible documenta un intento fallido del 3 de julio. No hay evidencia de una sesión de ejecución exitosa anterior o posterior.
  3. La exfiltración de datos no está probada. Exposición no equivale a extracción. Los archivos de respaldo eran accesibles; no hay evidencia de que hayan sido descargados por terceros. Esta distinción es relevante para la determinación de obligaciones bajo la LFPDPPP.
  4. El barrido del directorio de subidas cubrió aproximadamente un tercio. Es una estimación basada en la proporción del corte documentado, no un conteo exacto del total de archivos en el directorio.
  5. Una de las tres rutas del cargador de 56 bytes quedó sin verificar. La verificación en contenedor cubrió dos de los tres caminos de ejecución posibles; el tercero no fue probado.
  6. La cronología del contenido inyectado no es reconstruible. Solo la cronología de los archivos del sistema, basada en mtime, puede afirmarse con consistencia interna.

El caso está abierto al 2026-09-18

Estado al 2026-09-18: 25 de 33 acciones cerradas (76%). Tres pendientes críticos con fecha y owner asignado.

Gráfico de barras horizontal. Cerradas (76%): 25 acciones. Pendientes (24%): 8 acciones.
Ver fuente Mermaid
%%{init: {'theme': 'dark'}}%%
xychart-beta horizontal
    title "33 acciones documentadas — corte 2026-09-18"
    x-axis ["Pendientes (24%)", "Cerradas (76%)"]
    y-axis 0 --> 28
    bar [8, 25]
Figura 11. 25 de 33 acciones cerradas al 2026-09-18. Los 8 pendientes incluyen acceso sin autenticación a respaldos, hashes expuestos, gestor de archivos activo y confirmación de notificación LFPDPPP. Fuente: expediente forense interno, Transgenia, 2026.
Acción pendienteOwnerFecha límite
Cerrar archivos de respaldo descargables sin autenticación (documentado hace 20 días) Cliente + Saurat Xiuhcoatl 2026-09-19
Forzar cambio de contraseña en todas las cuentas del sitio (hashes phpass expuestos) Cliente 2026-09-22
Confirmar cierre de la notificación LFPDPPP: fecha de notificación y nombre del responsable Cliente + área legal Antes de quitar draft: true

Publicar este artículo con estado PENDIENTE es la decisión correcta por una razón práctica: la brecha existe independientemente del estado del artículo. Documentar los pendientes con owner y fecha los pone en un registro externo con timestamp. El artículo no anuncia el incidente: lo acota y establece responsabilidades visibles.

Marco regulatorio aplicable (LFPDPPP)

El artículo 36 de la Ley Federal de Protección de Datos Personales en Posesión de los Particulares establece que el responsable del tratamiento debe notificar a los titulares cuando ocurra una vulneración de seguridad significativa. El volcado SQL descargable contenía datos personales de clientes de la empresa afectada: nombres, datos de contacto y registros de transacciones de años anteriores. La determinación de si esta exposición equivale a una vulneración en los términos del artículo 36 requiere criterio legal. Lo que está documentado: los datos existían, eran accesibles sin autenticación y pertenecen a terceros identificables.

Flowchart del proceso LFPDPPP: incidente detectado → ¿datos personales afectados? → sí: evaluación de vulneración art. 36 → notificación → medidas correctivas → cierre con evidencia.
Ver fuente Mermaid
%%{init: {'theme': 'dark', 'themeVariables': {'primaryColor': '#1e293b', 'primaryTextColor': '#f8fafc', 'lineColor': '#94a3b8'}}}%%
flowchart TD
    inicio["Incidente de seguridad\ndetectado"]
    dp["¿El sistema afectado\ncontenia datos personales\nde terceros?"]
    si_dp["SI:\nVolcado SQL contenia\nhashes y datos de clientes"]
    vuln["¿Equivale a vulneracion\nbajo LFPDPPP Art. 36?\n(Determinar con asesoria legal)"]
    notif["Notificacion a la\nCuenta de Correo Electronico\nINAI (o directamente al titular\nsi el riesgo es alto)"]
    medidas["Implementar medidas\nde seguridad correctivas:\nCerrar acceso no autorizado\nCambiar credenciales"]
    cierre["Confirmar cierre:\nFecha de notificacion\nNombre del responsable\nEvidencia de remedios"]
    no_dp["NO aplica\nobligacion de notificacion\nLFPDPPP"]

inicio --> dp dp -->|"Si"| si_dp dp -->|"No"| no_dp si_dp --> vuln vuln -->|"Si equivale"| notif vuln -->|"Requiere evaluacion\n(pendiente)"| medidas notif --> medidas --> cierre

style si_dp fill:#7f1d1d,stroke:#ef4444,color:#fca5a5 style notif fill:#78350f,stroke:#f59e0b,color:#fde68a style cierre fill:#052e16,stroke:#22c55e,color:#86efac,font-weight:bold style no_dp fill:#1e293b,stroke:#64748b,color:#94a3b8

Figura 12. Marco de determinación de obligaciones bajo LFPDPPP Art. 36. Este diagrama describe el proceso; la determinación final de vulneración requiere asesoría legal. Fuente: expediente forense interno, Transgenia, 2026.

Responsable del tratamiento de datos personales en este informe: Efraín Carreón Ortiz, Director General, Centrum Transgenia S.A.S. de C.V. (RFC: CTR1708039T5). Contacto: dev@transgenia.org.

Captura de pantalla del navegador mostrando el sitio del cliente en su estado restaurado: hero image de carpintería en madera, logo corporativo, menú de navegación completo con Carpintería, Vestidores y Closet, Libreros, Mueble TV, Obra, Contacto.
Captura 3. El sitio restaurado al cierre de la remediación visual: el contenido original del cliente recuperó el control del dominio. Las brechas de seguridad documentadas en este artículo permanecen abiertas al 2026-09-18, independientemente del estado visual del sitio.

Preguntas frecuentes

¿Cuál es la diferencia entre limpiar el malware y cerrar la brecha?

Limpiar el malware implica remover artefactos maliciosos del sistema de archivos y la base de datos. Cerrar la brecha implica corregir los vectores que permitieron la intrusión y los que siguen exponiendo datos: accesos sin autenticación, credenciales comprometidas, permisos incorrectos. Un sitio puede estar limpio y seguir teniendo la brecha abierta; este expediente lo documenta en tiempo real.

¿Qué son los plugins de carga obligatoria (mu-plugins) en WordPress?

WordPress carga automáticamente, antes de cualquier plugin normal, todos los archivos PHP en el directorio mu-plugins. No aparecen en el listado de plugins del panel de administración y no pueden desactivarse desde ahí. Cualquier archivo en ese directorio se ejecuta en cada petición.

¿Por qué puede dar cero detecciones un análisis antimalware sobre un sitio comprometido?

Las firmas de los motores antimalware identifican patrones conocidos de longitud suficiente. Cuando el atacante usa fragmentos mínimos como cargadores de 56 bytes, las piezas no coinciden con ninguna firma documentada. Cero detecciones no equivale a ausencia de malware: equivale a que los artefactos están por debajo del umbral de cualquier firma actualmente en uso.

¿Qué es phpass y por qué representa un riesgo cuando el volcado de base de datos está expuesto?

phpass es el esquema de hash de contraseñas usado por WordPress por defecto hasta versiones recientes. Utiliza MD5 con iteraciones. Con equipo de GPU moderno, un hash phpass puede crackearse en horas o minutos. Si el volcado SQL de un sitio WordPress es descargable sin autenticación, los hashes de todos los usuarios con contraseña registrada están expuestos y son crackeables.

¿Cuándo aplica la LFPDPPP en un incidente de seguridad web?

La Ley Federal de Protección de Datos Personales en Posesión de los Particulares aplica cuando datos personales de terceros han sido expuestos por una vulneración de seguridad. En este caso, el volcado SQL contenía datos de clientes de la empresa afectada. La obligación de notificación y el plazo aplicable dependen de si la exposición equivale a una vulneración en los términos del artículo 36 de la ley. La determinación requiere criterio legal, no solo técnico.


Sobre el autor y Transgenia

Efraín Carreón Ortiz es Director General de Transgenia. Transgenia ayuda a empresas mexicanas a implementar soluciones de inteligencia artificial gobernada y a fortalecer su postura de seguridad digital. Para consultas escribe a dev@transgenia.org.


Sigue leyendo


Fuentes

El material primario de este artículo es el expediente forense interno de Transgenia, 2026. Los hallazgos son mediciones directas sobre el sistema de archivos, la base de datos y el registro de errores de PHP del sitio analizado, complementados con experimentos de verificación en contenedor aislado. No se publican rutas absolutas, nombres de archivo reales, identificadores de hosting ni datos de contacto del cliente.

Para contexto técnico externo:

Este caso se publica con los datos identificadores del cliente suprimidos y con autorización del responsable del tratamiento. Responsable de la publicación: Efraín Carreón Ortiz, Director General, Transgenia — dev@transgenia.org.

← Volver al Blog
Transgenia, Verified IT agency on Vai.me