文档结构  
翻译进度:已翻译     翻译赏金:10 元 (?)    ¥ 我要打赏

介绍

很多 ASP.NET  MVC 开发者都会写出高性能的代码,很好地交付软件,等等。但是却并没有安全性方面的计划。本文通过 10 个要点来保证 MVC 代码的安全性。

如果还是 MVC 开发的新手,建议你先看看 Youtube 上的教程:-

https://www.youtube.com/watch?v=Lp7nSImO5vk

1) 安全配置错误 (必须设置自定义错误页面来处理错误)

有一种攻击是攻击者截获最终用户提交的表单数据,将其改变再将修改后的数据发送到服务器。

对于这种情况,开发者需要进行适当的验证,不过验证显示的大量错误信息中可能会泄漏服务器信息。

现在来演示一下这个过程。

示例:-

为了演示,我创建了一个员工页面用来获取员工详情。

图 1,使用标记从浏览器视图添加员工

EmployeeDetail 模型视图

图 2,EmployeeDetail 模型

看起来页面上有足够多的注解验证来保证安全。然后并不是,这个页面还不够安全!我会演示如果绕过这些验证。

第 1 段(可获 2.64 积分)

如果你还刚知道数据注解,那么这个 Youtube https://www.youtube.com/watch?v=Gft64NdIx3k 会是个很好的参考,它详解了如何使用数据注解来验证数据。

下面的截图展示了对地址字段的验证,这个字段要求值在 50 个字符以内。

图 3,为模型添加验证之后,对表单进行验证的时候可以看到,要求最多 50 个字符的地址字段中输入了太多字符,所以显示出了错误消息。

拦截添加员工视图

现在来拦截这个表单,然后向服务器提交拦截后的数据。我使用的工具叫 burp suit,它捕捉向服务器发送的请求以及服务器的响应。

第 2 段(可获 1.56 积分)

下面的截图显示我捕捉到了发往服务器的请求。

图 4,使用 burp suite 拦截添加员工的表单,你可以看到这个工具捕捉到了用户提交的表单数据。

下面的截图中,我捕捉到了发往服务器的请求,你可以看到我改变了地址信息,虽然它限制最多 50 个字符,但是我添加了不止 50 个字符,然后提交到服务器。

拦截添加员工表单的地址字段

图 5,在 burp suite 中拦截处理地址字段,并提交到服务器

下面的截图显示提交到服务器的请求包含了超过 50 个字符的地址数据。

第 3 段(可获 1.41 积分)

调试模式下查看员工表单

在提交了超过 50 个字符的地址字段到服务器之后,服务器抛出了异常。因为在数据库中,地址字段的数据类型是 varchar(50),如果数据超出了 50 个字符,显然会产生异常。

图 6,在 burp suite 中拦截地址字段并提交到服务器

图 7,拦截修改的地址字段导致错误

向用户显示错误时的问题

现在发生的异常会直接显示给攻击者,这会泄漏大量有价值的服务器信息和程序行为相关的信息。通过这些信息,攻击者可以通过多种方式及其组合来试探我们的系统。

第 4 段(可获 1.34 积分)

图 8:- 直接向用户呈现错误

解决办法:-

要解决这个问题,需要我们设置一些不会显示内部技术错误的页面,这些页面只会显示一些自定义的错误消息。

这里有两种方法:-

  1. 创建自定义的错误处理属性。
  2. 在 Web.config 文件中设置自定义错误页面

方法 1:-

使用 HandleErrorAttribute IExceptionFilterFilter 创建自定义的错误处理属性。

下面是使用 HandleErrorAttribute 的示例

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Mvc;

namespace MvcSecurity.Filters
{
    public class CustomErrorHandler : HandleErrorAttribute
    {
        public override void OnException(ExceptionContext filterContext)
        {
            Exception e = filterContext.Exception;
            filterContext.ExceptionHandled = true;
            var result = new ViewResult()
            {
                ViewName = "Error"
            }; ;
            result.ViewBag.Error = "Error Occur While Processing Your Request Please Check After Some Time";
            filterContext.Result = result;
        }
    }
}

 

第 5 段(可获 0.98 积分)

创建了自定义错误属性之后,我们要在整个应用中把它作为全局属性使用。因此我们需要在 App_Start 目录下的 FilterConfig 类中调用这个属性,如下:

using MvcSecurity.Filters;
using System.Web;
using System.Web.Mvc;

namespace MvcSecurity
{
    public class FilterConfig
    {
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
           filters.Add(new CustomErrorHandler());
        }
    }
}

一旦发生错误,CustomErrorHandler 属性就会被调用,它会把请求重定向到 Error.cshtml 页面。如果你想传递一些消息,可以在 CustomErrorHandler 属性中通过 @ViewBag.Error 来传递。

第 6 段(可获 0.85 积分)

Html 错误页面的代码

@{
    Layout = null;
}

<!DOCTYPEhtml>
<html>
<head>
<metaname="viewport"content="width=device-width"/>
<title>Error</title>
</head>
<body>
    <hgroup>
        <h1 class="text-danger">Error.</h1>
        <h2>@ViewBag.Error</h2>
        <h2></h2>
    </hgroup>
</body></html>

 

错误页面视图

图 9,如果应用中发生错误,显示自定义的错误页面

方法 2:-

在 Web.config 文件中设置自定义页面

如果你不想写属性,你可以在 Web.config 文件中设置自定义页面。在此之前先使用  HTML 创建一个简单的错误页面用来显示发生的错误。

Web.config 文件中有一个  system.web 标签,在这里添加 CustomErrors 标签。如下所示:

第 7 段(可获 1.09 积分)

图 10,设置自定义错误页面时的 Web.config 文件截图

Html 错误页面代码

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Error</title>
</head>
<body>
    <hgroup>
        <h1 class="text-danger">Error.</h1>
        <h2>An error occurred while processing your request...........</h2>
        <h2></h2>
    </hgroup>
</body>
</html>

Html 错误页面呈现

图 11,如果应用中发生错误,显示自定义的 HTML 错误页面

哇,这才是保护 Web 应用程序的第一步。接下来看看第二点。

2) 跨站请求伪造 (CSRF)

CSRF 漏洞让攻击者可以通过已验证通过并登录的用户账号进行操作,而且不会引起用户的注意。

第 8 段(可获 1.06 积分)

举一个简单的例子。

  • 用户登入银行服务器。
  • 银行验证后授权并在用户和银行服务器之间建立一个安全会话。
  • 攻击者向用户发送一个恶意链接,其内容是“立刻赚取100000美元”。
  • 用户点击这个恶意链接,这个网站会试图从你的账号转账给攻击者的账号。因为安全会话已经建立,所以恶意代码可以执行成功。

微软已经认识到这种威胁,并使用 AntiForgeryToken 来防止这种攻击。

解决办法:-

我们需要在表单内部添加

第 9 段(可获 1.18 积分)
@Html.AntiForgeryToken()

然后在处理提交([HttpPost])请求的 Action 方法上添加 [ValidateAntiForgeryToken] 属性,用于检查令牌是否有效。

在视图中添加 [AntiForgeryToken] 辅助方法

图 13,在视图中添加 AntiForgeryToken

为 Post[HttpPost] 方法添加 [ValidateAntiForgeryToken] 属性

图 14,为 [HttpPost] 方法(Index) 添加 ValidateAntiForgeryToken

使用 AntiForgeryToken

我们在视图上添加 AntiForgeryToken 辅助方法后,它会创建一个隐藏域,其值是一个唯一的令牌,同时为浏览器添加一项 Cookie。

第 10 段(可获 1.08 积分)

在我们从 HTML 上提交数据的时候,它会检查 __RequestVerificationToken 隐藏字段和 __RequestVerificationToken Cookie 是否存在。如果 Cookie 或 __RequestVerificationToken 隐藏字段任意缺失一个,或者它们的值并不一致,ASP.NET MVC 就不会进行后续处理。因此,我们可以在 ASP.NET MVC 中防止跨站请求伪造攻击。

