抽丝剥茧:浅议ASP.NET Cookie的生成原理

简介:   前言  可能有人知道 Cookie的生成由 machineKey有关, machineKey用于决定 Cookie生成的算法和密钥,并如果使用多台服务器做负载均衡时,必须指定一致的 machineKey用于解密,那么这个过程到底是怎样的呢?  如果需要在 .NETCore中使用 ASP.NETCookie,本文将提到的内容也将是一些必经之路。  抽丝剥茧,一步一步分析  首先用户通过 AccountController->Login进行登录:  //  // POST: /Account/Login  public async Task Login(LoginV

  前言

  可能有人知道 Cookie的生成由 machineKey有关, machineKey用于决定 Cookie生成的算法和密钥,并如果使用多台服务器做负载均衡时,必须指定一致的 machineKey用于解密,那么这个过程到底是怎样的呢?

  如果需要在 .NETCore中使用 ASP.NETCookie,本文将提到的内容也将是一些必经之路。

  抽丝剥茧,一步一步分析

  首先用户通过 AccountController->Login进行登录:

  //

  // POST: /Account/Login

  public async Task Login(LoginViewModel model, string returnUrl)

  {

  if (!ModelState.IsValid)

  {

  return View(model);

  }

  var result=await SignInManager.PasswordSignInAsync(model, model.Password, model.RememberMe, shouldLockout: false);

  switch (result)

  {

  case SignInStatus.Success:

  return RedirectToLocal(returnUrl);

  // ......省略其它代码

  }

  }

  它调用了 SignInManager的 PasswordSignInAsync方法,该方法代码如下(有删减):

  public virtual async Task PasswordSignInAsync(string userName, string password, bool isPersistent, bool shouldLockout)

  {

  // ...省略其它代码

  if (await UserManager.CheckPasswordAsync(user, password).WithCurrentCulture())

  {

  if (!await IsTwoFactorEnabled(user))

  {

  await UserManager.ResetAccessFailedCountAsync(user.Id).WithCurrentCulture();

  }

  return await SignInOrTwoFactor(user, isPersistent).WithCurrentCulture();

  }

  // ...省略其它代码

  return SignInStatus.Failure;

  }

  想浏览原始代码,可参见官方的 Github链接:

  github/aspnet/AspNetIdentity/blob/master/src/Microsoft.AspNet.Identity.Owin/SignInManager.cs#L235-L276

  可见它先需要验证密码,密码验证正确后,它调用了 SignInOrTwoFactor方法,该方法代码如下:

  private async Task SignInOrTwoFactor(TUser user, bool isPersistent)

  {

  var id=Convert.ToString(user.Id);

  if (await IsTwoFactorEnabled(user) && !await AuthenticationManager.TwoFactorBrowserRememberedAsync(id).WithCurrentCulture())

  {

  var identity=new ClaimsIdentity(DefaultAuthenticationTypes.TwoFactorCookie);

  identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, id));

  AuthenticationManager.SignIn(identity);

  return SignInStatus.RequiresVerification;

  }

  await SignInAsync(user, isPersistent, false).WithCurrentCulture();

  return SignInStatus.Success;

  }

  该代码只是判断了是否需要做双重验证,在需要双重验证的情况下,它调用了 AuthenticationManager的 SignIn方法;否则调用 SignInAsync方法。 SignInAsync的源代码如下:

  public virtual async Task SignInAsync(TUser user, bool isPersistent, bool rememberBrowser)

  {

  var userIdentity=await CreateUserIdentityAsync(user).WithCurrentCulture();

  // Clear any partial cookies from external or two factor partial sign ins

  AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie, DefaultAuthenticationTypes.TwoFactorCookie);

  if (rememberBrowser)

  {

  var rememberBrowserIdentity=AuthenticationManager.CreateTwoFactorRememberBrowserIdentity(ConvertIdToString(user.Id));

  AuthenticationManager.SignIn(new AuthenticationProperties { IsPersistent=isPersistent }, userIdentity, rememberBrowserIdentity);

  }

  else

  {

  AuthenticationManager.SignIn(new AuthenticationProperties { IsPersistent=isPersistent }, userIdentity);

  }

  }

  可见,最终所有的代码都是调用了

  AuthenticationManager.SignIn方法,所以该方法是创建 Cookie的关键。

  AuthenticationManager的实现定义在 Microsoft.Owin中,因此无法在 ASP.NETIdentity中找到其源代码,因此我们打开 Microsoft.Owin的源代码继续跟踪(有删减):

  public void SignIn(AuthenticationProperties properties, params ClaimsIdentity[] identities)

  {

  AuthenticationResponseRevoke priorRevoke=AuthenticationResponseRevoke;

  if (priorRevoke !=null)

  {

  // ...省略不相关代码

  AuthenticationResponseRevoke=new AuthenticationResponseRevoke(filteredSignOuts);

  }

  AuthenticationResponseGrant priorGrant=AuthenticationResponseGrant;

  if (priorGrant==null)

  {

  AuthenticationResponseGrant=new AuthenticationResponseGrant(new ClaimsPrincipal(identities), properties);

  }

  else

  {

  // ...省略不相关代码

  AuthenticationResponseGrant=new AuthenticationResponseGrant(new ClaimsPrincipal(mergedIdentities), priorGrantperties);

  }

  }

  AuthenticationManager的 Github链接如下:

  github/aspnet/AspNetKatana/blob/c33569969e79afd9fb4ec2d6bdff877e376821b2/src/Microsoft.Owin/Security/AuthenticationManager.cs

  可见它用到了

  AuthenticationResponseGrant,继续跟踪可以看到它实际是一个属性:

  public AuthenticationResponseGrant AuthenticationResponseGrant

  {

  // 省略get

  set

  {

  if (value==null)

  {

  SignInEntry=null;

  }

  else

  {

  SignInEntry=Tuple.Create((IPrincipal)value.Principal, valueperties.Dictionary);

  }

  }

  }

  发现它其实是设置了 SignInEntry,继续追踪:

  public Tuple> SignInEntry

  {

  get { return _context.Get>>(OwinConstants.Security.SignIn); }

  set { _context.Set(OwinConstants.Security.SignIn, value); }

  }

  其中, _context的类型为 IOwinContext,

  OwinConstants.Security.SignIn的常量值为 "security.SignIn"。

  跟踪完毕……

  啥?跟踪这么久,居然跟丢啦!?

  当然没有!但接下来就需要一定的技巧了。

  原来, ASP.NET是一种中间件( Middleware)模型,在这个例子中,它会先处理 MVC中间件,该中间件处理流程到设置

  AuthenticationResponseGrant/ SignInEntry为止。但接下来会继续执行 CookieAuthentication中间件,该中间件的核心代码在 aspnet/AspNetKatana仓库中可以看到,关键类是

  CookieAuthenticationHandler,核心代码如下:

  protected override async Task ApplyResponseGrantAsync()

  {

  AuthenticationResponseGrant signin=Helper.LookupSignIn(Options.AuthenticationType);

  // ... 省略部分代码

  if (shouldSignin)

  {

  var signInContext=new CookieResponseSignInContext(

  Context,

  Options,

  Options.AuthenticationType,

  signin.Identity,

  signinperties,

  cookieOptions);

  // ... 省略部分代码

  model=new AuthenticationTicket(signInContext.Identity, signInContextperties);

  // ... 省略部分代码

  string cookieValue=Options.TicketDataFormattect(model);

  Options.CookieManager.AppendResponseCookie(

  Context,

  Options.CookieName,

  cookieValue,

  signInContext.CookieOptions);

  }

  // ... 又省略部分代码

  }

  这个原始函数有超过 200行代码,这里我省略了较多,但保留了关键、核心部分,想查阅原始代码可以移步 Github链接:

  github/aspnet/AspNetKatana/blob/0fc4611e8b04b73f4e6bd68263e3f90e1adfa447/src/Microsoft.Owin.Security.Cookies/CookieAuthenticationHandler.cs#L130-L313

  这里挑几点最重要的讲。

  与 MVC建立关系

  建立关系的核心代码就是第一行,它从上文中提到的统招证书位置取回了

  AuthenticationResponseGrant,该 Grant保存了 Claims、 AuthenticationTicket等 Cookie重要组成部分:

  AuthenticationResponseGrant signin=Helper.LookupSignIn(Options.AuthenticationType);

  继续查阅 LookupSignIn源代码,可看到,它就是从上文中的 AuthenticationManager中取回了

  AuthenticationResponseGrant(有删减):

  public AuthenticationResponseGrant LookupSignIn(string authenticationType)

  {

  // ...

  AuthenticationResponseGrant grant=_context.Authentication.AuthenticationResponseGrant;

  // ...

  foreach (var claimsIdentity in grant.Principal.Identities)

  {

  if (string.Equals(authenticationType, claimsIdentity.AuthenticationType, StringComparison.Ordinal))

  {

  return new AuthenticationResponseGrant(claimsIdentity, grantperties new AuthenticationProperties());

  }

  }

  return null;

  }

  如此一来,柳暗花明又一村,所有的线索就立即又明朗了。

  Cookie的生成

  从 AuthenticationTicket变成 Cookie字节串,最关键的一步在这里:

  string cookieValue=Options.TicketDataFormattect(model);

  在接下来的代码中,只提到使用 CookieManager将该 Cookie字节串添加到 Http响应中,翻阅 CookieManager可以看到如下代码:

  public void AppendResponseCookie(IOwinContext context, string key, string value, CookieOptions options)

  {

  if (context==null)

  {

  throw new ArgumentNullException("context");

  }

  if (options==null)

  {

  throw new ArgumentNullException("options");

  }

  IHeaderDictionary responseHeaders=context.Response.Headers;

  // 省去“1万”行计算chunk和处理细节的流程

  responseHeaders.AppendValues(Constants.Headers.SetCookie, chunks);

  }

  有兴趣的朋友可以访问 Github看原始版本的代码:

  github/aspnet/AspNetKatana/blob/0fc4611e8b04b73f4e6bd68263e3f90e1adfa447/src/Microsoft.Owin/Infrastructure/ChunkingCookieManager.cs#L125-L215

  可见这个实现比较……简单,就是往 Response.Headers中加了个头,重点只要看 TicketDataFormattect方法即可。

  逐渐明朗

  该方法源代码如下:

  public string Protect(TData data)

  {

  byte[] userData=_serializer.Serialize(data);

  byte[] protectedData=_protectortect(userData);

  string protectedText=_encoder.Encode(protectedData);

  return protectedText;

  }

  可见它依赖于 _serializer、 _protector、 _encoder三个类,其中, _serializer的关键代码如下:

  public virtual byte[] Serialize(AuthenticationTicket model)

  {

  using (var memory=new MemoryStream())

  {

  using (var compression=new GZipStream(memory, CompressionLevel.Optimal))

  {

  using (var writer=new BinaryWriter(compression))

  {

  Write(writer, model);

  }

  }

  return memory.ToArray();

  }

  }

  其本质是进行了一次二进制序列化,并紧接着进行了 gzip压缩,确保 Cookie大小不要失去控制(因为 .NET的二进制序列化结果较大,并且微软喜欢搞 xml,更大)。

  然后来看一下 _encoder源代码:

  public string Encode(byte[] data)

  {

  if (data==null)

  {

  throw new ArgumentNullException("data");

  }

  return Convert.ToBase64String(data).TrimEnd('=').Replace('+', '-').Replace('/', '_');

  }

  可见就是进行了一次简单的 base64-url编码,注意该编码把 =号删掉了,所以在 base64-url解码时,需要补 =号。

  这两个都比较简单,稍复杂的是 _protector,它的类型是 IDataProtector。

  IDataProtector

  它在

  CookieAuthenticationMiddleware中进行了初始化,创建代码和参数如下:

  IDataProtector dataProtector=app.CreateDataProtector(

  typeof(CookieAuthenticationMiddleware).FullName,

  Options.AuthenticationType, "v1");

  注意它传了三个参数,第一个参数是

  CookieAuthenticationMiddleware的 FullName,也就是 "

  Microsoft.Owin.Security.Cookies.CookieAuthenticationMiddleware",第二个参数如果没定义,默认值是

  CookieAuthenticationDefaults.AuthenticationType,该值为定义为 "Cookies"。

  但是,在默认创建的 ASP.NET MVC模板项目中,该值被重新定义为 ASP.NETIdentity的默认值,即 "ApplicationCookie",需要注意。

  然后来看看 CreateDataProtector的源码:

  public static IDataProtector CreateDataProtector(this IAppBuilder app, params string[] purposes)

  {

  if (app==null)

  {

  throw new ArgumentNullException("app");

  }

  IDataProtectionProvider dataProtectionProvider=GetDataProtectionProvider(app);

  if (dataProtectionProvider==null)

  {

  dataProtectionProvider=FallbackDataProtectionProvider(app);

  }

  return dataProtectionProvider.Create(purposes);

  }

  public static IDataProtectionProvider GetDataProtectionProvider(this IAppBuilder app)

  {

  if (app==null)

  {

  throw new ArgumentNullException("app");

  }

  object value;

  if (appperties.TryGetValue("security.DataProtectionProvider", out value))

  {

  var del=value as DataProtectionProviderDelegate;

  if (del !=null)

  {

  return new CallDataProtectionProvider(del);

  }

  }

  return null;

  }

  可见它先从 IAppBuilder的 "

  security.DataProtectionProvider"属性中取一个 IDataProtectionProvider,否则使用

  DpapiDataProtectionProvider。

  我们翻阅代码,在 OwinAppContext中可以看到,该值被指定为

  MachineKeyDataProtectionProvider:

  builderperties[Constants.SecurityDataProtectionProvider]=new MachineKeyDataProtectionProvider().ToOwinFunction();

  文中的

  Constants.SecurityDataProtectionProvider,刚好就被定义为 "

  security.DataProtectionProvider"。

  我们翻阅 MachineKeyDataProtector的源代码,刚好看到它依赖于 MachineKey:

  internal class MachineKeyDataProtector

  {

  private readonly string[] _purposes;

  public MachineKeyDataProtector(params string[] purposes)

  {

  _purposes=purposes;

  }

  public virtual byte[] Protect(byte[] userData)

  {

  return MachineKeytect(userData, _purposes);

  }

  public virtual byte[] Unprotect(byte[] protectedData)

  {

  return MachineKey.Unprotect(protectedData, _purposes);

  }

  }

  最终到了我们的老朋友 MachineKey。

  逆推过程,破解Cookie

  首先总结一下这个过程,对一个请求在 Mvc中的流程来说,这些代码集中在 ASP.NETIdentity中,它会经过:

  AccountControllerSignInManagerAuthenticationManager设置 AuthenticationResponseGrant

  然后进入 CookieAuthentication的流程,这些代码集中在 Owin中,它会经过:

  CookieAuthenticationMiddleware(读取 AuthenticationResponseGrant)ISecureDataFormat(实现类: SecureDataFormat)IDataSerializer(实现类: TicketSerializer)IDataProtector(实现类: MachineKeyDataProtector)ITextEncoder(实现类: Base64UrlTextEncoder)

  这些过程,结果上文中找到的所有参数的值,我总结出的“祖传破解代码”如下:

  string cookie="nZBqV1M-Az7yJezhb6dUzS_urj1urB0GDufSvDJSa0pv27CnDsLHRzMDdpU039j6ApL-VNfrJULfE85yU9RFzGV_aAGXHVkGckYqkCRJUKWV8SqPEjNJ5ciVzW--uxsCBNlG9jOhJI1FJIByRzYJvidjTYABWFQnSSd7XpQRjY4lb082nDZ5lwJVK3gaC_zt6H5Z1k0lUFZRb6afF52laMc___7BdZ0mZSA2kRxTk1QY8h2gQh07HqlR_p0uwTFNKi0vW9NxkplbB8zfKbfzDj7usep3zAeDEnwofyJERtboXgV9gIS21fLjc58O-4rR362IcCi2pYjaKHwZoO4LKWe1bS4r1tyzW0Ms-39Njtiyp7lRTN4HUHMUi9PxacRNgVzkfK3msTA6LkCJA3VwRm_UUeC448Lx5pkcCPCB3lGat_5ttGRjKD_lllI-YE4esXHB5eJilJDIZlEcHLv9jYhTl17H0Jl_H3FqXyPQJR-ylQfh";

  var bytes=TextEncodings.Base64Url.Decode(cookie);

  var decrypted=MachineKey.Unprotect(bytes,

  "Microsoft.Owin.Security.Cookies.CookieAuthenticationMiddleware",

  "ApplicationCookie",

  "v1");

  var serializer=new TicketSerializer();

  var ticket=serializer.Deserialize(decrypted);

  ticket.Dump(); // Dump为LINQPad专有函数,用于方便调试显示,此处可以用循环输出代替

  运行前请设置好 app.config/ web.config中的 machineKey节点,并安装 NuGet包: Microsoft.Owin.Security,运行结果如下(完美破解):

  抽丝剥茧:浅议ASP.NET Cookie的生成原理

  总结

  学习方式有很多种,其中看代码是我个人非常喜欢的一种方式,并非所有代码都会一马平川。像这个例子可能还需要有一定 ASP.NET知识背景。

  注意这个“祖传代码”是基于 .NETFramework,由于其用到了 MachineKey,因此无法在 .NETCore中运行。我稍后将继续深入聊聊 MachineKey这个类,看它底层代码是如何工作的,然后最终得以在 .NETCore中直接破解 ASP.NETIdentity中的 Cookie,敬请期待!

