Reports
MEDIA[AUTENTICACIÓN]#4043537

CampTix (WordPress): basta el email de un asistente para abrir el contenido privado de [camptix_private]

El token de CampTix se calculaba con la IP de quien lo pedía, así que conocer el email de un asistente bastaba para leer contenido reservado y, de paso, expulsar al titular real de la entrada.

Resumen
Resumen en castellano de un reporte público, no una traducción literal. El código y los comandos se mantienen como en el original.

Resumen

El shortcode [camptix_private] del plugin CampTix sirve para esconder parte de una página y enseñarla solo a quien tiene entrada para el evento. El fallo es de autenticación: esa comprobación no demostraba nada, porque el "token de vista" se construía únicamente con datos que aporta quien hace la petición.

El token es un MD5 del email del asistente y de la IP del visitante, generado en plugins/camptix/addons/shortcodes.php:

// plugins/camptix/addons/shortcodes.php:691
public function generate_view_token_for_attendee( $attendee_id ) {
    $email = get_post_meta( $attendee_id, 'tix_email', true );
    $ip    = isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : '';

    $view_token = md5( 'tix-view-token-' . strtolower( $email . $ip ) );
    return $view_token;
}

La IP pretendía atar el token al dispositivo del titular, pero en la práctica lo ata a quien pregunta. El manejador del formulario, enganchado a template_redirect, no usa nonce (el propio fichero desactiva ese aviso en la línea 441) y no envía nada a la dirección recibida: ni correo de confirmación ni enlace mágico. Si el email coincide con un asistente, calcula el token con la IP del solicitante, lo pone en la cookie y lo inyecta en la misma petición:

// shortcodes.php:454
if ( isset( $_POST['tix_private_shortcode_submit'] ) ) {
    $email = isset( $_POST['tix_email'] ) ? trim( stripslashes( $_POST['tix_email'] ) ) : '';
    ...
    $attendees = get_posts( array( 'post_type' => 'tix_attendee', 'post_status' => 'publish',
        'meta_query' => array( array( 'key' => 'tix_email', 'value' => $email ) ) ) );

    if ( $attendees ) {
        $attendee   = $attendees[0];
        $view_token = $this->generate_view_token_for_attendee( $attendee->ID );   // uses the REQUESTER's IP
        setcookie( 'tix_view_token', $view_token, ... );
        $_COOKIE['tix_view_token'] = $view_token;                                  // forced into this same request
        foreach ( $attendees as $attendee ) {
            update_post_meta( $attendee->ID, 'tix_view_token', $view_token );      // overwrites the real holder's token
            ...

Después, la verificación vuelve a calcular el valor esperado de la misma forma:

// shortcodes.php:568
$expected_view_token = $this->generate_view_token_for_attendee( $attendee->ID );
if ( $expected_view_token !== $view_token ) {
    $camptix->error( __( 'Looks like you logged in from a different computer. Please log in again.' ) );

Los dos lados de la comparación salen de la misma petición y de la misma IP, la del atacante, así que siempre coinciden. Lo único que se exige de verdad es conocer el email de un asistente, un dato que no es secreto: aparece en páginas de patrocinadores y ponentes, en listas de correo públicas o en confirmaciones reenviadas, y en direcciones corporativas se puede deducir por el formato.

El investigador lo clasificó con CVSS 4.0 6.3 (Media), CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N. El informe se envió el 19 de septiembre de 2026, se divulgó el 30 de octubre de 2026 y consta que hubo recompensa, de importe no público.

Pasos de reproducción

La prueba se hizo con CampTix 2.0.0 sobre WordPress 7.1.1, en una instalación local. El investigador añadió un fichero que solo simula get_wordcamp_post() para que CampTix cargue fuera de la plataforma wordcamp.org, sin tocar el código afectado.

  1. Se prepara una página con [camptix_private]...gated content...[/camptix_private] y un registro de asistente para [email protected].
  1. Primer control: un GET anónimo, sin POST, no muestra el contenido protegido, de modo que la barrera funciona.
occurrences of the gated marker: 0
  1. Segundo control: un POST con una dirección que no corresponde a ningún asistente tampoco lo muestra. Hace falta una dirección real, así que no es una anulación total de la barrera, sino un fallo de autenticación.
POST tix_private_shortcode_submit=1&[email protected]
occurrences of the gated marker: 0
  1. Ataque: desde el mismo cliente, sin cookie, nonce ni cuenta, se envía un POST con la dirección de la víctima y la respuesta incluye el contenido reservado.
POST tix_private_shortcode_submit=1&[email protected]
occurrences of the gated marker: 1
CTPRIVATE8B3F attendees-only content
  1. Efecto secundario: esa misma petición sobrescribe el token guardado del asistente.
tix_view_token was : ORIGINAL-TOKEN-OF-THE-REAL-ATTENDEE
tix_view_token now : [REDACTADO]

La cookie del titular real deja de coincidir y, en su siguiente visita, ve el mensaje "Looks like you logged in from a different computer. Please log in again."

Impacto

Cualquiera que conozca el email de un asistente puede leer lo que la organización de un WordCamp reservó a quienes tienen entrada: horarios para asistentes, datos de acceso al lugar, enlaces privados a vídeos, material de patrocinadores o información sobre alojamiento y fiestas posteriores. Basta un POST, sin cuenta ni entrada.

Hay dos efectos más:

  • Oráculo de asistencia: el formulario responde de forma distinta si la dirección tiene entrada o no, lo que permite comprobar si una persona asistió a un evento, un dato que esa persona no había publicado.
  • Bloqueo del titular: el token se sobrescribe en todos los registros que comparten esa dirección. Repitiendo la petición, un atacante puede mantener fuera indefinidamente a quien pagó la entrada, y el plugin solo deja constancia de la IP del atacante junto al asistente.

El propio investigador señala los límites:

  • Solo afecta a los sitios que usan [camptix_private], un complemento opcional. No comprobó cuántos sitios WordCamp lo emplean, y por eso puntuó AT:P; sin esa condición la nota sería 6.9.
  • El atacante debe conocer o adivinar la dirección de un asistente real.
  • Solo se revela el bloque protegido: esta vía no devuelve registros de entradas, datos de pago ni la lista de asistentes.
  • Se verificó en local con CampTix 2.0.0 y no contra un WordCamp en producción, porque eso habría supuesto leer contenido real usando la dirección de una persona real.

Remediación

El reporte figura como resuelto, pero no detalla el cambio aplicado. Estas son las recomendaciones del investigador:

  1. Generar el token con wp_generate_password( 32, false ) o wp_generate_uuid4() y guardarlo, sin derivarlo nunca de datos que aporte el solicitante.
  2. Enviar el enlace resultante por correo a la dirección indicada en lugar de desbloquear el contenido en la misma respuesta, de forma que se demuestre el control del buzón. Según el investigador, esta medida basta por sí sola para cerrar la divulgación del contenido.
  3. Añadir un nonce al formulario y limitar los envíos por IP, para que no sirva como oráculo de direcciones con entrada.
  4. No sobrescribir tix_view_token en todos los asistentes con esa dirección en cada envío, sino emitir tokens por sesión. Es lo que evita dejar sin acceso al titular real.

Qué aprender de este caso

  • Cuando un token se valide recalculándolo, comprueba de dónde salen sus ingredientes: si REMOTE_ADDR o un email enviado en el formulario son la única entrada, el atacante obtiene el mismo valor que el servidor y la comparación nunca falla.
  • En accesos "sin contraseña" por email, busca si el contenido se desbloquea en la misma respuesta al POST: si no llega nada al buzón, conocer la dirección equivale a ser su dueño.
  • Compara siempre el resultado de una dirección válida con el de una inexistente: una respuesta distinta ya convierte el formulario en un oráculo de quién está registrado.
  • Revisa qué se escribe antes de comprobar la identidad: aquí un update_post_meta sin verificar nada permitía invalidar la sesión de otra persona. Un token aleatorio por sesión evita que una petición ajena expulse al titular.