RequestVerificationToken 截图

图 15,RequestVerificationToken 产生的隐藏域

RequestVerificationToken Cookie 截图

图 16,RequestVerificationToken 产生的 Cookie

第 11 段(可获 1.03 积分)

3) 跨站脚本 (XSS) 攻击

跨站脚本 (XSS) 攻击很觉,它通过向输入字段注入恶意脚本,使攻击者能够窃取凭据和其它有价值的数据,这会导致重大安全漏洞。

图 17,跨站脚本 (XSS).

在这个攻击中,攻击者访问网站并试图通过表单的注释输入框执行恶意脚本。这时候如果网站没有检查恶意代码,那么代码就可能在服务器上执行,造成损害。

让我们用一个简单的例子来帮助理解。下面是我们要保存的员工表单。在文本框中,我通过 SCRIPT 标记尝试执行某些恶意脚本。不过在提交的时候,MVC 会抛出错误提示有坏事发生。

第 12 段(可获 1.8 积分)

总的来说,默认情况下 ASP.NET 会防止跨站脚本攻击。

理解显示出来的错误

从客户端检查到一个存在潜在威胁的表单数据 (worktype="<script>alert('hi');").

因为 MVC 会验证用户输入的数据,如果用户试图执行不允许的脚本,就会发生这样的错误,这实在是个好消息。

图 18,通过 input 字段提交恶意脚本会导致错误

但现在我们希望能填入 SCRIPT 标签。比如,像 CodeProject 这样的编程网站确实需要用户提交代码和脚本片段。在这些场景中,我们希望用户能从 UI 提交代码。

 

第 13 段(可获 1.4 积分)

那么我们得搞清楚如何在不失安全性的情况下完成这项任务。

为了提交脚本,我们有4件事要做。

解决办法:-

  1. [ValidateInput(false)]
  2. [AllowHtml]
  3. [RegularExpressionAttribute]
  4. AntiXSS 库

方法 1:-

ValidateInput

[ValidateInput] 属性用在我们希望允许提交代码的控制器和动作方法上。

如果我们想允许提交标签,就需要设置 enableValidation 属性为 false([ValidateInput(false)]),这样它就不会进行验证。如果我们设置为 true ([ValidateInput(true)]),那输入的内容就会被验证。如果你把这个属性用于控制器,它就会作用于该控制器内的所有动作方法上,如果你把它用于某个动作方法,则只有这个方法会受到影响。

 

第 14 段(可获 1.73 积分)

不过 ValidateInput 属性会应用于模型的所有属性 (EmployeeDetails).

在 HttpPost 方法上应用 ValidateInputAttribute 的截图

图 19,在 HttpPost 方法上应用 ValidateInput 属性

应用 ValidateInputAttribute 之后的截图

图 20,在 HttpPost 方法加添加 ValidateInput 属性后,它允许提交脚本

方法 2:-

AllowHtml

[AllowHtml] 用在模型属性上,使用了 AllowHtml 属性的模型属性将不会被验证,这样就可以在防御了跨站脚本攻击的情况下提交 HTML。

第 15 段(可获 0.93 积分)

在下面的截图中,我在 EmpolyeeDetail 模型的 Address 属性中应用了 AllowHtml 属性。

图 21,在指定模型属性上应用 [AllowHtml] 属性

在 Address 属性上应用了 AllowHtml 属性之后,将不再验证 Address 属性,从而允许向这个字段提交 HTML。

图 22,在地址属性中应用 [AllowHtml] 之后它就允许提交脚本了

方法 3:-

正则表达式

第三种解决 XSS 攻击的办法使用正则表达式验证所有字段,这样可以限制只有有效的数据可以填入。

第 16 段(可获 1.05 积分)

下面是用正则表达式验证输入以保护不受 XSS 攻击的截图

图 23,在模型属性中应用正则表达式验证

常用正则表达式列表

字母和空格

[a-zA-Z ]+$

字母

^[A-z]+$

数字

^[0-9]+$

字母和数字

^[a-zA-Z0-9]*$

电子邮件

[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?

手机号

^([7-9]{1})([0-9]{9})$

日期格式( mm/dd/yyyy | mm-dd-yyyy | mm.dd.yyyy)

/^(0[1-9]|1[012])[- /.](0[1-9]|[12][0-9]|3[01])[- /.](19|20)\\d\\d+$/

网站 URL

第 17 段(可获 1.05 积分)

^http(s)?://([\\w-]+.)+[\\w-]+(/[\\w- ./?%&=])?$

信用卡号

Visa

^4[0-9]{12}(?:[0-9]{3})?$

MasterCard

^5[1-5][0-9]{14}$

American Express

^3[47][0-9]{13}$

含小数的数字

((\\d+)((\\.\\d{1,2})?))$

办法 4:-

AntiXSS 库

防御 XSS 攻击的第四个办法是使用 Microsoft AntiXSS 库,它能保证你的应用程序。

我们从使用 NuGet 安装 Microsoft AntiXSS 库开始, 右键点击项目,然后选择管理NuGet包

图 24,选择管理 NuGet 包来添加包

选择之后会弹出一个管理 NuGet 包对话框,在这里搜索 AntiXSS 库。选中第一项,AntiXSS,并点击安装按钮。

第 18 段(可获 1.35 积分)

图 25,往项目中添加 Microsoft AntiXSS 库

安装后的引用

图 26,在项目中添加 Microsoft AntiXSS 库之后

完成安装之后我们来看看如何使用 AntiXSS 库

Sanitizer 类

图 27,Sanitizer 类用于净化输入

下面的截图展示如何使用 Sanitizer 类方法

Sanitizer 是一个静态类,我们可以在任何地方访问它。我们只需要向 Sanitizer 类方法 (GetSafeHtmlFragment) 提供要验证的字段的输入,它就会返回经过检查和净化后的字符串。

第 19 段(可获 1.16 积分)

图 28,这里以净化地址字段为例展示如何使用 Sanitizer 类净化输入

我们可以用这个方法在保存到数据库和显示到浏览器的时候过滤恶意脚本。

技巧:- 在使用 AntiXSS 库之前使用 [ValidateInput(false)] 或者 [AllowHtml],否则会报错“Form 请求中存在潜在的威胁”

4) 恶意文件上传

到目前为止,我们已经学会了如何保护所有输入字段不受攻击,但仍然缺少一个主要的输入域,即文件上传。很多攻击者会通过上传恶意文件来进行攻击,造成安全问题,我们需要对此进行防护。攻击者可能会修改文件扩展名 [tuto.exe 改为 tuto.jpeg],恶意脚本可能会被当作图片上传上来。多数开发者只看文件扩展名,就将这些文件保存在目录或数据库。但文件扩展名并不代码文件内容,这个文件有可能是恶意脚本。

第 20 段(可获 2.11 积分)

图 29,此图展示人们如何上传允许或不允许的文件

解决办法:-

  1. 我们需要做的第一件事是验证文件上传
  2. 只允许上传需要的文件扩展名
  3. 检查文件头

首先,在视图中添加一个文件上传控件

添加文件上传控件

图 30,在添加员工视图中添加文件上传控件

添加文件上传控件之后需要在提交时验证文件

在 Index HttpPost 方法中验证文件上传

在这个方法中,我们先验证文件的内容长度是否为 0 [upload.ContentLength == 0],为 0 则不允许上传这个文件。

第 21 段(可获 1.45 积分)

