ZX Security


Security Feature Bypass In ASP.NET and Visual Studio – Race Condition

Jack Moran, TC, and Ethan McKee-Harris discovered a security feature bypass within the SignInManger in ASP.NET.

Published on

Introduction

During an engagement, Jack Moran, with the aide of TC and Ethan McKee-Harris, discovered a security feature bypass within ASP.NET SignInManager. The SignInManager was found to be susceptible to a race condition which when exploited could allow for thousands of brute-force login attempts to be conducted before ever triggering the lockout threshold.

CVE-2023-33170 Attributions

Screenshot of MSRC Acknowledgements.

So What Happened?

During a web application pentest, Jack Moran from ZX Security found an inconsistency when attempting to credential stuff a login form. This inconsistency centred around the lockout threshold and when the application was triggering it. Further analysis indicated that the default lockout was configured for 3 invalid login attempts, however, testing showed that this was inconsistent and could far exceed the expected lockout. Initial tests at the time highlighted that the lockout triggered sporadically, sometimes ranging from 10 to 50 failed authentication attempts without the lockout triggering.

Early indications of the system highlighted the culprit to be the ASP.NET SignInManager as when this function is called it ‘Attempts to sign in the specified userName and password combination as an asynchronous operation’. With asynchronous operations, when multiple threads can access or change a shared resource a race condition can occur. When looking at the database logs it was observed that this was happening. A snippet of the database log is included below, with an error indicating that a ‘Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException’ was present and that the ‘database operation failed as the data may have been modified or deleted’

  fail: Microsoft.EntityFrameworkCore.Update[10000]
    An exception occurred in the database while saving changes for context type 'WebApp.Data.ApplicationDbContext'.
    Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException: The database operation was expected to affect 1 row(s), but actually affected 0 row(s); data may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions.
      at Microsoft.EntityFrameworkCore.Update.AffectedCountModificationCommandBatch.ThrowAggregateUpdateConcurrencyExceptionAsync(RelationalDataReader reader, Int32 commandIndex, Int32 expectedRowsAffected, Int32 rowsAffected, CancellationToken cancellationToken)
      at Microsoft.EntityFrameworkCore.Update.AffectedCountModificationCommandBatch.ConsumeResultSetWithRowsAffectedOnlyAsync(Int32 commandIndex, RelationalDataReader reader, CancellationToken cancellationToken)
      at Microsoft.EntityFrameworkCore.Update.AffectedCountModificationCommandBatch.ConsumeAsync(RelationalDataReader reader, CancellationToken cancellationToken)
      at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
      at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
      at Microsoft.EntityFrameworkCore.SqlServer.Update.Internal.SqlServerModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
      at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
      at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
      at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
      at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(IList`1 entriesToSave, CancellationToken cancellationToken)
      at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(StateManager stateManager, Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
      at Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
      at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
    Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException: The database operation was expected to affect 1 row(s), but actually affected 0 row(s); data may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions.
      at Microsoft.EntityFrameworkCore.Update.AffectedCountModificationCommandBatch.ThrowAggregateUpdateConcurrencyExceptionAsync(RelationalDataReader reader, Int32 commandIndex, Int32 expectedRowsAffected, Int32 rowsAffected, CancellationToken cancellationToken)
      at Microsoft.EntityFrameworkCore.Update.AffectedCountModificationCommandBatch.ConsumeResultSetWithRowsAffectedOnlyAsync(Int32 commandIndex, RelationalDataReader reader, CancellationToken cancellationToken)
      at Microsoft.EntityFrameworkCore.Update.AffectedCountModificationCommandBatch.ConsumeAsync(RelationalDataReader reader, CancellationToken cancellationToken)
      at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
      at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
      at Microsoft.EntityFrameworkCore.SqlServer.Update.Internal.SqlServerModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
      at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
      at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
      at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
      at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(IList`1 entriesToSave, CancellationToken cancellationToken)
      at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(StateManager stateManager, Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
      at Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
      at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)

These errors highlighted multiple concurrent requests are attempting to update the database, with many of these requests failing to update appropriately. This is due to the next concurrent request already modifying the database. As a result, many login attempts are performed, but the database is not modified consistently, indicating a race condition may be present allowing a user to perform a brute-force attack. Jack Moran decided to write a proof of concept to test this out locally which resulted in thousands of failed authentication requests being conducted before triggering the lockout threshould.

