WordPress es el gestor de contenidos más popular del mundo, utilizado por más del 40% de los usuarios. Esta amplia adopción lo convierte en el objetivo principal de los piratas informáticos.
Esta publicación describe una vulnerabilidad bastante simple referente a la implementación de pingbacks de WordPress. Si bien el impacto de esta vulnerabilidad es baja para la mayoría de los usuarios en el caso de WordPress, el patrón de código vulnerable relacionado es bastante interesante de documentar, ya que posiblemente también esté presente en la mayoría de las aplicaciones web.
Primer contacto:
Esta vulnerabilidad fue reportada a WordPress el 21 de enero de 2022; de momento no hay ningún parche disponible. Consulta la sección “Parche” para obtener una orientación sobre las posibles soluciones a aplicar a tus instancias de WordPress.
Este problema fue informado por primera vez hace unos seis años, en enero de 2017 por otro investigador y muchos otros a lo largo de los años.
Debido a su bajo impacto y la necesidad de vincularlo a vulnerabilidades adicionales en software de terceros, se estima que esta versión no pondrá en peligro a los usuarios de WordPress y que estas recomendaciones que te haremos te ayudaran a fortalecer tu politica de seguridad establecida en tus instancias.
Detalles Técnicos:
Uso de la construcción vulnerable en la característica de pingback:
Los pingbacks es una forma en que los autores de blogs son notificados y se les muestra cuando otros blogs «amigos» hacen referencia a un artículo determinado: se muestran junto con los comentarios y se pueden aceptar o rechazar libremente. Tras bambalinas, los blogs tienen que realizar solicitudes HTTP entre sí para identificar la presencia de enlaces. Los visitantes también pueden activar este mecanismo.
Esta característica ha sido ampliamente criticada, ya que permite a los piratas informáticos realizar ataques distribuidos de denegación de servicio solicitando maliciosamente a miles de blogs que verifiquen los pingbacks en un solo servidor víctima. Los pingbacks todavía están habilitados de forma predeterminada en las instancias de WordPress debido a la importancia de las funciones sociales y comunitarias cuando se trata de blogs personales. Sin embargo, no se espera que esas solicitudes puedan enviarse a otros servicios internos alojados en el mismo servidor o segmento de red local.
La funcionalidad de pingback está expuesta en la API XML-RPC de WordPress. Como recordatorio, este es un endpoint de API que espera documentos XML en los que el cliente puede elegir una función a invocar junto con argumentos.
Uno de los métodos implementados es pingback.ping, esperando argumentos pagelinkedfrom y pagelinkedto: el primero es la dirección del artículo que hace referencia al segundo.
pagelinkedto tiene que apuntar a un artículo existente de la instancia local, aquí http://blog.tld/?p=1, y pagelinkedfrom a la URL externa que debe contener un enlace a pagelinkedto.
A continuación se muestra cómo se vería una solicitud a este endpoint:
POST /xmlrpc.php HTTP/1.1 Host: blog.tld [...] <methodCall> <methodName>pingback.ping</methodName> <params> <param> <value><string>http://evil.tld</string></value> </param> <param> <value><string>http://blog.tld/?p=1</string></value> </param> </params> </methodCall>
Implementación de la validación de URL
El método wp_http_validate_url() de WordPress core ejecuta un par de comprobaciones en las URL proporcionadas por el usuario para reducir los riesgos de abuso. Por ejemplo:
1. El destino no puede contener un nombre de usuario y contraseña;
2. El nombre de host no debe contener los siguientes caracteres: #:?[]
3. El nombre de dominio no debe apuntar a una dirección IP local o privada como 127.0.0.1, 192.168.*, etc.
4. El puerto de destino de la URL debe ser 80, 443 o 8080.
El tercer paso puede implicar la resolución de nombres de dominio si están presentes en la URL (por ejemplo, http://foo.bar.tld). En este caso, la dirección IP del servidor remoto se obtiene analizando la URL [1] y luego resolviéndola [2] antes de validarla para excluir rangos de IP no públicos:
Se observa en la ruta src/wp-includes/http.php lo siguiente:
$parsed_url = parse_url( $url ); // [1] // [...] $ip = gethostbyname( $host ); // [2] if ( $ip === $host ) { // Error condition for gethostbyname(). return false; } // IP validation happens here } // [...]
El código de validación parece implementado correctamente y la URL ahora se considera confiable.
Implementación de los clientes HTTP
Dos clientes HTTP pueden manejar solicitudes de pingback después de validar la URL, esto lo hace en función de las funciones de PHP disponibles: Requests_Transport_cURL y Requests_Transport_fsockopen. Ambos son parte de la biblioteca de solicitudes, desarrollados de forma independiente bajo el paragua de WordPress.
Echaremos un vistazo a la implementación de esto último. Sabemos que utiliza la API de flujos de PHP por su nombre. Opera a nivel de transporte y el cliente tiene que elaborar la solicitud HTTP manualmente. La URL se analiza nuevamente usando parse_url(), y luego la parte de host se usa para crear un destino compatible con la API de flujos de PHP (por ejemplo, tcp://host:port):
Revisamos la siguiente ruta wp-includes/Requests/Transport/fsockopen.php:
public function request($url, $headers = array(), $data = array(), $options = array()) { // [...] $url_parts = parse_url($url); // [...] $host = $url_parts['host']; else { $remote_socket = 'tcp://' . $host; } // [...] $remote_socket .= ':' . $url_parts['port'];
Adicionalmente, este destino se usa para crear una nueva transmisión con stream_socket_client(), y la solicitud HTTP se elabora y se escribe en él: (observamos la ruta: wp-includes/Requests/Transport/fsockopen.php)
$socket = stream_socket_client($remote_socket, $errno, $errstr, ceil($options['connect_timeout']), STREAM_CLIENT_CONNECT, $context); // [...] $out = sprintf("%s %s HTTP/%.1F\r\n", $options['type'], $path, $options['protocol_version']); // [...] if (!isset($case_insensitive_headers['Host'])) { $out .= sprintf('Host: %s', $url_parts['host']); // [...] } // [...] fwrite($socket, $out);
Como podemos ver, este proceso implica otra resolución de DNS, por lo que stream_socket_client() puede identificar la IP del host para enviar los paquetes.
El comportamiento del otro cliente HTTP, cURL, es muy similar y no lo trataremos aquí.
La vulnerabilidad
Esta construcción tiene un problema: el cliente HTTP tiene que volver a analizar la URL y volver a resolver el nombre de host para enviar su solicitud. Mientras tanto, ¡un atacante podría haber cambiado el dominio para apuntar a una dirección diferente de la validada antes!.
Esta clase de error también se llama Time-of-Check-Time-of-Use: un recurso es validado pero se puede cambiar más adelante antes de su uso efectivo. Es común encontrar dichas vulnerabilidades en las mitigaciones contra falsificaciones de solicitudes del lado del servidor (SSRF).
<div class="table"><blockquote class="twitter-tweet"><p lang="en" dir="ltr">Can you spot the vulnerability? <a href="https://twitter.com/hashtag/codeadvent2021?src=hash&ref_src=twsrc%5Etfw">#codeadvent2021</a> <a href="https://twitter.com/hashtag/csharp?src=hash&ref_src=twsrc%5Etfw">#csharp</a> <br/><br/>SSRF vulnerabilities are so 2020! <a href="https://t.co/y9CSxdc5MH">pic.twitter.com/y9CSxdc5MH</a></p>— Sonar (@SonarSource) <a href="https://twitter.com/SonarSource/status/1468248939379847168?ref_src=twsrc%5Etfw">December 7, 2021</a></blockquote> <script src="https://platform.twitter.com/widgets.js" charSet="utf-8"></script> </div>
Resumimos cómo se ven estos pasos sucesivos con el siguiente diagrama:
Escenarios de explotación
Hemos auditado el código con la esperanza de encontrar errores diferenciales del parser que permitan llegar a puertos no deseados o realizar solicitudes POST sin éxito: los pasos iniciales de validación de URL son lo suficientemente restrictivos como para evitar su explotación. Como se mencionó anteriormente, los atacantes tendrían que encadenar este comportamiento con otra vulnerabilidad para afectar significativamente la seguridad de la organización objetivo.
Parche
No tenemos conocimiento de ningún parche público disponible en este momento.
Abordar dichas vulnerabilidades requiere conservar los datos validados hasta que se utilicen para realizar la solicitud HTTP. Esto no debería desecharse ni transformarse después del paso de validación.
Los desarrolladores de WordPress siguieron este camino al introducir un segundo argumento opcional para wp_http_validate_url(). Este parámetro se pasa por referencia y contiene las direcciones IP en las que WordPress realizó la validación. El código final es un poco más detallado para adaptarse a versiones anteriores de PHP, pero la idea principal está aquí.
Como workaround, recomendamos que los administradores del sistema eliminen el handler pingback.ping del endpoint XMLRPC. Una forma de hacer esto es actualizar functions.php del theme en uso para introducir la siguiente llamada:
add_filter('xmlrpc_methods', function($methods) { unset($methods['pingback.ping']); return $methods; });
También es posible bloquear el acceso a xmlrpc.php a nivel del servidor web.
Resumen
En este artículo, describimos una vulnerabilidad blind SSRF que afecta a WordPress Core. Si bien el impacto se considera bajo, este es un patrón de código vulnerable generalizado que seguimos encontrando incluso en grandes proyectos. Alentamos a los desarrolladores a verificar sus propias bases de código para detectar este tipo de vulnerabilidades que, como hemos demostrado, puede ocultarse incluso en código muy popular y bien revisado.