如果内容长度大于 0 则说明包含文件 [upload.ContentLength > 0],然后我们会访问 File 的 Filename,以及 ContentType 和 Bytes。

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Index(EmployeeDetail EmployeeDetail)
{
    if (ModelState.IsValid)
    {
        HttpPostedFileBase upload = Request.Files["upload"];
        if (upload.ContentLength == 0)
        {
            ModelState.AddModelError("File", "Please Upload Your file");
        }
        else if (upload.ContentLength > 0)
        {
            string fileName = upload.FileName; // getting File Name
            string fileContentType = upload.ContentType; // getting ContentType
            byte[] tempFileBytes = new byte[upload.ContentLength]; // getting filebytes
            var data = upload.InputStream.Read(tempFileBytes, 0, Convert.ToInt32(upload.ContentLength));
            var types = MvcSecurity.Filters.FileUploadCheck.FileType.Image;  // Setting Image type
            var result = FileUploadCheck.isValidFile(tempFileBytes, types, fileContentType); // Validate Header

            if (result == true)
            {
                int FileLength = 1024 * 1024 * 2; //FileLength 2 MB 
                if (upload.ContentLength > FileLength)
                {
                    ModelState.AddModelError("File", "Maximum allowed size is: " + FileLength + " MB");
                }
                else
                {
                    string demoAddress = Sanitizer.GetSafeHtmlFragment(EmployeeDetail.Address);
                    dbcon.EmployeeDetails.Add(EmployeeDetail);
                    dbcon.SaveChanges();
                    return View();
                }
            }
        }
    }
    return View(EmployeeDetail);
}

 

第 22 段(可获 0.35 积分)

目前为止我们对文件所做的都是基本验证。我写了一个静态类 FileUploadCheck,在这个类中有各种方法用来验证不同的文件类型。现在我告诉你们如何验证图片文件,并只允许上传图片文件。

FileUploadCheck 类

图 31,展示 FileUploadCheck 类,它被定制用于验证文件上传。

在上面的截图中有个 ImageFileExtension 枚举,它包含了图像格式和文件类型。

private enum ImageFileExtension
{
    none = 0,
    jpg = 1,
    jpeg = 2,
    bmp = 3,
    gif = 4,
    png = 5
}
public enum FileType
{
    Image = 1,
    Video = 2,
    PDF = 3,
    Text = 4,
    DOC = 5,
    DOCX = 6,
    PPT = 7,
}

 

第 23 段(可获 1.08 积分)

如果它通过了基本验证,我们会调用 isValidFile 方法,它需要字节(内容)、文件类型和文件的 ContentType 作为输入。

public static bool isValidFile(byte[] bytFile, FileType flType, String FileContentType)
{
    bool isvalid = false;

    if (flType == FileType.Image)
    {
        isvalid = isValidImageFile(bytFile, FileContentType);
    }
    else if (flType == FileType.Video)
    {
        isvalid = isValidVideoFile(bytFile, FileContentType);
    }
    else if (flType == FileType.PDF)
    {
        isvalid = isValidPDFFile(bytFile, FileContentType);
    }

    return isvalid;

}

 

第 24 段(可获 0.25 积分)

调用 isValidFile 方法后它会调用另一个关于文件类型的方法。

如果文件类型是图像,会调用第1个方法 [isValidImageFile],如果不是图像而是视频则会调用第2个方法 [isValidVideoFile]。类似的如果文件是 PDF,则会调用最后一个方法 [isValidPDFFile]。

搞明白 isValidFile 方法之后我们来看看要调用的 [isValidImageFile] 方法。

下面是 [isValidImageFile] 方法的完整代码段

在这个方法中,我们允许限制文件扩展名 [jpg, jpeg, png, bmp, gif]

第 25 段(可获 1.11 积分)

使用 isValidImageFile 方法

我们把字节和文件的 ContentType 传入这个方法之后,它会先检查 ContentType 并据此设置

ImageFileExtension

之后就会检查文件头是否与之匹配,如果匹配则文件是有效的 [true],不匹配则无效 [false]。

public static bool isValidImageFile(byte[] bytFile, String FileContentType)
{
    bool isvalid = false;

    byte[] chkBytejpg = { 255, 216, 255, 224 };
    byte[] chkBytebmp = { 66, 77 };
    byte[] chkBytegif = { 71, 73, 70, 56 };
    byte[] chkBytepng = { 137, 80, 78, 71 };


    ImageFileExtension imgfileExtn = ImageFileExtension.none;

    if (FileContentType.Contains("jpg") | FileContentType.Contains("jpeg"))
    {
        imgfileExtn = ImageFileExtension.jpg;
    }
    else if (FileContentType.Contains("png"))
    {
        imgfileExtn = ImageFileExtension.png;
    }
    else if (FileContentType.Contains("bmp"))
    {
        imgfileExtn = ImageFileExtension.bmp;
    }
    else if (FileContentType.Contains("gif"))
    {
        imgfileExtn = ImageFileExtension.gif;
    }

    if (imgfileExtn == ImageFileExtension.jpg || imgfileExtn == ImageFileExtension.jpeg)
    {
        if (bytFile.Length >= 4)
        {
            int j = 0;
            for (Int32 i = 0; i <= 3; i++)
            {
                if (bytFile[i] == chkBytejpg[i])
                {
                    j = j + 1;
                    if (j == 3)
                    {
                        isvalid = true;
                    }
                }
            }
        }
    }


    if (imgfileExtn == ImageFileExtension.png)
    {
        if (bytFile.Length >= 4)
        {
            int j = 0;
            for (Int32 i = 0; i <= 3; i++)
            {
                if (bytFile[i] == chkBytepng[i])
                {
                    j = j + 1;
                    if (j == 3)
                    {
                        isvalid = true;
                    }
                }
            }
        }
    }


    if (imgfileExtn == ImageFileExtension.bmp)
    {
        if (bytFile.Length >= 4)
        {
            int j = 0;
            for (Int32 i = 0; i <= 1; i++)
            {
                if (bytFile[i] == chkBytebmp[i])
                {
                    j = j + 1;
                    if (j == 2)
                    {
                        isvalid = true;
                    }
                }
            }
        }
    }

    if (imgfileExtn == ImageFileExtension.gif)
    {
        if (bytFile.Length >= 4)
        {
            int j = 0;
            for (Int32 i = 0; i <= 1; i++)
            {
                if (bytFile[i] == chkBytegif[i])
                {
                    j = j + 1;
                    if (j == 3)
                    {
                        isvalid = true;
                    }
                }
            }
        }
    }

    return isvalid;
}

 

第 26 段(可获 0.7 积分)

从动作方法中调用 isValidFile 方法

我们会调用 (FileUploadCheck.isValidFile) 并传入文件的字节(内容)、类型、ContentType 作为参数。

这个方法会返回布尔值,用 true 表示文件有效,false 表示文件无效。

string fileName = upload.FileName; // getting File Name
string fileContentType = upload.ContentType; // getting ContentType
byte[] tempFileBytes = new byte[upload.ContentLength]; // getting filebytes
var data = upload.InputStream.Read(tempFileBytes, 0, Convert.ToInt32(upload.ContentLength));
var types = MvcSecurity.Filters.FileUploadCheck.FileType.Image;  // Setting Image type
var result = FileUploadCheck.isValidFile(tempFileBytes, types, fileContentType); // Validate Header

 

第 27 段(可获 0.55 积分)

搞懂了上面那段代码之后我们来看个演示。

下面的截图是一个带文件上传 控件的员工表单

我们会填写这个表单并上传一个有效的文件。

图 32,含文件上传的添加员工表单

选择一个有效的 .jpg 文件并检查它具体如何验证

从磁盘上选择一个 .jpg 图像。

图 33,选择文件上传

选择文件之后的员工表单

在这里我们已经选择了一个文件。

图 34,已选择文件准备上传

调试 Index Post 动作

这一部分我们提交了一个含有文件的员工表单,我们可以看看对它进行的基本验证。

第 28 段(可获 1.51 积分)

图 35,提交用于保存的表单之后,实时显示出来我们上传的文件

调试 Index Post 动作

这一部分,你会看到已经通过了基本验证

图 36,提交表单数据后,实时显示上传的文件

调用 isValidFile 方法时的 FileUploadCheck 类