So You Want The PoC?

Unfortunately, we are unable to post the PoC today, however, watch this space. This is due to the MSRC terms and conditions stating:

We require that detailed proof-of-concept exploit code and details that would make attacks easier on customers be withheld for 30 days after the Vulnerability is fixed.

Development of Testing Environment

While the proof of concept was in development, Ethan McKee-Harris deployed a testing environment based on Microsoft’s ‘Create a Web app with authentication’ documentation. A local server and web application was set up to verify that this is not isolated to the current engagement but could be widespread in the SignInManger itself. After installing the latest version of dotnet (at the time we were testing), we generated a new web application using Microsoft’s scaffolding:

  dotnet new webapp --auth Individual -o WebApp

After creating the scaffolded web application, developers can also install templated account management with the following commands:

  cd WebApp
  dotnet tool install -g dotnet-aspnet-codegenerator
  dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
  dotnet aspnet-codegenerator identity -dc WebApp.Data.ApplicationDbContext --files "Account.Register;Account.Login;Account.Logout;Account.RegisterConfirmation" --databaseProvider=sqlite

Navigate to and open the file Areas/Identity/Pages/Account/Login.cshtml.cs. Change the PasswordSignInAsync method call so that lockoutOnFailure is set to true. We also need to enable account lockout, so navigate to Program.cs in the base web application directory. After the builder is defined, paste the following configuration options. This sets up the requirements to support account lockout on our test web application.

  builder.Services.Configure<IdentityOptions>(options =>
  {
      // Password settings.
      // Turn off requirements for testing purposes
      options.Password.RequireDigit = false;
      options.Password.RequireLowercase = false;
      options.Password.RequireNonAlphanumeric = false;
      options.Password.RequireUppercase = false;
      options.Password.RequiredLength = 6;
      options.Password.RequiredUniqueChars = 1;

      // Lockout settings.
      options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
      options.Lockout.MaxFailedAccessAttempts = 5;
      options.Lockout.AllowedForNewUsers = true;

      // User settings.
      options.User.AllowedUserNameCharacters =
      "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";
      options.User.RequireUniqueEmail = false;
  });

  builder.Services.ConfigureApplicationCookie(options =>
  {
      // Cookie settings
      options.Cookie.HttpOnly = true;
      options.ExpireTimeSpan = TimeSpan.FromMinutes(5);

      options.LoginPath = "/Identity/Account/Login";
      options.AccessDeniedPath = "/Identity/Account/AccessDenied";
      options.SlidingExpiration = true;
  });

Further to this, if you wish to conduct testing from non-local IP addresses, add the following config to appsettings.json in order to expose the web application to all IP addresses.

  "Kestrel": {
      "EndPoints": {
        "Http": {
          "Url": "http://0.0.0.0:5010"
        }
      }
    }

Then to run the application, simply use dotnet run on the command line.

Throughout the disclosure process, Microsoft have been exceedingly helpful. Working within the bounds of the Microsoft Security Researcher Center (MSRC) disclosure policy, they were quick to confirm the issue and work with ZX Security to navigate the vulnerability through the various stages, resulting in a patch that resolves the issues.

Vulnerability Disclosure Timeline (NZST):

  • 03/03/2023 - Discovered a race condition in ASP.NET Core SignInManager
  • 07/03/2023 - Submission of vulnerability to Microsoft Security Research Center for review
  • 08/03/2023 - MSRC: Case Number Assigned, Stage - New
  • 08/03/2023 - MSRC: Vulnerability Being Reviewed, Stage - Review/Reproduce
  • 07/04/2023 - MSRC: Vulnerability Confirmed, Stage - Develop
  • 18/05/2023 - MSRC: Vulnerability Disclosure Extension Request
  • 26/05/2023 - MSRC: Vulnerability Severity Rating Important (High), Stage - Develop
  • 12/07/2023 - MSRC: Vulnerability Fixed, Stage - PreRelease
  • 12/07/2023 - MSRC: Vulnerability Fixed, Stage - Complete

References