目录
相关文章
|
3月前
|
机器学习/深度学习 算法 网络架构
【CVPR2017】AOD-Net:端到端的除雾网络(原理&实操)
【CVPR2017】AOD-Net:端到端的除雾网络(原理&实操)
1112 0
【CVPR2017】AOD-Net:端到端的除雾网络(原理&实操)
|
3月前
|
存储 开发框架 NoSQL
ASP.NET WEB——项目中Cookie与Session的用法
ASP.NET WEB——项目中Cookie与Session的用法
70 0
|
3月前
|
存储 编解码 开发者
Cookie原理及使用细节
Cookie原理及使用细节
60 0
|
3月前
|
存储 JSON 算法
net core jwt的基本原理和实现
这篇文章介绍了.NET Core中JWT(JSON Web Token)的基本原理和实现。JWT是一种用于安全传输信息的开放标准,由头部、负载和签名三部分组成。在.NET Core中实现JWT,需要安装`Microsoft.AspNetCore.Authentication.JwtBearer`包,然后在`Startup.cs`配置JWT认证服务,包括设置密钥和验证参数。生成JWT令牌后,客户端存储并将其包含在请求头中发送给服务器进行验证和授权。JWT提供了一种无需服务器存储会话数据的安全身份验证和授权机制。
|
3月前
|
存储 安全 对象存储
Cookie和Session的区别:从原理到应用
【2月更文挑战第18天】
283 6
|
存储 编解码
cookie的相关概念及原理
cookie的相关概念及原理
94 0
|
存储 PHP
php开发实战分析(2):cookie的动态使用(设置、获取、删除、猜你喜欢原理、购物车调用)
php开发实战分析(2):cookie的动态使用(设置、获取、删除、猜你喜欢原理、购物车调用)
180 0
|
编解码 分布式计算 Java
基于 netty 封装的超简单通俗易用 服务端客户端交互框架 《net-framework》原理,源码和使用说明,开箱即用,只需要开发业务逻辑,完全自定义无限扩充 [结尾附github源码]
基于 netty 封装的超简单通俗易用 服务端客户端交互框架 《net-framework》原理,源码和使用说明,开箱即用,只需要开发业务逻辑,完全自定义无限扩充 [结尾附github源码]
基于 netty 封装的超简单通俗易用 服务端客户端交互框架 《net-framework》原理,源码和使用说明,开箱即用,只需要开发业务逻辑,完全自定义无限扩充 [结尾附github源码]
|
存储 Web App开发 编解码
浏览器原理 32 # 跨站脚本攻击(XSS):为什么Cookie中有HttpOnly属性?
浏览器原理 32 # 跨站脚本攻击(XSS):为什么Cookie中有HttpOnly属性?
132 0
浏览器原理 32 # 跨站脚本攻击(XSS):为什么Cookie中有HttpOnly属性?
|
PHP 数据安全/隐私保护
PHP的cookie的域名、路径的区别是什么?底层原理是什么?
PHP的cookie的域名、路径的区别是什么?底层原理是什么?
114 0

相关实验场景

更多