这部分是调用 isValidFile 方法之后,正准备根据文件的 ContentType 调用另一个方法。

图 37,根据文件类型调用 FileUploadCheck 类内部的方法

第 29 段(可获 1.24 积分)

isVaildImageFile 方法检查文件头数据

这个方法会检查上传的图像文件的文件头是否与我们指定的相匹配,并以此判断文件是否有效。

图 38,检查图像的文件头是否与指定的相匹配

5) 版本披露

攻击者可以利用暴露出来的版本信息针对特定的版本进行攻击。

浏览器通过 HTTP 向服务器发送请求后都收到响应头会包含这些信息:[Server, X-AspNet-Version,X-AspNetMvc-Version, X-Powered-By]。

第 30 段(可获 1.2 积分)

显示的这些信息正是 Web 服务器正在使用的。

X-AspNet-Version 是使用的 Asp.Net 版本。

X-AspNetMvc-Version 是 ASP.NET MVC 版本。

X-Powered-By 是框架版本信息。

图 39,响应头暴露了版本信息

解决办法:-

  1. 删除 X-AspNetMvc-Version 头

我们可以通过内置的 MVC 属性显示 ASP.NET MVC 版本的 X-AspNetMvc-Version。

只需要在 Global.asax 的应用启动事件 [Application_Start()] 中设置 [MvcHandler.DisableMvcResponseHeader = true;],这样响应头中就不会再出现 MVC 的版本信息 了。

第 31 段(可获 1.19 积分)

图 40,在 Global.asax 中通过设置属性移除 X-AspNetMvc-Version 响应头。

图 41,删除 X-AspNetMvc-Version 响应头之后的响应信息

2) 删除 X-AspNet-Version 和服务器响应头

要删除服务器响应头显示的服务相关的信息以及 X-AspNet-Version,只需要在 Global.asax 的 [Application_PreSendRequestHeaders()] 事件中像下面这样设置个属性:

protected void Application_PreSendRequestHeaders()
{
    Response.Headers.Remove("Server");           //Remove Server Header    
    Response.Headers.Remove("X-AspNet-Version"); //Remove X-AspNet-Version Header
}

 

第 32 段(可获 0.98 积分)

图 42,在 Global.asax 中添加 Application_PreSendRequestHeaders 事件,从中删除响应头

图 43,删除 X-AspNet-Version 和 Server 响应头之后的响应信息

3) 删除 X-Powered-By 头信息

X-Powered-By 响应头中包含运行网站的框架信息。

要删除 [X-Powered-By]  响应头,只需要在 Web.Config 文件的 System.webServer 配置添加下面的标签。

<httpprotocol>
<customheaders>

</customheaders>
</httpprotocol>

图 44,在 Web.config 中添加自定义头标签来删除响应头

图 45,删除 X-Powered-By 响应头之后的响应信息

第 33 段(可获 0.94 积分)

6) SQL 注入攻击

SQL 注入攻击是最危险的攻击之一,它在 OWASP2013 [开放Web应用安全项目] 提到的 10 大漏洞中排名第1。SQL 注入攻击可以向攻击者提供有价值的数据,攻击者可以利用这个安全漏洞访问数据库服务。

SQL注入攻击者总是试图输入恶意的 SQL 语句,使之在在数据库中执行并返回出本来不应该返回的数据给攻击者。

图 46,SQL 注入攻击示例,展示最常见的使用内联查询进行攻击。

第 34 段(可获 1.18 积分)

显示用户数据的简单视图

下面的截图是基于 EmployeeID 显示单个员工的数据。

图 47,显示用户数据的员工视图

在受到 SQL 注入攻击之后显示所有用户数据的简单视图

攻击者可以在这个浏览器视图中看到应用 URL 中包含重要数据 ID [http://localhost:3837/EmployeeList/index?Id=2],攻击者如下图所示进行SQL注入攻击。

图 48,受到 SQL 注入攻击之后显示所有用户数据的员工视图

通过对排序和组合 SQL 注入攻击,攻击者可以访问所有用户数据。

第 35 段(可获 1.16 积分)

在调试模式下显示 SQL 注入

这里我们可以看到攻击者使用的恶意 SQL 语句。

图 49,调试模式下看 Index Action 方法

SQL Profiler 视图中的 SQL 语句

图 50,SQL Profiler 视图

解决办法:-

  1. 验证输入
  2. 使用权限较低的数据库用户
  3. 使用参数化查询
  4. 使用 ORM (比如 Dapper、Entity framework )
  5. 使用存储过程
  1. 验证输入

在客户端和服务器端都对输入进行验证,避免攻击者通过特殊字符进入系统。

第 36 段(可获 1.16 积分)

我们在 MVC 中使用数据注解来进行验证

数据注解属性很容易应用于模型来验证数据

客户端验证输入

图 51,客户端验证输入

服务端验证输入

模型状态为 false,这表明模型有错

图 52,服务端验证输入

2) 给予最小权限的数据库用户

Db_owner 是数据库的默认角色,它可以授权和取消授权,创建表、存储过程、视图,执行备份、计划任务,甚至删除数据库。如果使用这种角色来访问数据库,用户会拥有完整的访问权,并可以进行各种活动。我们必须创建一个具有访问数据所需要的最低权限的新用户来进行操作。

第 37 段(可获 1.59 积分)

比如,如果用户只需要进行查询、插入和更新员工详情的操作,那么只需要为其分配 select、insert 和 update 权限。

添加用户并分配权限的步骤

这里我会展示一个创建用户并分配指定权限的示例。

  1. 创建新用户
  2. 创建用户之后进行查询
  3. 选择用户可以访问的对象(表)
  4. 选择指定的表来分配权限

图 53,创建新用户并分配权限

选择表之后,我们按下图所示分配权限。你可以看到我们已经给用户分配了 'Inset'、'Select‘、’Update' 权限。

第 38 段(可获 1.38 积分)

图 54,分配 Insert、Select 和 Update 权限

使用最小权限可以帮助我们防止攻击者对数据库的攻击。

3) 使用存储过程

存储过程是参数化查询的一种形式。使用存储过程也是保护不受 SQL 注入攻击的方法之一。

在下面的截图中我们去掉了之前使用的内联查询,改写为使用存储过程来从数据库获取数据,这能有效防止 SQL 注入攻击。使用存储过程的时候我们需要传入参数 [@EmpID],它会根据这个参数从数据库获取数据记录。我们会在代码中使用 CommandType [CommandType.StoredProcedure] 来表明是在使用存储过程。

第 39 段(可获 1.41 积分)

注意:- 总是使用带参数的存储过程,如果你不使用参数,仍然很容易受到 SQL 注入攻击。

图 55,使用存储过程显示员工详情

使用存储过程之后的 GetEmployee 视图

显示了数据的员工视图。

图 56,使用存储过程之后的员工详情

使用存储过程后的 Profiler 视图

跟踪显示我们使用的存储过程。

图 57,跟踪存储过程执行

使用存储过程后再次尝试进行 SQL 注入攻击

图 58,尝试在使用存储过程后进行 SQL 注入攻击

第 40 段(可获 1.19 积分)

使用存储过程后在 Debug 模式下查看 SQL 注入攻击的执行过程

你可以看到,包含恶意 SQL 脚本的参数 id [?Id=2 or 1=1] 在传递给存储过程之后会显示一个错误,指出存储过程未能执行,因为它需要一个数值类型的参数,但我们传递进去的却是恶意 SQL 脚本 [?Id=2 or 1=1]。

图 59,使用存储过程

图 60,使用存储过程后尝试 SQL 注入攻击

使用存储过程后的 Profiler 视图

图 61,跟踪存储过程执行

第 41 段(可获 1.26 积分)

输出:-

图 62,在攻击者进行攻击之后显示错误

这个时间你可能会认为如果参数定义为 varchar 类型,攻击就会成功。让我们再来看看。

