Advertencia: Este bug ya está puesto en conocimiento de Microsoft.
Desde los tiempo más remotos, Microsoft ha incluido un método Format en todos sus entornos RAD. Format nos permite hacer cosas como imprimir un número en formato Octal o Hexadecimal, imprimir la fecha en un formato personalizado o incluso definir nuestro propio formato para números (utilizando la vieja notación del VB, #;-#;#).
Cuando uno entra en contacto con string.Format, se enamora de ella. Evita tener que estar utilizando constantemente el método ToString() de todo lo que no sea un string. Por ejemplo, cuando uno quiere rellenar una cadena de tareas completadas, resulta más fácil y legible por ejemplo hacer esto:
string.Format("Tareas completadas: {0} de {1}", numeroTarea, totalTareas);
Sin embargo, hay cosas para las que string.Format no fue creada como:
- Completar consultas SQL parametrizadas.
- Crear las rutas de acceso a archivo.
Ambos de estos casos hacen que nuestras aplicaciones tengan problemas de seguridad que podrían comprometer de manera notable el sistema.
En el primer caso, las consultas SQL parametrizadas haciendo uso de format consiguen que nuestra aplicación sea vulnerable a una SQL Injection. Sobre la SQL Injection, si no conoceis los riesgos que puede entrañar, recomiento la lectura de el blog de Chema Alonso: http://www.elladodelmal.com/
La cuestión es que para hacer consultas parametrizadas se dispone de un número de clases en el namespace System.Data.SqlClient que permitirán hacer estas de manera automatica.
Así, por tanto, lo que comunmente es fácil de encontrar como:
string.Format("SELECT COUNT(*) FROM users WHERE username = '{0}' AND password = '{1}'", username, password);
Se debe crear de otra manera: como un SqlCommand.
SqlCommand cmd = new SqlCommand("SELECT COUNT(*) FROM users WHERE username = @username AND password = @password");
cmd.Parameters.Add("@username", username);
cmd.Parameters.Add("@password", password);
La diferencia entre la primera y la segunda es que en la segunda se prepara la inserción y se elimina todo aquello que pueda ser nocivo para la cadena SQL. (Se duplican las ‘, etc…) y sin embargo los programadores (sobre todo aquellos que vienen del Visual Basic 6.0) no lo usan…
El segundo de los casos es otro igualmente interesante: combinar directorios tanto físicos como virtuales.
En este caso, voy a pegar un ejemplo muy usado: se trata del MyWebPagesStarterKit (version 1.1.3) disponible a través de Codeplex: http://www.codeplex.com/MyWebPagesStarterKit/
Se trata del fichero: DownloadHandler.ashx, localizado en el directorio raíz.
//===============================================================================================
//
// (c) Copyright Microsoft Corporation.
// This source is subject to the Microsoft Permissive License.
// See http://www.microsoft.com/resources/sharedsource/licensingbasics/sharedsourcelicenses.mspx.
// All other rights reserved.
//
//===============================================================================================
using System;
using System.Web;
using System.IO;
using MyWebPagesStarterKit;
///
/// This handler serves files from the "~/App_Data/_Downloads/" folder (which is by default not accessible over the web).
///
public class DownloadHandler : IHttpHandler
{
#region IHttpHandler Members
public bool IsReusable
{
get { return true; }
}
public void ProcessRequest(HttpContext context)
{
try
{
//get the sectionId
string section = context.Request.QueryString["section"];
//get the filename
string file = context.Request.QueryString["file"];
//build the path
string path = context.Server.MapPath(string.Format("~/App_Data/_Downloads/{0}/{1}", section, file));
// get the pageId for the authentication
string pageId = context.Request.QueryString["pg"];
//user authentication:
WebPage _page = new WebPage(pageId);
if (_page.AllowAnonymousAccess == false && context.User.Identity.IsAuthenticated == false)
throw new Exception("User is not allowed to view this file");
if (File.Exists(path))
{
context.Response.Clear();
context.Response.AddHeader("Pragma", "public");
context.Response.AddHeader("Expires", "0");
context.Response.AddHeader("Cache-Control", "must-revalidate, post-check=0, pre-check=0");
context.Response.AddHeader("Content-Type", "application/force-download");
context.Response.AddHeader("Content-Type", "application/octet-stream");
context.Response.AddHeader("Content-Type", "application/download");
context.Response.AddHeader("Content-Disposition", string.Format("attachment; filename={0}", file));
context.Response.AddHeader("Content-Transfer-Encoding", "binary");
context.Response.AddHeader("Content-Length", new FileInfo(path).Length.ToString());
context.Response.WriteFile(path);
}
}
catch
{
context.Response.Clear();
context.Response.End();
}
}
#endregion
}
Pues bien, para empezar a “despotricar” en estas 72 lineas de código se utiliza string.Format dos veces y en ambas se utiliza MAL. :P
Comenzaré por la segunda, ya que es la menos relevante en este post.
string.Format("attachment; filename={0}", file)
En este caso, parecen no haberse dado cuenta que los ficheros pueden contener espacios en el nombre. Al hacer esto así, el archivo llegará sin extensión dada la naturaleza de los encabezados http (al menos, pasa así en FireFox). La solución sería incluir unas comillas escapadas dentro de la cadena, como estas:
string.Format("attachment; filename=\"{0}\"", file)
Bien, solucionar esto no requiere demasiados conocimientos, sino más bien, experiencia en el desarrollo web. Además, la culpa no es del string.Format… pero bueno, por despotricar que no falte… }:-)
Ahora pasemos al primer problema:
string path = context.Server.MapPath(string.Format("~/App_Data/_Downloads/{0}/{1}", section, file));
Para empezar a criticar, leamos los comentarios en el código:
This handler serves files from the “~/App_Data/_Downloads/” folder (which is by default not accessible over the web).
Jejeje… :P
Para explotar el este bug, podríamos hacer uno o varios ejemplos… para empezar, vamos a hacer algo que lógicamente no podemos hacer en ASP.NET: descargarnos el Web.Config de un site.
Para ello, no hay más que:
- Buscar un id de página que sea público en el site.
- Algún archivo que bajar.
Así, podríamos descargar cualquier cosa que se nos antoje… utilizando la siguiente URL:
~/DownloadHandler.ashx?pg=d96dfbe6-5cc2-4256-80c5-b765c766b68b§ion=../..&file=Web.Config
Cuando se resulve el string.Format, lo que ocurre es esto:
~/App_Data/_Downloads/../../Web.Config y nos envia directamente hacia el archivo Web.Config. Parece que los chicos de Microsoft que han desarrollado este CMS no conocen a nuestro viejo amigo “..” que permite subir en el árbol de directorios… al poner el id de cualquier página pública, podemos descargar lo que nos plazca.
Lo mejor de descargar el web.config es encontrar algunas ConnectionStrings sin cifrar, etc…
Pero, lo mejor de todo, es descargar la base de datos de los usuarios del site: para ello, no hay más que llamar a la siguiente URL:
~/DownloadHandler.ashx?pg=d96dfbe6-5cc2-4256-80c5-b765c766b68b§ion=..&file=Users.Config
Con un id de página válido y público en las aplicaciones webs que utilicen MyWebPagesStarterKit.
Lo siguiente, después de haberse descargado la base de usuarios sería romper por fuerza bruta el hash SHA1 de la contraseña (para lo cual existen programas muy útiles) y tened en cuenta que, por defecto, este CMS no requiere caracteres no-alfanuméricos para ésta…
Sin embargo, como se trata de contruir y no de destruir, lo que podríamos hacer es añadir una linea de código más y solucionar este problema reemplazando “../” por string.Empty, para que nadie pueda subir de directorio:
string path = context.Server.MapPath(string.Format("~/App_Data/_Downloads/{0}/{1}", section, file).Replace("../", "");
Finalmente para no seguir extendiendo más este post, he de comentarios que existen distintos métodos para hacer este tipo de cosas de buena manera, haciendo uso de las clases:
- System.Web.Hosting.VirtualDirectory
- System.Web.Hosting.VirtualFile
- System.Web.Hosting.VirtualPathProvider
- System.Web.Security.FileAuthorizationModule
Así que ya sabeis: a securizar vuestras aplicaciones y aprended a usar el string.Format.
PD0: Por lo demás, el MyWebPagesStarterKit es seguro y ofrece innumerables ventajas. En ningún momento pretendo desprestigiar a Microsoft con esto.
PD1: En el Web.Config del MyWebPagesStarterKit viene el atributo <compilation debug=”true”>, ponedlo a false!!!
PD2: Esta información se ofrece con buenos propósitos, para securizar las aplicaciones web. Cualquier daño producido por ella no es de mi responsabilidad.