Inspired by question on StackOverflow. Some parts of code are copied from MvcRouteHandler.
Note:
Your area names should not match any controller or method name. Otherwise, routing will not work correctly.
Step 1: Custom router class
We have to create class, which will split url into parts and use the first part as area. Original MvcRouteHandler is used here as a base class. Also, some code from it is used in RouteAsync method. Calling base method somewhy don’t work.
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
namespace Sith.Main
{
    public class AreaRouter : MvcRouteHandler, IRouter
    {
        private string[] _allowedSubdomains = { "Vpn", "Password" };
        //These are actualy copies of same values from base class. Some of them are used later.
        private IActionContextAccessor _actionContextAccessor;
        private IActionInvokerFactory _actionInvokerFactory;
        private IActionSelector _actionSelector;
        private ILogger _logger;
        private DiagnosticSource _diagnosticSource;
        public AreaRouter(
            IActionInvokerFactory actionInvokerFactory,
            IActionSelector actionSelector,
            DiagnosticSource diagnosticSource,
            ILoggerFactory loggerFactory)
            : this(actionInvokerFactory, actionSelector, diagnosticSource, loggerFactory, actionContextAccessor: null)
        {
        }
        public AreaRouter(IActionInvokerFactory actionInvokerFactory, IActionSelector actionSelector, DiagnosticSource diagnosticSource,
            ILoggerFactory loggerFactory, IActionContextAccessor actionContextAccessor)
            : base(actionInvokerFactory, actionSelector, diagnosticSource,
            loggerFactory, actionContextAccessor)
        {
            _actionContextAccessor = actionContextAccessor;
            _actionInvokerFactory = actionInvokerFactory;
            _actionSelector = actionSelector;
            _diagnosticSource = diagnosticSource;
            _logger = loggerFactory.CreateLogger<MvcRouteHandler>();
        }
        public new Task RouteAsync(RouteContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }
            string url = context.HttpContext.Request.Headers["HOST"];
            string firstDomain = url.Split('.')[0];
            //Areas usually start from uooer-case letter.
            string subDomain = char.ToUpper(firstDomain[0]) + firstDomain.Substring(1);
            //check if our app knows subdomain
            if(_allowedSubdomains.Contains(subDomain))
                context.RouteData.Values.Add("area", subDomain);
                
            //All the next code is copied from base class
            var candidates = _actionSelector.SelectCandidates(context);
            if (candidates == null || candidates.Count == 0)
            {
                return TaskCache.CompletedTask;
            }
            var actionDescriptor = _actionSelector.SelectBestCandidate(context, candidates);
            if (actionDescriptor == null)
            {
                return TaskCache.CompletedTask;
            }
            context.Handler = (c) =>
            {
                var routeData = c.GetRouteData();
                var actionContext = new ActionContext(context.HttpContext, routeData, actionDescriptor);
                if (_actionContextAccessor != null)
                {
                    _actionContextAccessor.ActionContext = actionContext;
                }
                var invoker = _actionInvokerFactory.CreateInvoker(actionContext);
                if (invoker == null)
                {
                    throw new InvalidOperationException();
                }
                return invoker.InvokeAsync();
            };
            return TaskCache.CompletedTask;
        }
    
    }
}
Step 2: Setting up Startup.cs
Firstly, we need to create AreaRouter which will be later used:
public void ConfigureServices(IServiceCollection services)
        {
            //some other stuff
            services.AddSingleton<AreaRouter>();
        }
Now we have working instance of area router and can configure the routes(note the change of the signature of the method):
//pass area router using dependancy injection
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, AreaRouter areaRouter)
    {
        //some other stuff
        app.UseMvc(routes =>
            {
                routes.DefaultHandler = areaRouter;
                routes.MapRoute("areaRoute", "{area:exists}/{controller=Admin}/{action=Index}/{id?}");
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
    }        
Step 3(only IIS):
Open .vs\config\applicationHost.config in project folder. Inside it find line similar to these:
<bindings>
   <binding protocol="http" bindingInformation="*:5252:localhost" />
</bindings>
Add your subdomains here. They should look like <binding protocol="http" bindingInformation="*:5252:contoso.localhost" />. Start Visual Studio with admin rights and test your application. IIS will not start working with subdomains without admin rights.
Finally:
This example can be improved in some ways and it has some drawbacks(read Note on the top), but it is working. Complete project is available here.