使用带有 @name 参数的存储过程后在 Debug 模式下查看 SQL 注入攻击的执行过程

这已经是第二次我们通过传入不同的参数来观察存储过程真正保护 SQL 注入攻击了。

图 63,使用存储过程

图 64,在使用存储过程后尝试SQL注入攻击

图 65,跟踪存储过程执行

第 42 段(可获 1.15 积分)

输出:-

图 66,使用存储过程后如果我们尝试 SQL 注入攻击,它不会执行也不会有结果输出

使用参数化查询

使用参数化查询是另外一种保护不受 SQL 注入攻击的解决办法。它不通过连接字符串的方式,而是需要为 SQL 查询添加参数并传递给 SqlCommand 使用。

图 68,参数化查询也可以保护不受 SQL 注入攻击

参数化查询的 Profiler 视图

图 69,跟踪参数化查询的执行

输出:-

图 70,使用参数化查询之后如果我们尝试 SQL 注入攻击它不会执行也没有结果显示出来

第 43 段(可获 1.38 积分)

使用 ORM (Entity framework )

ORM 是对象关系映射(Object Relational Mapper),用于将 SQL 对象映射到领域(模型)对象 [C#]。

如果你使用实体框架,则不容易受到 SQL 注入攻击,因为实体框架内部使用参数化查询。

实体框架的常规执行

这里我们把名称作为参数 [?name=Saineshwar].

图 71,使用 Entity Framework 后传入参数

控制器执行时的截图

在调试模式下我们可以看到查询字符串传入的 name 参数,我们在 Linq 中使用它来获取记录。

第 44 段(可获 1.18 积分)

图 72,控制器执行

Linq 查询的 Profiler 视图

Entity Framework 内部确实使用参数化查询的截图

图 73,跟踪 Entity Framework 生成的查询

尝试通过 SQL 注入攻击 Entity Framework

在这部分我们尝试传入参数  [?name=Saineshwar or 1=1] 来攻击实体框架。

图 74,尝试对 Entity Framework 进行 SQL 注入攻击

控制器执行时的截图

在调试模式中可以看到我们把查询字符串中的 name 参数传递给 Linq 来获取记录,但这时候正常的参数中存在恶意脚本。

第 45 段(可获 1.38 积分)

图 75,使用 Enity Framework 后尝试 SQL 注入攻击没有输出

Linq 查询的 Profiler 视图

如果你仔细观察跟踪信息就会发现它把名称和恶意脚本作为一个单独的参数来处理,这就防止了 SQL 注入攻击。

图 76,尝试 SQL 注入攻击时跟踪由 Entity Framework 产生的查询

7) 暴露敏感数据

所有网站或应用程序都有一个存储着所有数据的数据库。也就是说,我们把用户的私人信息(可能包含密码、PAN码,护照详情,信用卡号等)保存在其中,然而我们只对密码进行了加密,其它数据都是明文存储,这在攻击者通过攻击得到数据库访问权的时候会造成敏感数据泄漏。他们会找到存储着这些私人数据和财务数据的表并从中偷取信息。

第 46 段(可获 1.66 积分)

图 77,敏感数据泄漏

简单的演示嗅探敏感数据。

登录页面截图

创建项目时如果选择 Internet 模板,默认的登录页会有这样简单的代码。

图 78,登录页面标记及效果截图

看到了登录页面的代码和显示效果,现在输入凭证进行登录。

输入凭证进行登录

在登录页面我们会输入用户名和密码来登录到应用程序。

图 79,输入凭证

攻击者拦截登录页面来盗取凭证

第 47 段(可获 1.18 积分)

用户在登录页面输入凭证并向服务器提交数据[username 和 password]的时候,数据是明文传输的,这些数据[username 和 password] 可以被攻击者拦截以获取凭证。

下面的截图展示了你的数据被攻击者拦截

图 80,拦截登录页面并显示 form 数据

解决办法 :-

  1. 总是以加密的方式向服务器传输敏感数据,使用健壮的随机 Hash
  2. 总是对 Web 应用使用 SSL
  3. 不要保存敏感数据,如果你需要存储,使用强大的 Hash 技术
第 48 段(可获 1.16 积分)

使用强大的随机 Hash 加密发往服务器的敏感数据

在这个示例中,我们会使用  MD5 算法在客户端加密数据并发送到服务器端,这样攻击者就不能盗取相关信息了。

下面的截图是用户在输入凭证。

图 81,使用加密数据之后输入凭证

用户输入凭证之后,在点击登录按钮时,会对用户输入的密码使用某个种子进行 MD5 加密,之后再发往服务器。这个过程中如果有攻击者在嗅探网络,他只会看到加密后没法解密的 Hash。

第 49 段(可获 1.3 积分)

你可以在下面的截图中看到攻击者嗅探网络的详情。

截图中蓝色线条表示用户名

截图中的红色线条表示随机加密后的密码

截图中的绿色线条表示由服务器生成的随机种子

图 82,拦截加密数据后的登录页数据

到目前为止,我们已经从截图看到了效果,再来看看代码。

登录模型

图 83,LoginModel 截图

下面是 [HttpGet] Login 行为方法的代码

在这个方法中,我们产生随机 Hash [种子] 然后把它赋值给 LoginModel 再传递给登录视图。

第 50 段(可获 1.35 积分)

图 84,生成随机 Hash 并将其赋予 [hdrandomSeed],然后将这个模型传递给视图。

给模型的 [hdrandomSeed] 赋值后在 Login 行为方法中我们将它处理成一个隐藏域。

图 85,使用 [hdrandomSeed] 属性在视图中生成隐藏域

现在页面上添加了一个隐藏域,我们来看看 JavaScript 中如何使用这个种子和 MD5 来加密数据。

我们需要使用 jQuery 1.8 和 md5.js 库,在用户点输入凭证并点击登录按钮之后,我们先获取用户输入的密码 [ var password1 = $('#Password');] 并生成 Hash [ calcMD5(password).toUpperCase()],然后加入种子再生成一次 Hash [ var hash = calcMD5(seed + calcMD5(password).toUpperCase());],这个结果是唯一的,它会被发送给服务器。

第 51 段(可获 1.73 积分)

在下面的截图中查看详情

图 86,客户端使用种子和 MD5 算法产生 Hash 的代码截图

登录页面的调试视图

在变量窗口中,你可以实时看到用户点击登录按钮时生成的值。

图 87,调试模式下的客户端加密

客户端加密后攻击者可以拦截并查看密码文本。

拦截登录页

下面的截图中,用户输入了密码,然后密码被加密了,攻击者不会知道那是什么,因为它是由种子和密码共同计算的 Hash。

第 52 段(可获 1.39 积分)

图 88,拦截登录页面

拦截之后我们可以看到提交到 Login 行为方法的数据。

从登录视图提交数据到 Login 行为方法之后

这里我们可以清楚地看到密码是加密后的形式。

图 89,从登录视图提交数据到 Login 行为方法后

下一步,我们要比较数据库中存储的密码和用户输入并加密的密码。标记为绿色的是种子值,标记为红色的是通过用户名从数据库取得的存储的密码,标记为黄色的是从登录页面提交过来的密码,最后蓝色标记的数据是通过数据库密码和种子计算而来,用于和提交的密码进行比较。

第 53 段(可获 1.98 积分)

图 90,Login Actoin 方法的代码及实时显示的值

最终,用户输入的敏感数据受到了保护。

2) 使用 SSL 来保护 Web 应用程序

SSL (Secure Sockets Layer,安全套接字层) 是客户端和服务端进行通讯的安全(加密)层,它保证在客户端和服务端传输的所有数据 [银行信息、密码和其它金融交易] 都是安全(加密)的。

Fig 91.SSL (安全套接字层)

SSL 主要用在登录页面和支付网关。如果你愿意,也可以用于整个应用。

第 54 段(可获 1.1 积分)

