Prevención de ataques CSRF en ASP.NET Framework 4.6.1
En este post trataremos un problema que sufren muchos sitios web, los ataques CSRF (Cross Site Request Forgery).
Las páginas web sufren este tipo de ataques debido a dos factores:
- No comprobar que los formularios de los que la aplicación recibe la información son los suyos propios.
- La ingeniería social.
¿Pero qué quiere decir esto?, paso a explicarlo:
¿De que va todo esto?
Un ataque CSRF se basa en el uso de las sesiones de usuario para actuar.
Cuando un usuario inicia sesión en la aplicación atacada, normalmente la sesión perdura durante todo el tiempo que el usuario utiliza esta aplicación, o incluso, cuando el usuario vuelve a utilizar la aplicación tras días después de haberla cerrado. A veces, incluso, la sesión puede no tener fecha de caducidad.
Por eso es muy importante comprobar que la información que recibe la aplicación proviene de un origen conocido.
Un ataque CSRF aprovechara esta falta de comprobación, y que la sesión del usuario este abierta (aquí entra el factor de ingeniería social) para colarle al usuario un formulario que parezca inofensivo, o incluso que sea idéntico al de nuestra aplicación, pero internamente envía información maliciosa para que esta actúe de manera que el atacante saque provecho.
Entremos en código
Nuestra base de datos es muy simple, solo necesitamos una simple tabla con usuarios y contraseñas para hacer login:
CREATE DATABASE CSRFDEMO;
USE CSRFDEMO;
CREATE TABLE CSRFUSER(
CSRF_ID INT IDENTITY(1,1) PRIMARY KEY,
CSRF_USERNAME NVARCHAR(50) UNIQUE,
CSRF_USERPASSWORD NVARCHAR(50)
)
INSERT INTO CSRFUSER VALUES
('PACO', 'PACO1234'),
('JUAN', 'JUAN1234'),
('JESUS', 'JESUS1234'),
('JOHN', 'JOHN1234')
Podemos copiar este script y ejecutarlo en nuesto SqlServer para crear la BBDD.
En Visual Studio abriremos una nueva solución en la que incluiremos 2 proyectos, en mi caso, uno llamado “CSRF_demo_post”, que será el “cliente” normal de la aplicación.
El otro proyecto llamado “Hacker” emulara lo que haría el atacante. Los dos proyectos son .NET Framework MVC
Podéis crearlos con contenido MVC, pero yo lo voy a hacer vacío y lo rellenaré a mano explicando paso a paso las partes del proyecto.
Para nuestro “cliente” (CSRF_demo_post) vamos a dividir la aplicación en partes.
Para poder conectar a la BBDD debemos añadir la dependencia de Entity Framerwork
Tendremos un contexto para acceder a la base de datos e identificar a los usuarios, crearemos una nueva carpeta “Contexts” sobre el proyecto y añadiremos una nueva clase para el contexto.
CsrfContext.cs
using CSRF_post_demo.Models;
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Web;
namespace CSRF_post_demo.Contexts
{
public class CsrfContext : DbContext
{
//Context of the database
public CsrfContext() : base("name=tjConnStr") { }
//DbSet to retrieve users
public DbSet<User> Users { get; set; }
}
}
Y añadiremos una cadena de conexión con nuestra base de datos en el fichero Web.config del proyecto escribiendo estas líneas dentro de <configuration>.
Web.config de Csrf_demo_post
<connectionStrings>
<add name="[Nombre de tu conexion]" connectionString="Data Source=LOCALHOST;Initial Catalog=CSRFDEMO;Persist Security Info=True;User ID=[Tu usuario];Password=[Tu contraseña]"
providerName="System.Data.SqlClient"/>
</connectionStrings>
También crearemos un repositorio con un modelo para mapear la base de datos, igual crearemos una nueva carpeta “Repositories” para el repositorio y añadiremos las clases.
RepositoryUser.cs
using CSRF_post_demo.Contexts;
using CSRF_post_demo.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace CSRF_post_demo.Repositories
{
//This class provides the functionallity for handleling users to the controllers.
public class RepositoryUser
{
CsrfContext Context;
public RepositoryUser()
{
Context = new CsrfContext();
}
/// <summary>
/// Searchs for a user in the DDBB by the parameters given.
/// </summary>
/// <param name="user">string. Users´s name.</param>
/// <param name="password">string. User´s password</param>
/// <returns>An user object or null.</returns>
public User ExistsUser(string user, string password )
{
User existingUser = (from data in Context.Users
where data.Name.Equals(user)
&& data.Password.Equals(password)
select data).FirstOrDefault();
return existingUser;
}
}
}
User.cs
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web;
namespace CSRF_post_demo.Models
{
//This class maps the users of the application
[Table("CSRFUSER")]
public class User
{
[Key]
[Column("CSRF_ID")]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ID { get; set; }
[Column("CSRF_USERNAME")]
public string Name { get; set; }
[Column("CSRF_USERPASSWORD")]
public string Password { get; set; }
}
}
Controladores
Ahora crearemos cuatro controladores, Click derecho sobre Controllers > Add Controller:
- HomeController, como inicio de aplicación. Nos servirá el index
- ValidationController, que será el encargado de autenticar los usuarios e iniciar sesión
HomeController.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace CSRF_post_demo.Controllers
{
public class HomeController : Controller
{
// GET: Home
public ActionResult Index()
{
return View();
}
}
}
ValidationController.cs
using CSRF_post_demo.Models;
using CSRF_post_demo.Repositories;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace CSRF_post_demo.Controllers
{
public class ValidationController : Controller
{
RepositoryUser Repo;
public ValidationController()
{
Repo = new RepositoryUser();
}
// GET: Vulnerable
public ActionResult Index()
{
return View();
}
// GET: Login
public ActionResult Login()
{
return View();
}
// POST: Login
[HttpPost]
public ActionResult Login(string user, string password)
{
User currentUser = Repo.ExistsUser(user.ToUpper(), password.ToUpper());
if (currentUser != null)
Session["USER"] = currentUser.Name;
return RedirectToAction("Index", "Home");
}
}
}
Validation utiliza un método muy simple de sesiones, porque para la demostración de este ataque no hace falta complicar mucho esa parte del código.
- Y dos controladores más que funcionaran como si
fuesen nuestra aplicación de verdad:
- CSRFController, que es un controlador vulnerable al ataque.
- ProtectedController, que es un controlador protegido contra el ataque.
CSRFController.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace CSRF_post_demo.Controllers
{
public class CSRFController : Controller
{
// GET: Principal
public ActionResult Index()
{
return View();
}
// GET: Buy
public ActionResult Buy()
{
return View();
}
// POST: Buy
[HttpPost]
//HERE SHOULD BE THE DIRECTIVE "[ValidateAntiForgeryToken]" TO PREVENT THE ATTACK
public ActionResult Buy(string product, string address)
{
TempData["SALE"] = "Product bought: " + product + "€. Sent to address: " + address + ".";
return RedirectToAction("SaleReport", "CSRF");
}
// GET: SaleReport
public ActionResult SaleReport()
{
ViewBag.SaleReport = TempData["SALE"].ToString();
return View();
}
}
}
ProtectedController.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace CSRF_post_demo.Controllers
{
public class ProtectedController : Controller
{
// GET: Principal
public ActionResult Index()
{
return View();
}
// GET: Buy
public ActionResult Buy()
{
return View();
}
// POST: Buy
[HttpPost]
//ATTRBUTE TO VALIDATE THE TOKEN FROM THE FORM
//IF THE TOKEN IS NOR RECIEVED, THIS ATTRBUTE PREVENTS THE ACTIONRESULT FROM DOING NOTHING
[ValidateAntiForgeryToken]
public ActionResult Buy(string product, string address)
{
TempData["SALE"] = "Product bought: " + product + "€. Sent to address: " + address + ".";
return RedirectToAction("SaleReport", "Protected");
}
// GET: SaleReport
public ActionResult SaleReport()
{
ViewBag.SaleReport = TempData["SALE"].ToString();
return View();
}
}
}
Estos dos controladores son exactamente iguales con la única diferencia de que uno está protegido y otro no.
Vistas
Tendremos una serie de vistas para cada controlador, podéis crearlas haciendo Click derecho sobre el nombre del ActionResult > Add View:
- Index (HomeController).
- Login (ValidationController).
- Buy y SaleReport (Tanto para CSRFController como para ProtectedController).
Ademas del _Layout.cshtml, ya que es una aplicacion MVC
Index.cshtml
@{
ViewBag.Title = "Index";
}
<h2>Index</h2>
<h1>CSRF demo</h1>
Login.cshtml
@{
ViewBag.Title = "Login";
}
<h2>Who are you?</h2>
@using (Html.BeginForm())
{
<div>
<label>
User:
<input class="form-control" type="text" name="user" value="" required />
</label>
</div>
<div>
<label>
Password:
<input class="form-control" type="password" name="password" value="" required />
</label>
</div>
<div>
<button class="btn btn-primary" type="submit">Login</button>
</div>
}
Buy.cshtml (CSRFController)
@{
ViewBag.Title = "Buy";
}
<h2>Buy</h2>
@using (Html.BeginForm())
{
@*WHE SHOULD PLACE HERE THE ANTI-FORGERY TOKEN*@
<div>
<label>
Product:
<select class="form-control" name="product">
<option value="Iphone X 1000">Iphone X</option>
<option value="Tesla model 3 10000">Tesla model 3</option>
<option value="Diamond collar 1500">Diamond collar</option>
</select>
</label>
</div>
<div>
<label>
Address:
<input class="form-control" type="text" name="address" value="" placeholder="Your address..." />
</label>
</div>
<div>
<button class="btn btn-warning" type="submit">Buy product</button>
</div>
}
SaleReport.cshtml (CSRCController)
@{
ViewBag.Title = "SaleReport";
}
<h2>Sales Report</h2>
<h1>@ViewBag.SaleReport</h1>
Buy.cshtml (ProtectedController)
@{
ViewBag.Title = "Buy";
}
<h2>Buy</h2>
@using (Html.BeginForm())
{
//THIS IS THE TOKEN THAT PREVENTS THE ATTACK FROM OCCUR
@Html.AntiForgeryToken()
<div>
<label>
Product:
<select class="form-control" name="product">
<option value="Iphone X 1000">Iphone X</option>
<option value="Tesla model 3 10000">Tesla model 3</option>
<option value="Diamond collar 1500">Diamond collar</option>
</select>
</label>
</div>
<div>
<label>
Address:
<input class="form-control" type="text" name="address" value="" placeholder="Your address..." />
</label>
</div>
<div>
<button class="btn btn-warning" type="submit">Buy product</button>
</div>
}
SaleReport.cshtml (ProtectedController)
@{
ViewBag.Title = "SaleReport";
}
<h2>Sales Report</h2>
<h1>@ViewBag.SaleReport</h1>
_Layout.cshtml
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@ViewBag.Title - My ASP.NET Application</title>
<link href="~/Content/Site.css" rel="stylesheet" type="text/css" />
<link href="~/Content/bootstrap.min.css" rel="stylesheet" type="text/css" />
<script src="~/Scripts/modernizr-2.6.2.js"></script>
</head>
<body>
<div class="container">
<nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
<a class="navbar-brand" href="#">CSRF</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarCollapse">
<ul class="navbar-nav mr-auto">
@if (Session["USER"] != null)
{
<li class="nav-item active">
@Html.ActionLink("vulnerable", "Buy", "CSRF", null, new { @class = "nav-link" })
</li>
<li class="nav-item active">
@Html.ActionLink("not vulnerable", "Buy", "Protected", null, new { @class = "nav-link" })
</li>
}
</ul>
<form class="form-inline mt-2 mt-md-0">
@if (Session["USER"] != null)
{
<a class="navbar-brand" href="#">Welcome back @Session["USER"].ToString()!</a>
}
else
{
@Html.ActionLink("Login", "Login", "Validation", null, new { @class = "btn bg-light" });
}
</form>
</div>
</nav>
</div>
<div class="container body-content">
@RenderBody()
<hr />
<footer>
<p>© @DateTime.Now.Year - My ASP.NET Application</p>
</footer>
</div>
<script src="~/Scripts/jquery-3.3.1.min.js"></script>
<script src="~/Scripts/bootstrap.min.js"></script>
</body>
</html>
Ahora vamos con nuestro proyecto “Hacker”. Este es mucho más simple, solo necesitamos un HomeController con tres vistas: Index, HackerVulneravleCSRF y HackerNotVulneravleCSRF.
HomeController.cs (Proyecto Hacker)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace Hacker.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}
public ActionResult HackerVulnerableCSRF()
{
ViewBag.Message = "Your contact page.";
return View();
}
public ActionResult HackerNotVulnerableCSRF()
{
ViewBag.Message = "Your contact page.";
return View();
}
}
}
Index.cshtml
@{
ViewBag.Title = "Index";
}
<h2>Index</h2>
@*This is just an index to access the two attack views.*@
<ul class="list-group">
<li class="list-group-item">
@Html.ActionLink("Attack to vulnerable controller", "HackerVulnerableCSRF", "Home")
</li>
<li class="list-group-item">
@Html.ActionLink("Attack to not vulnerable controller", "HackerNotVulnerableCSRF", "Home")
</li>
</ul>
HackerNotVulnerableCSRF.cshtml
@{
ViewBag.Title = "HackerNotVulnerableCSRF";
}
<h2>This page attack to a not vulnerable CSRF controller</h2>
@*The formulary targets the Action method in the controller with hidden inputs to secretly send the information and perform the attack.*@
<form action="http://localhost:57336/Protected/Buy" method="post">
<input type="hidden" name="product" value="10 Iphones X 10.000" />
<input type="hidden" name="address" value="The hackers house" />
<button class="btn btn-success" type="submit">Press this button for fun!!!</button>
</form>
HackerVulnerableCSRF.cshtml
@{
ViewBag.Title = "HackerVulnerableCSRF";
}
<h2>This page attack to a vulnerable CSRF controller</h2>
@*The formulary targets the Action method in the controller with hidden inputs to secretly send the information and perform the attack.*@
<form action="http://localhost:57336/CSRF/Buy" method="post">
<input type="hidden" name="product" value="10 Iphones X 10.000" />
<input type="hidden" name="address" value="The hackers house" />
<button class="btn btn-success" type="submit">Press this button for fun!!!</button>
</form>
_Layout.cshtml
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@ViewBag.Title - Mi aplicación ASP.NET</title>
@Styles.Render("~/Content/css")
@Scripts.Render("~/bundles/modernizr")
</head>
<body>
<nav class="navbar navbar-expand-lg fixed-top navbar-dark bg-dark">
<a class="navbar-brand" href="#">Hacker page</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarsExample01" aria-controls="navbarsExample01" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarsExample01">
<ul class="navbar-nav mr-auto">
<li class="nav-item active">
<a class="nav-link" href="#">It will look like a normal page<span class="sr-only">(current)</span></a>
</li>
</ul>
</div>
</nav>
<div class="container body-content">
@RenderBody()
<hr />
<footer>
<p>© @DateTime.Now.Year - Mi aplicación ASP.NET</p>
</footer>
</div>
@Scripts.Render("~/bundles/jquery")
@Scripts.Render("~/bundles/bootstrap")
@RenderSection("scripts", required: false)
</body>
</html>
¡Y con esto ya lo tenemos todo listo para probar la demo!
Demostración
Arranquemos el proyecto “CSRF_demo_post”. Click derecho sobre el proyecto > Debug > Start new instance.
Nos logueamos en la aplicación.
En mi caso voy a loguearme con estas credenciales:
- Usuario: john
- Contraseña: john1234
Ahora podemos ver que tenemos la sesión iniciada con nuestro mensaje de bienvenida y que en nuestra barra de navegación han aparecido dos opciones, una a la parte de la aplicación vulnerable y otra a la parte protegida.
Las dos partes parecen idénticas, pero no lo son. Al acceder a cualquiera de ellas nos encontramos con un pequeño formulario de compra en el que podemos seleccionar un producto de un desplegable, una dirección de envío y podremos comprar el producto con un botón. Accedamos a “vulnerable”.
Tras comprar el producto veremos un informe de la compra con el producto, su precio y la dirección de envío.
¡Y en este proyecto ya hemos acabado!, todo parece correcto ¿verdad?, pero si todo funcionase bien no estaríamos aquí, asique vamos a lanzar el proyecto del hacker y ver qué pasa si atacamos la aplicación.
Importante lanzar la aplicación mientras esta está aun iniciada con la sesión abierta.
Ataquemos la aplicación
Mismo proceso de antes, Click derecho sobre el proyecto > Debug > Start new instance.
Y al lanzar la aplicación del hacker nos encontraremos con esto .
Una página que puede parecer exactamente igual a la nuestra o completamente distinta y que no levanta sospechas y que el hacker nos habrá hecho abrir sin darnos cuenta debido a la ingeniería social. Desde esta página podemos atacar a las dos partes de nuestro otro proyecto, vamos a probar que pasa si atacamos a la vulnerable.
La página nos muestra un botón de apariencia inofensiva, pero al pulsar en él…
¿¡Que ha pasado!?
¡Vaya, que ha ocurrido!, ¿cuándo hemos comprado 10 Iphones?
Lo que ha pasado es muy simple. La página con el botón contenía un formulario oculto con información apuntando a nuestra aplicación, que como no distingue la fuente de la que llegan las peticiones, ha utilizado los datos de nuestra sesión abierta para comprarle al hacker 10 teléfonos y enviarlos a su casa. Todo un desastre. Pero ya que el daño está hecho, no hay que llorar sobre la leche derramada, vamos a ponerle solución al problema.
Solucionemos el problema
Podemos arreglar esto añadiendo dos simples líneas de código en nuestra aplicación.
[ValidateAntiForgeryToken]
Pongamos esta directiva en nuestro controlador:
// POST: Buy
[HttpPost]
//ATTRBUTE TO VALIDATE THE TOKEN FROM THE FORM
//IF THE TOKEN IS NOR RECIEVED, THIS ATTRBUTE PREVENTS THE ACTIONRESULT FROM DOING NOTHING
[ValidateAntiForgeryToken]
public ActionResult Buy(string product, string address)
{
TempData["SALE"] = "Product bought: " + product + "€. Sent to address: " + address + ".";
return RedirectToAction("SaleReport", "Protected");
}
// GET: SaleReport
public ActionResult SaleReport()
{
ViewBag.SaleReport = TempData["SALE"].ToString();
return View();
}
Y en nuestro formulario:
//THIS IS THE TOKEN THAT PREVENTS THE ATTACK FROM OCCUR
@Html.AntiForgeryToken()
@{
ViewBag.Title = "Buy";
}
<h2>Buy</h2>
@using (Html.BeginForm())
{
//THIS IS THE TOKEN THAT PREVENTS THE ATTACK FROM OCCUR
@Html.AntiForgeryToken()
<div>
<label>
Product:
<select class="form-control" name="product">
<option value="Iphone X 1000">Iphone X</option>
<option value="Tesla model 3 10000">Tesla model 3</option>
<option value="Diamond collar 1500">Diamond collar</option>
</select>
</label>
</div>
<div>
<label>
Address:
<input class="form-control" type="text" name="address" value="" placeholder="Your address..." />
</label>
</div>
<div>
<button class="btn btn-warning" type="submit">Buy product</button>
</div>
}
Como funciona la protección
Estas dos líneas añaden un “token” para identificar que el formulario del que recibimos la información es en verdad nuestro.
Si inspeccionamos el código de la página vemos que se ha añadido un input hidden con un valor aleatorio con el nombre de _RequestVerificationToken.
Y además, si utilizamos una herramienta de inspección de cookies en nuestro navegador, podemos observar que también se ha añadido una nueva cookie con el token de verificación.
Lo que la aplicación está haciendo es lo siguiente:
- Genera un valor aleatorio antes de cargar la pagina
- Mete ese valor en el input y en la cookie
- Al devolver la petición desde el formulario compara esos dos valores, y si no coinciden, la aplicación sabe que es una petición maliciosa.
De esa forma el atacante, al no tener ese token, no puede atacar la aplicación y es expulsado al lanzar el ataque.
Vamos a hacer la prueba.
Ahora que estamos protegidos
Esta vez, atacaremos a la parte protegida de la aplicación.
Y ahora, como nuestra aplicación está protegida, ¡el hacker no puede hacer nada que perjudique a nuestros usuarios!
Autor/a: Andrés Sánchez Robleño
Curso: Microsoft MCSA Web Applications + Microsoft MCSD App Builder + Xamarin
Centro: Tajamar
Año académico: 2018-2019
Enlace a GitHub:https://github.com/AndresSanRo/CSRF_post_demo
Enlace a Linkedin:https://www.linkedin.com/in/andrés-sánchez-robleño-7a5863162/