Forensic Case · WordPress Security

Cleaning Malware Is Not Closing the Breach: Forensic Anatomy of a Compromised WordPress

The anti-malware scan finished. The screen showed zero active threats. The team exhaled. Three weeks later, a routine analysis of the uploads folder revealed five backup files — database dumps included — downloadable from the production server without any authentication.

Short answer. Cleaning malware and closing the breach are two distinct jobs. The first can finish while the second remains completely open. This article documents both, including the five methodological errors our team made that kept them separate for weeks.

Browser screenshot showing the client's domain serving attacker content from AYAMJP: an online gambling portal with a games menu, attacker logo, and registration buttons. The URL bar belongs to the client's domain.
Screenshot 1. The client's site at the moment of compromise: the attacker had replaced the domain's visible content with their own gambling portal. The company's corporate domain was serving content entirely unrelated to its business. Source: internal case documentation, Transgenia, July 2026.

What Does a Compromised Site Do While the Team Cleans It?

The Database Lies; the mtimes Do Not

Any forensic analysis of a compromised WordPress site starts with one question: when did this happen? The answer the database offers is, in this case, unusable as a timeline.

Eight published entries declared dates earlier than others with a higher database identifier: they were created later but dated as if they were older. The same pattern appears in another table on the site, with four comments of consecutive identifiers distributed across two different years. It was not a timezone error: the date field was modified manually.

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.
View Mermaid source
%%{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

Figure 4. The actual creation order (database ID) contradicts the declared dates. Entry B has a higher ID — it was created later — yet declares a date earlier than Entry A. Source: Transgenia internal forensic file, 2026.

The valid timeline for this site is that of the file system modification times (mtime). They are consistent with each other and with the only documented code execution record. It was not proven that the attacker could alter them; claiming they are incorruptible would exceed the evidence. What can be stated: they are the only consistent source available.

The activity window reconstructed by mtime runs from June 25 to August 26, 2026.

The July 3 Execution Chain

The only moment when code execution can be demonstrated occurred on July 3, 2026. A directory created less than ten minutes before contained four elements: the complete copy of a legitimate plugin, a hidden file, a PHP loader, and a 16-byte configuration file. The choice of a plugins directory as the container was not accidental: any PHP file in that location is loaded automatically with every WordPress request.

The chain: the loader included the hidden file, which invoked an internal function that attempted to open an operating system process with popen(). The hosting provider had that function disabled. The attempt failed with the error recorded verbatim:

[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}

Figure 1. PHP error record from July 3, 2026. Absolute paths and real file names have been substituted. The failing function (popen), error type, and stack structure are from the original record. The exact timestamp is pending authorization to publish. Source: Transgenia internal forensic file, 2026.

This record is the only proof of code execution in the entire incident. The canonical reading of the block is the hosting provider's list of disabled functions; there is no other evidence that contradicts that interpretation.

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.
View Mermaid source
%%{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

Figure 2. Execution chain documented on July 3, 2026. The attempt to open an operating system process was blocked by the hosting provider's list of disabled functions. Source: Transgenia internal forensic file, 2026.

The Content in Numbers

At the time of analysis, the site had 604 inventoried entries: 115 published, 483 in the trash, 4 drafts, and 2 WordPress autodrafts. Of the 115 published, four corresponded to the client's legitimate content: 3.5 percent of the total. The rest: 102 sports betting entries in nine languages, five casino entries, and four WordPress default template entries.

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%).
View Mermaid source
%%{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]
Figure 3. The 115 published entries on the site. 96.5% corresponds to content injected by the attacker, concentrated in sports betting entries in nine languages. Source: Transgenia internal forensic file, 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.
View Mermaid source
%%{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

Figure 7. Compromised layers of the site and active exposure points. Nodes in red remained open at the time of the analysis cutoff. Source: Transgenia internal forensic file, 2026.

The authorship pattern confirms that the legitimate administrator account was used to publish spam: 10 of 18 entries with an assigned author were published under that identity. Of the 431 registered users, 428 were fake subscribers with no associated entries. The site's 38 categories included 29 sports betting categories. Of the 108 comments, none came from a real visitor: four were WordPress default template entries and eight were internal order notes from e-commerce platforms from previous years.


Cleaning Is Necessary. It Is Not Enough.

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).
View Mermaid source
%%{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

Figure 8. As of 2026-09-18, 76% of actions were closed and 24% remained open. The right column is the work that remains active. Source: Transgenia internal forensic file, 2026.

As of the 2026-09-18 cutoff, 25 of the 33 documented actions had been closed: 76 percent. The must-use plugins directory was clean. The second administrator account had been deleted. The WordPress core had been updated. Fifteen malware artifacts no longer existed in the file system.

What remained open was not minor.

Browser screenshot showing the client's site during the recovery phase. The logo and navigation menu are visible, but the page appears on a dark background without fully loaded styles.
Screenshot 2. The site during partial remediation: the client's content was already present on the server, but the CSS layer had not yet stabilized. A common transitional state when files are restored from backup without first removing the malware artifacts that modify WordPress's loading stack.

The Gaps That Remain Open

Backup files downloadable without authentication. Five backup files were downloadable directly from the production server without any credentials. They had been documented for twenty days since they were identified. The largest exceeds 200 MB and dates from 2017; its contents were not opened. By size and format, the case file infers file system plus database.

The smaller SQL dump, approximately 12 MB, contained the password hashes of at least two user accounts in phpass format: MD5 with iterations, crackable with modern GPU hardware in hours. It also contained personal data belonging to third parties: clients of the affected company registered on the e-commerce platforms that used the site.

The current database, approximately 118 MB in size, returns HTTP 404 on direct request: it is on disk but is not accessible from the outside. Not all sensitive files have the same level of exposure.

Active spam content page. At the time of analysis, the spam gateway page returned HTTP 200 and served approximately 800 KB of content. As of 2026-09-19 it was verified as HTTP 404: that specific gap has already been closed.

File manager active via admin panel. The administration panel still had the file manager enabled, which allows editing any PHP file on the site from the browser.

The .htaccess That Hides Without Protecting

The uploads folder had a .htaccess file with the IndexIgnore * directive. This directive hides the directory listing when someone browses to that URL. It does not deny access. An attacker who knows the file name can download it directly.

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.
View Mermaid source
%%{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

Figure 5. IndexIgnore * hides the directory listing but does not deny access to individual files. The site's actual protection covered .php files and did not extend to .sql or .zip. The right column shows an example configuration that would have denied access; it is not a universal recipe. Source: Transgenia internal forensic file, 2026.

The actual protection the directory had covered .php files — a Deny directive for PHP. It did not cover .sql or .zip. The security configuration was partially correct, and that partiality made it ineffective exactly where it mattered.


The Latent Loader: How Not to Label a Finding

The Persistence Cycle

The reason a file in mu-plugins is more dangerous than in any other directory is not its content: it is its position. WordPress loads all PHP files from that directory before processing any request, without exception and without the administration panel showing them.

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.
View Mermaid source
%%{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

Figure 9. Every request to the site passed through the attacker's loader. The block by popen() stopped the execution in this case; without it, the cycle would have completed. Source: Transgenia internal forensic file, 2026.

The 56-Byte File

In the must-use plugins directory, a 56-byte file was found. Its content was a single instruction: a phar:// wrapper inside trim().

trim() does not trigger that wrapper. The phar:// wrapper in PHP is activated when a function that touches the file system — include, require, fopen, among others — receives a phar:// path. trim() processes a string and returns it; it does not touch the file system. This was verified in an isolated container before including the finding in the report.

The file is removed from the site regardless, for two reasons: it loads on every WordPress request by being in the must-load directory, and it does not appear in the plugins list in the administration panel. The exact reason for that invisibility in the panel was not established in the case file.

There is a second artifact of 522 bytes with three execution functions encoded in hexadecimal escapes. Hexadecimal encoding is an evasion technique: a plain-text search on the file will not find the function keywords.

The Anti-Malware Analysis

Before sealing the evidence container, 210 files were subjected to anti-malware analysis. The result was zero detections. The container was sealed with AES-256 encryption; subsequent analysis attempts on the sealed container returned password protection errors for all 288 registered entries. The zero-detection result corresponds to the 210 files reviewed before sealing.

The explanation is not the absence of malware: the 16-, 56-, and 522-byte fragments are below the length threshold of any known anti-malware signature. A signature engine cannot identify what it has no mapping for. No one on the team issued the diagnosis "there is no malware because the antivirus says there is none"; the case file records this explicitly because it is the reasoning the result invites.

How to Label It Correctly

The tempting headline was: An active PHAR loader was found in the must-use plugins directory.

The correct headline is: A PHAR loader was found one word away from being armed, in a directory that loads it on every request and makes it invisible to the panel.

The difference is not semantic: one describes an active threat that did not exist; the other correctly describes the real threat: latent, not active, but in a privileged position.


An AI Connector Granted More Access Than the Equivalent REST Credential

During the analysis, the WordPress REST API was blocked in edit mode: it returned error 401 even with a valid administrator credential. The block is a standard hardening measure for sites that do not need to expose that interface.

An artificial intelligence connector plugin installed on the site did not respect that hardening. Through that channel, the team was able to read all 431 users with their roles without any problem, using the same credentials the REST API rejected.

The lesson has two sides. The immediate one: a read vector was open where the operator believed it was closed. The structural one: installing a second connector to gain access would have added third-party code with few reviews to an already compromised site. Adding attack surface to save five clicks is a poor security operation.


Five Errors Plus One Sister Correction

E1 — Partial count reported as final count. The sweep of the uploads directory produced a manifest of exactly 4,000 lines. The last line of the manifest is the tool's cutoff marker, not a real file name. The sweep covered approximately one third of the directory. The honest conclusion: a leak was found in the reviewed portion; what is in the rest is unknown.

E1-sister — Installed does not equal active. The panel API returns plugins installed on the server. The source that answers what is active is the options table in the database. These are two distinct questions with distinct sources.

E2 — Attacker capability overestimated. The initial description of the 56-byte file characterized it as an active PHAR deserialization. The experiment in an isolated container demonstrated that it was not, as written. The report preserves the original statement alongside its correction, with a date. That makes it auditable: whoever reads the case file can follow the reasoning that led to the error and the evidence that refuted it.

E3 — Attacker artifact confused with own noise. Two PHP error records, on the same site. One documents the attacker's execution chain from July 3. The other was generated by the audit's own HTTP probe in a previous session. The only thing that distinguishes them: having recorded what one did oneself and when. Without that record, the second error would have appeared to be part of the attack and would have biased the analysis.

E4 — The audit blocked Transgenia's IP. For nineteen days, the team could not access the site from the office. It took six days to realize the problem was their own access, not the site. "The site is down" meant "our access is blocked."

Four independently sufficient causes in a single work session: repeated failed authentications, recursive directory traversal, probing of paths with a vulnerability scanner signature, and range requests on large files. The hosting operator shared the same public IP address as the audit team. The confirmation that "I can't see it either" ratified the block, not disproved it. The solution was to measure from an external point with no connection to the client.

E5 — Frozen homepage attributed to the cache plugin. The homepage showed an outdated version of the site. A GET and a POST to the same URL, in the same second, returned different responses: the origin was healthy. The outdated copy came from the hosting's intermediate layer, fed by a one-year cache header that a plugin had left written. One particular detail: the hosting's anti-malware web shield intercepts URLs containing query strings and returns a JavaScript challenge of approximately 12 KB. This invalidates any measurement with a cache-buster on the command line and breaks automated requests. If the measurement tool is being intercepted, what is being measured is the tool, not the site.

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.
View Mermaid source
%%{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

Figure 6. Three incorrect diagnoses in the same case file, presented by symptom, documented reality, and correction applied. Without a record of the team's own actions, E3 and E4 would have been invisible. Source: Transgenia internal forensic file, 2026.

Why counting the errors adds authority. A report where nothing went wrong is not a report without errors: it is a report without quality control. The adversarial round that refuted E2, the isolated container that decided it, the scope note that bounded E1, the attribution withdrawn in writing in E3: that is the method working. The client has an auditable case file, not a polished narrative.

Exportable rule. Before touching a compromised site, record your own footprint: outgoing IP address, time, type, and volume of requests. Notify the hosting provider before you begin. If you do not, you will spend part of the time investigating your own marks.


What Does This Report Not Prove?

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).
View Mermaid source
%%{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

Figure 10. The two working hypotheses about the entry vector. The case file documents both as unproven; the report does not choose between them without evidence to discriminate. Source: Transgenia internal forensic file, 2026.
  1. The entry vector is not proven. The most plausible working hypothesis is compromised credential or unpatched vulnerable plugin. It is a hypothesis, not a documented certainty.
  2. Successful code execution is not proven. The only available record documents a failed attempt from July 3. There is no evidence of a successful execution session before or after.
  3. Data exfiltration is not proven. Exposure does not equal extraction. The backup files were accessible; there is no evidence that they were downloaded by third parties. This distinction is relevant for determining obligations under LFPDPPP (Mexico's Federal Personal Data Protection Law).
  4. The sweep of the uploads directory covered approximately one third. This is an estimate based on the proportion of the documented cutoff, not an exact count of the total files in the directory.
  5. One of the three paths of the 56-byte loader was not verified. The container verification covered two of the three possible execution paths; the third was not tested.
  6. The timeline of the injected content is not reconstructible. Only the timeline of the file system files, based on mtime, can be stated with internal consistency.

The Case Is Open as of 2026-09-18

Status as of 2026-09-18: 25 of 33 actions closed (76%). Three critical pending items with assigned dates and owners.

Gráfico de barras horizontal. Cerradas (76%): 25 acciones. Pendientes (24%): 8 acciones.
View Mermaid source
%%{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]
Figure 11. 25 of 33 actions closed as of 2026-09-18. The 8 pending items include unauthenticated access to backups, exposed hashes, active file manager, and confirmation of LFPDPPP notification. Source: Transgenia internal forensic file, 2026.
Pending ActionOwnerDeadline
Close downloadable backup files without authentication (documented 20 days ago) Client + Saurat Xiuhcoatl 2026-09-19
Force password change on all site accounts (phpass hashes exposed) Client 2026-09-22
Confirm LFPDPPP notification closure: notification date and responsible party name Client + legal team Before removing draft: true

Publishing this article with PENDING status is the correct decision for a practical reason: the breach exists independently of the article's status. Documenting the pending items with owner and date puts them in an external record with a timestamp. The article does not announce the incident: it bounds it and establishes visible responsibilities.

Applicable Regulatory Framework (LFPDPPP)

Article 36 of LFPDPPP (Mexico's Federal Personal Data Protection Law) establishes that the data controller must notify the data subjects when a significant security breach occurs. The downloadable SQL dump contained personal data belonging to clients of the affected company: names, contact information, and transaction records from previous years. The determination of whether this exposure constitutes a breach under the terms of Article 36 requires legal judgment. What is documented: the data existed, was accessible without authentication, and belongs to identifiable third parties.

Flowchart del proceso LFPDPPP: incidente detectado → ¿datos personales afectados? → sí: evaluación de vulneración art. 36 → notificación → medidas correctivas → cierre con evidencia.
View Mermaid source
%%{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

Figure 12. Framework for determining obligations under LFPDPPP Art. 36. This diagram describes the process; the final determination of a breach requires legal counsel. Source: Transgenia internal forensic file, 2026.

Data controller responsible for personal data in this report: Efraín Carreón Ortiz, General Director, Centrum Transgenia S.A.S. de C.V. (RFC: CTR1708039T5). Contact: dev@transgenia.org.

Browser screenshot showing the client's site in its restored state: carpentry hero image in wood, corporate logo, full navigation menu with Carpintería, Vestidores y Closet, Libreros, Mueble TV, Obra, Contacto.
Screenshot 3. The client's site restored at the close of visual remediation: the original content reclaimed control of the domain. The security gaps documented in this article remain open as of 2026-09-18, regardless of the site's visual state.

Frequently Asked Questions

What is the difference between cleaning malware and closing the breach?

Cleaning malware involves removing malicious artifacts from the file system and database. Closing the breach involves correcting the vectors that enabled the intrusion and those still exposing data: unauthenticated access, compromised credentials, incorrect permissions. A site can be clean and still have the breach open; this report documents that in real time.

What are must-use plugins (mu-plugins) in WordPress?

WordPress automatically loads, before any normal plugin, all PHP files in the mu-plugins directory. They do not appear in the plugin list in the admin panel and cannot be deactivated from there. Any file in that directory executes on every request.

Why can an anti-malware analysis return zero detections on a compromised site?

Anti-malware engine signatures identify known patterns of sufficient length. When the attacker uses minimal fragments like 56-byte loaders, the pieces do not match any documented signature. Zero detections does not equal absence of malware: it equals the artifacts being below the threshold of any currently active signature.

What is phpass and why does it represent a risk when the database dump is exposed?

phpass is the password hash scheme used by WordPress by default until recent versions. It uses MD5 with iterations. With modern GPU hardware, a phpass hash can be cracked in hours or minutes. If the SQL dump of a WordPress site is downloadable without authentication, the hashes of all registered users with a password are exposed and crackable.

When does LFPDPPP apply to a web security incident?

Mexico's Federal Personal Data Protection Law (LFPDPPP) applies when personal data belonging to third parties has been exposed through a security breach. In this case, the SQL dump contained data belonging to the affected company's clients. The notification obligation and applicable timeframe depend on whether the exposure constitutes a breach under Article 36 of the law. The determination requires legal judgment, not only technical judgment.


About the Author and Transgenia

Efraín Carreón Ortiz is General Director of Transgenia. Transgenia helps Mexican companies implement governed artificial intelligence solutions and strengthen their digital security posture. For inquiries write to dev@transgenia.org.


Keep Reading


Sources

The primary material for this article is Transgenia's internal forensic case file, 2026. The findings are direct measurements on the file system, database, and PHP error log of the analyzed site, supplemented by verification experiments in an isolated container. No absolute paths, real file names, hosting identifiers, or client contact data are published.

For external technical context:

This case is published with the client's identifying information suppressed and with authorization from the data controller. Publication responsible party: Efraín Carreón Ortiz, General Director, Transgenia — dev@transgenia.org.

← Back to Blog
Transgenia, Verified IT agency on Vai.me