如果你想知道关于如何在 IIS Server上启用SSL的详细情况的话,这里有一篇来自Scott Guthrie Sir的博客的好文可以参考,地址如下:

http://weblogs.asp.net/scottgu/tip-trick-enabling-ssl-on-iis7-using-self-signed-certificates

3) 不要在数据库里以明文形式来存储敏感数据

一定不要在数据库里以明文形式存储信用卡、借记卡和金融数据以及其他敏感数据。要总是先使用强散列技术来对数据进行加密,然后再存储进数据库里。如果攻击者获取到直接访问数据库的权限,那么所有以明文形式存储的数据将都会被窃取。

第 55 段(可获 1.18 积分)

下面几种算法可按需使用。

哈希(散列)算法

如果只是想计算摘要可以使用哈希算法,我们常用哈希算法来加密密码。

对称算法

如果想使用一个密钥来进行加密和解密,那么可以选用对称算法。

非对称算法

如果想使用一个密钥来(公钥)加密,然后使用另一个密钥(私钥)来解密,那可以选用非对称算法。比如我们可以在与客户分享 Web 服务和 Web API 的时候使用非对称算法。

哈希算法

第 56 段(可获 1.01 积分)
  1. MD5
  2. SHA256
  3. SHA384
  4. SHA512

示例:-

生成 MD5 哈希的方法

private string Generate_MD5_Hash(string data_To_Encrypted)
{
    using (MD5 encryptor = MD5.Create())
    {
        MD5 md5 = System.Security.Cryptography.MD5.Create();
        byte[] inputBytes = System.Text.Encoding.ASCII.GetBytes(data_To_Encrypted);
        byte[] hash = md5.ComputeHash(inputBytes);

        StringBuilder sb = new StringBuilder();

        for (int i = 0; i < hash.Length; i++)
        {
            sb.Append(hash[i].ToString());
        }

        return sb.ToString();
    }
}
输入文本来生成哈希值

string Hash = Generate_MD5_Hash("Hello");

输出

对称算法

  1. Aes
  2. DES
  3. RC2
  4. Rijndael
  5. TripleDES
第 57 段(可获 0.23 积分)

示例:-

使用 AES 加密的方法

private string Encrypt_AES(string clearText)
{
    string EncryptionKey = "##SAI##1990##";
    byte[] clearBytes = Encoding.Unicode.GetBytes(clearText);
    byte[] array = Encoding.ASCII.GetBytes("##100SAINESHWAR99##");

    using (Aes encryptor = Aes.Create())
    {
        Rfc2898DeriveBytes pdb = new Rfc2898DeriveBytes(EncryptionKey, array);
        encryptor.Key = pdb.GetBytes(32);
        encryptor.IV = pdb.GetBytes(16);
        using (MemoryStream ms = new MemoryStream())
        {
            using (CryptoStream cs = new CryptoStream(ms, encryptor.CreateEncryptor(), CryptoStreamMode.Write))
            {
                cs.Write(clearBytes, 0, clearBytes.Length);
                cs.Close();
            }
            clearText = Convert.ToBase64String(ms.ToArray());
        }
    }
    return clearText;
}

 

第 58 段(可获 0.09 积分)

使用 AES 解密的方法

private string Decrypt_AES(string cipherText)
{
    string EncryptionKey = "##SAI##1990##";
    byte[] cipherBytes = Convert.FromBase64String(cipherText);
    byte[] array = Encoding.ASCII.GetBytes("##100SAINESHWAR99##");

    using (Aes encryptor = Aes.Create())
    {
        Rfc2898DeriveBytes pdb = new Rfc2898DeriveBytes(EncryptionKey, array);
        encryptor.Key = pdb.GetBytes(32);
        encryptor.IV = pdb.GetBytes(16);
        using (MemoryStream ms = new MemoryStream())
        {
            using (CryptoStream cs = new CryptoStream(ms, encryptor.CreateDecryptor(), CryptoStreamMode.Write))
            {
                cs.Write(cipherBytes, 0, cipherBytes.Length);
                cs.Close();
            }
            cipherText = Encoding.Unicode.GetString(ms.ToArray());
        }
    }
    return cipherText;
}

 

第 59 段(可获 0.06 积分)

加密文本

string DataEncrypt = Encrypt_AES("Hello");   // Encrypting Data (Pass text to Encrypt)

解密文本

string DataDecrypt = Decrypt_AES(DataEncrypt);  // Decrypt data (Pass Encrypt text to Decrypt)

输出

非对称算法

  1. DSA
  2. ECDiffieHellman
  3. ECDsa
  4. RSA

示例:-

使用 RSA 加密的方法

public byte[] Encrypt(string publicKeyXML, string dataToDycript)
{
    RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
    rsa.FromXmlString(publicKeyXML);
    return rsa.Encrypt(ASCIIEncoding.ASCII.GetBytes(dataToDycript), true);
}

 

第 60 段(可获 0.29 积分)

使用 RSA 解密的方法

public string Decrypt(string publicPrivateKeyXML, byte[] encryptedData)
{
    RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
    rsa.FromXmlString(publicPrivateKeyXML);
    return ASCIIEncoding.ASCII.GetString(rsa.Decrypt(encryptedData, true));
}

输出

8) 跟踪审查

在 IT 世界,跟踪审查用于在用户使用的 Web 应用中追踪其活动,这对于检查安全问题、性能问题和应用系统错误来说非常重要。这能帮助我们了解问题所在并解决问题。

第 61 段(可获 0.68 积分)

图 96,审查

解决办法:-

  1. 在 Web 应用中对所有用户活动进行跟踪审查,并随时监控审查信息

在 Web 应用中对所有用户活动进行跟踪审查,并随时监控审查信息

为了维护跟踪审查,首先我们要在数据库中创建一张表来保存审查数据,表名叫作 [AuditTB]。然后我们得创建一个名为 [UserAuditFilter] 的 ActionFilterAttribute,并写代码在其执行 Action 时往数据库插入数据,这些数据能表明当前是谁在访问应用程序。

AuditTB 表视图

在这个表中,我们加入可以识别用户及其行为的一些常见的列。

第 62 段(可获 1.35 积分)

Fig 97.AuditTB 表视图

看过表视图之后,我们来关注根据表创建的模型。

AuditTB 模型

public partial class AuditTB
{
    public int UsersAuditID { get; set; }
    public int UserID { get; set; }
    public string SessionID { get; set; }
    public string IPAddress { get; set; }
    public string PageAccessed { get; set; }
    public Nullable<System.DateTime> LoggedInAt { get; set; }
    public Nullable<System.DateTime> LoggedOutAt { get; set; }
    public string LoginStatus { get; set; }
    public string ControllerName { get; set; }
    public string ActionName { get; set; }
}

 

第 63 段(可获 0.3 积分)

现在来创建名为 UserAuditFilter 的 ActionFilter。

名称 UserAuditFilter 的 Actionfilter 代码

UserAuditFilter 是一个自定义的 ActionFilter,我们创建它来将用户活动相关的数据插入 AuditTB 表中,顺便我们会检查用户是否登入应用程序,我们同样也可以将用户访问应用程序的 IP 地址和时间戳插入数据表,当作记录。我们使用 Entity Framework 来插入数据。

public class UserAuditFilter : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        AllSampleCodeEntitiesappcontext = newAllSampleCodeEntities();
        AuditTBobjaudit = newAuditTB();
        //Getting Action Name
        string actionName = filterContext.ActionDescriptor.ActionName;
        //Getting Controller Name
        string controllerName = filterContext.ActionDescriptor.ControllerDescriptor.ControllerName;
        var request = filterContext.HttpContext.Request;
        if (HttpContext.Current.Session["UserID"] == null) // For Checking User is Logged in or Not 
        {
            objaudit.UserID = 0;
        }
        else
        {
            objaudit.UserID = Convert.ToInt32(HttpContext.Current.Session["UserID"]);
        }
        objaudit.UsersAuditID = 0;
        objaudit.SessionID = HttpContext.Current.Session.SessionID; // Application SessionID
        // User IPAddress
        objaudit.IPAddress =
        request.ServerVariables["HTTP_X_FORWARDED_FOR"] ?? request.UserHostAddress;
        objaudit.PageAccessed = request.RawUrl;  // URL User Requested
        objaudit.LoggedInAt = DateTime.Now;      // Time User Logged In || And time User Request Method
        if (actionName == "LogOff")
        {
            objaudit.LoggedOutAt = DateTime.Now; // Time User Logged OUT
        }        
        objaudit.LoginStatus = "A";
        objaudit.ControllerName = controllerName; // ControllerName
        objaudit.ActionName = actionName;         // ActionName
        appcontext.AuditTBs.Add(objaudit);
        appcontext.SaveChanges();                    // Saving in database using Entity Framework
        base.OnActionExecuting(filterContext);
    }
}

 

第 64 段(可获 1.06 积分)

将 UserAuditFilter 注册为全局 Action 过滤器

全局 Action 过滤器主要用于处理和记录错误。

如果你希望在项目中所有 Action 方法上应用某个 Action 过滤,你可以将其注册为全局 Action 过滤器。在这里,因为我们想记录所有用户请求,进行跟踪审查,所以我们需要全局 Action 过滤器。

图 98,将  UserAuditFilter 添加到全局过滤器集合中

输出

用户请求页面和进行某些活动的时候相关数据会被被插入到审查表中

图 99,插入数据之后的审查表视图

第 65 段(可获 1.24 积分)

9) 打破谁和会话管理

如果认证和会话管理并没有恰当的在同一个 Web 应用中实现,那攻击者就有可能偷取密码、会话令牌、Cookie 和类似能让攻击者不需要用户凭据访问整个应用的东西。

攻击者偷取数据的途径有

  1. 非安全连接(没使用SSL)
  2. 可预见的登录凭证
  3. 不以加密形式存储凭证的表单
  4. 未正确注销

可能的攻击

  1. 固化会话

在找到应对这种攻击的解决办法之前,让我们先来看一个小小的固化会话的攻击演示。

第 66 段(可获 1.13 积分)

用户第一次发送请求到服务器时,加载了登录页面,之后用户输入有效的登录凭证来登录 Web 应用。如果登录成功,我们会在会话里保存一些值来对用户进行唯一识别,也就是说,[“ASP.NET_SessionId”]  这个 Cookie 被加入浏览器用于识别当前用户,之后所有向服务器发送的请求都会包含 [“ASP.NET_SessionId”] Cookie 值直到注销。而在注销的时候,我们基本上会写一段代码来删除创建于会话中的数据,但我们不会删除登录时创建的 [“ASP.NET_SessionId”] Cookie 值。这个数据会帮助攻击者进行固化会话攻击。

 

第 67 段(可获 1.4 积分)

图 100,固化会话

固化会话的演示

我们访问登录页面时,浏览器中没有 [“ASP.NET_SessionId”] cookie,我们可以在 Cookie 管理器中查看。

用户输入有效的凭证之后

输入有效凭证之后 [“ASP.NET_SessionId”] Cookie 被添加到浏览器。

注意:- 任何数据保存到 Session 对象都会导致在浏览器端创建 [“ASP.NET_SessionId”] cookie 。

从应用程序注销之后 Cookie 仍然存在于浏览器中

从应用程序注销之后仍然存在 [“ASP.NET_SessionId”] Cookie。

注意:之前的 cookie 和之后的 cookie 是相同的,所以会造成固化会话

第 68 段(可获 1.38 积分)

我们来固化会话

[“ASP.NET_SessionId”] Cookie 在注销之后仍未删除,这会有助于攻击者进行固化会话攻击。打开一个浏览器(Chrome),输入应用的 URL [http://localhost:3837/] ,然后我们来进行固化会话的操作。

在浏览器中输入 URL 之后,现在来检查是否存在 [“ASP.NET_SessionId”] Cookie —— 目前还没有任何 Cookie。

Firefox 浏览器中已经创建了 Cookie

图中展示用户登录后 Firefox 浏览器中的 [“ASP.NET_SessionId”] Cookie。

第 69 段(可获 1.28 积分)

注:- 为了管理Cookie,我已经在Chrome 浏览器上安装了Cookie Manager+插件。

在看了firefox浏览器里的[“ASP.NET_SessionId”] cookie之后, 现在让我们在Chrome浏览器里也撞见 [“ASP.NET_SessionId”] cookie,就跟firefox浏览器一样的名字和值。

在Chrome浏览器里创建跟Firefox浏览器里类似的[“ASP.NET_SessionId”] Cookie。

这一步里,我有了固定的会话,这个会话在另外一个浏览器(firefox)里也存在。我们把类似的值复制过来,然后创建[“ASP.NET_SessionId”] Cookie,并把SessionID的值赋给这个Cookie。

第 70 段(可获 1.39 积分)

注意:- 我在 Chrome 浏览器中安装了编辑 Cookie 的插件来添加 Cookie

加入正确的 Cookie 后,我们输入应用内部 URL 后不再需要进行登录验证,可以直接访问应用,因为对应的会话 (Session) 已经在之前认证时建立了。

解决办法:-

  1. 注销后删除 [“ASP.NET_SessionId”]
  2. 保护(加密) Cookie
  3. 使用 SSL 来保护 Cookie 和 Session(会话)

注销后删除 [“ASP.NET_SessionId”]

注销的时候删除 Session 值同时从浏览器删除 [“ASP.NET_SessionId”] Cookie。

//
// POST: /Account/LogOff
publicActionResultLogOff()
{
//Removing Session
    Session.Abandon();
    Session.Clear();
    Session.RemoveAll();

    //Removing ASP.NET_SessionId Cookie
    if (Request.Cookies["ASP.NET_SessionId"] != null)
    {
        Response.Cookies["ASP.NET_SessionId"].Value = string.Empty;
        Response.Cookies["ASP.NET_SessionId"].Expires = DateTime.Now.AddMonths(-10);
    }

    if (Request.Cookies["AuthenticationToken"] != null)
    {
        Response.Cookies["AuthenticationToken"].Value = string.Empty;
        Response.Cookies["AuthenticationToken"].Expires = DateTime.Now.AddMonths(-10);
    }

    returnRedirectToAction("Login", "Account");
}

 

第 71 段(可获 1.14 积分)

保护(加密) Cookie

为了保护 Cookie,在 Login 这个 [HttpPost] Action 方法中我们会创建一个新的会话(Session),[Session["AuthenticationToken"]],这里我们会保存一个新的 Guid 并加入 Cookie,其键命名为  ["AuthenticationToken"]。同样的 [Guid] 值也会保存在 Session 中。

代码段

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
publicActionResult Login(LoginModel model, stringreturnUrl)
{
    if (ModelState.IsValid)
    {   //Getting Pasword from Database
        varstoredpassword = ReturnPassword(model.UserName);
        // Comparing Password With Seed
        if (ReturnHash(storedpassword, model.hdrandomSeed) == model.Password)
        {
            Session["Username"] = model.UserName;
            Session["UserID"] = 1;

            // Getting New Guid
            stringguid = Convert.ToString(Guid.NewGuid());
            //Storing new Guid in Session
            Session["AuthenticationToken"] = guid;
            //Adding Cookie in Browser
            Response.Cookies.Add(newHttpCookie("AuthenticationToken", guid));

            returnRedirectToAction("Index", "Dashboard");
        }
        else
        {
            ModelState.AddModelError("", "The user name or password provided is incorrect.");
        }
    }
    return View(model);
}

 

第 72 段(可获 0.7 积分)

代码说明

创建新的 Guid。

// Getting New Guid
stringguid = Convert.ToString(Guid.NewGuid());

将这个新的 Guid 保存在 Session 中。

 
//Storing new Guid in Session
Session["AuthenticationToken"] = guid;

把这个 Guid 加到 Cookie 中。

 
//Adding Cookie in Browser
Response.Cookies.Add(newHttpCookie("AuthenticationToken", guid));

在把数据保存到 Session 和 浏览器的 Cookie 中之后,我们会到每个请求中去匹配这个值,如果检查到这个值不匹配直接跳到登录页面。

为此我在项目中添加了一个 AuthroizationFilter,我会在里面写检查 Session 和 Cookie 是否匹配的逻辑。

 

第 73 段(可获 1.11 积分)

AuthenticateUser ActionFilter

请观察下面的代码片段,我创建了一个AuthorizationFilter,命名为AuthenticateUser,在这个过滤器中,我们继承实现了IAuthorizationFilter 接口和FilterAttribute 类, 在接口方法[OnAuthorization]内进行实现,并且在这个方法中编写整个逻辑。

using System;
using System.Web.Mvc;

namespace MvcSecurity.Filters
{
    public class AuthenticateUser: FilterAttribute, IAuthorizationFilter
    {
        public void OnAuthorization(AuthorizationContextfilterContext)
        {
            string TempSession = Convert.ToString(filterContext.HttpContext
                .Session["AuthenticationToken"]);
            string TempAuthCookie = Convert.ToString(filterContext.HttpContext.Request
                .Cookies["AuthenticationToken"].Value);

            if (TempSession != null && TempAuthCookie != null)
            {
                if (!TempSession.Equals(TempAuthCookie))
                {
                    ViewResult result = newViewResult();
                    result.ViewName = "Login";
                    filterContext.Result = result;
                }
            }
            else
            {
                ViewResult result = newViewResult();
                result.ViewName = "Login";
                filterContext.Result = result;
            }
        }
    }
}

 

第 74 段(可获 0.51 积分)

代码解释

在这个方法中我们先获取 Session 和 Cookie 中的值。

stringTempSession = Convert.ToString(filterContext.HttpContext.Session["AuthenticationToken"]);
stringTempAuthCookie = Convert.ToString(filterContext.HttpContext.Request.Cookies["AuthenticationToken"].Value);

从 Session 和 Cookie 获取值后,我们检查两个值是否存在(不为 null),然后再检查它们是否相等。如果它们不等,就重定向到登录页面。

 

第 75 段(可获 0.79 积分)
if (TempSession != null && TempAuthCookie != null)
{
    if (!TempSession.Equals(TempAuthCookie))
    {
        ViewResult result = newViewResult();
        result.ViewName = "Login";
        filterContext.Result = result;
    } 
}
else
{
    ViewResult result = newViewResult();
    result.ViewName = "Login";
    filterContext.Result = result;
}

搞明白上面的代码之后,现在我们接下来将这个过滤器应用到用户登录之后需要访问的每个控制器上。

应用 AuthenticateUser 过滤器

将这个过滤器应用到用户登录之后需要访问的每个控制器上。

第 76 段(可获 0.51 积分)

将这个过滤器应用到用户登录之后需要访问的每个控制器上。

现在如果攻击者知道 [“ASP.NET_SessionId”] Cookie 值,以及新的 [Cookies["AuthenticationToken"]] Cookie 值,仍然不能进行固化会话攻击,因为 [Cookies["AuthenticationToken"]] 包含的 GUID 唯一而且与 [Session["AuthenticationToken"]] 中保存的值一样,并且用户每次登录应用都会改变。攻击者在脚本中使用以前的 Session 值将会无效。

第 77 段(可获 1.33 积分)

最后,如果我们只会允许那些拥有正确 Session["AuthenticationToken"] 值和 Cookies["AuthenticationToken"] 值的用户访问应用。

两个 Cookie 的实时值。

使用  SSL 来保护 Cookie 和 Session 值

SSL (Secure Sockets Layer,安全套接字层) 是客户端和服务端进行通讯的安全(加密)层,它(通过加密)保护任何在客户端和服务器端传递的数据 [银行账号信息、密码、Session、Cookie 和其它金融交易数据]。

图 111.SSL (Secure Sockets Layer,安全套接字层)。

10) 未经验证的重定向和转发

在所有 Web 应用中,我们都会使用从一个页面到另一个页面的重定向,有时候甚至会重定向到另一个应用。但重定向的时候我们不会验证目标 URL,这会导致未经验证的重定向和转发攻击。

第 78 段(可获 1.48 积分)

这类攻击主要是通过钓鱼的手段来获取有价值的用户数据(用户凭证)或在用户的计算机上安装恶意软件。

示例

下图上,你会看到一个简单 MVC 应用的 URL 被攻击者的创建的恶意 URL 重定向到某个恶意网站,这个网站通过钓鱼的手段向用户计算机安装恶意软件。

图 112,攻击者的恶意 URL

原 URL :- http://localhost:7426/Account/Login

攻击者的恶意 URL :-?returnUrl=https://www.google.co.in

攻击场景

在这类攻击中,用户会收到攻击者的邮件,其内容包含一个电子商务购物的链接,用户点击这个链接后会被重定向到购物网站 [http://demotop.com],但如果仔细观察 URL 就会发现这个 URL 包含这样的重定向地址  [http://demotop.com/Login/Login?url=http://mailicious.com],用户如果输入了正确的用户名和密码则会转到 [http://mailicious.com],这是一个与  [http://demotop.com] 购物网站极为相似的网站。恶意网站上会显示“无效的用户名和密码”这样的消息,用户会再次输入用户名和密码后会转回原来的购买网站,但这时候攻击者已经盗取了用户凭证信息。

第 79 段(可获 2.51 积分)

图 113,未经验证的重定向和转发场景

解决办法

  1. 最简单的办法是不使用重定向和转发。
  2. 如果仍然想使用重定向和转发,一定要验证 URL。
  3. 在 MVC 中使用 Url.IsLocalUrl 来预防重定向和转发攻击。

在 MVC 中使用 Url.IsLocalUrl

下面是一个 MVC 登录页面的图示,它包含重定向到 www.google.com 的 URL。用户输入用户名和密码之后会重定向到 www.google.com,这在 MVC4 中不能预防。我们有一个内建的方法叫 Url.IsLocalUrl,它会检查重定向的 URL 是否本地 URL,如果不是,将不会进行重定向。

第 80 段(可获 1.33 积分)

图 114,MVC 中含有重定义地址的登录页面。

理解了重定向是如何进行的之后,先不管这个 URL 是如何检查和执行的。

下面的 [HttpPost] Login 方法会在用户输入凭证并提交表单时被调用,可能包含恶意 URL 的重定向地址也随之被提交。为了演示我只检查了用户名和密码不为 null,然后就调用 RedirectToLocalAcion 方法。这个方法中我们重定向到指定的 URL (returnUrl)。

图 115. 带有 returnURL 的 Login Action 方法

第 81 段(可获 1.18 积分)

在将URL (returnUrl)传给RedirectToLocal Action 的Method方法之后,接着它将会将URL再传给IsLocalUrl方法[Url.IsLocalUrl(returnUrl)],这个方法将会检查该URL是否是本地的,并返回一个布尔类型的值,如果是本地地址,那么它将会重定向至首页,否则的话则会重定向至其传递的returnUrl 地址上。

[True if the URL is local]
[False if URL is not local]

图表116.RedirectToLocal Action 用来方法检查returnUrl是否是本地地址

第 82 段(可获 0.79 积分)

文章评论

班纳睿
这篇文章有点老
班纳睿
而且标点符号,空格什么的缺失很严重,翻译起来很不爽,能否编辑下原文?@CY2
边城
所以我翻译起来很痛苦……不要说编辑可译英文原文,就是 CodeProject 上的原文都是各种不对,可译上只是对原文比较真实的反应……
班纳睿
居然翻译完了, :thumbsup:
CY2
牛逼!