Description
It seems that WindowsIdentity.RunImpersonated() works differently in .NET Core compared with .NET Framework. This is causing a variety of issues including one affecting ASP.NET Core, #29351.
There is some difference in the way that the identity token permissions are getting set on the impersonated token. This is causing "access denied" issues in a variety of ways.
Consider the following repro program included in this issue.
Program.cs
using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Security.Principal;
using Microsoft.Win32.SafeHandles;
namespace ImpersonateTest
{
class Program
{
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern bool LogonUser(
string username,
string domain,
string password,
int logonType,
int logonProvider,
out SafeAccessTokenHandle token);
const int LOGON32_PROVIDER_DEFAULT = 0;
const int LOGON32_LOGON_INTERACTIVE = 2;
const int LOGON_TYPE_NETWORK = 3;
const int LOGON_TYPE_NEW_CREDENTIALS = 9;
static void Main(string[] args)
{
Console.WriteLine($"(Framework: {Path.GetDirectoryName(typeof(object).Assembly.Location)})");
SafeAccessTokenHandle tokenin;
bool returnValue = LogonUser("test1", ".", "****", LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, out tokenin); // ** Fails on .NET Core
//bool returnValue = LogonUser("test1", ".", "****", LOGON_TYPE_NETWORK, LOGON32_PROVIDER_DEFAULT, out tokenin); // ** Works on .NET Core
Debug.Assert(returnValue);
Run(tokenin);
tokenin.Dispose();
}
static void Run(SafeAccessTokenHandle token)
{
WindowsIdentity.RunImpersonated(token, () =>
{
RunDnsTest();
RunSocketsHttpHandlerTest();
RunWinHttpHandlerTest();
});
}
static void RunSocketsHttpHandlerTest()
{
try
{
var client = new HttpClient();
HttpResponseMessage response = client.GetAsync("http://corefx-net.cloudapp.net/echo.ashx").GetAwaiter().GetResult();
Console.WriteLine($"{WindowsIdentity.GetCurrent().Name} {WindowsIdentity.GetCurrent().ImpersonationLevel}");
Console.WriteLine($"{(int)response.StatusCode} {response.ReasonPhrase}");
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
static void RunWinHttpHandlerTest()
{
try
{
var handler = new WinHttpHandler();
var client = new HttpClient(handler);
HttpResponseMessage response = client.GetAsync("http://corefx-net.cloudapp.net/echo.ashx").GetAwaiter().GetResult();
Console.WriteLine($"{WindowsIdentity.GetCurrent().Name} {WindowsIdentity.GetCurrent().ImpersonationLevel}");
Console.WriteLine($"{(int)response.StatusCode} {response.ReasonPhrase}");
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
static void RunDnsTest()
{
try
{
string host = "www.google.it";
Console.WriteLine($"{WindowsIdentity.GetCurrent().Name} {WindowsIdentity.GetCurrent().ImpersonationLevel}");
Console.WriteLine($"Dns.GetHostAddressesAsync({host}) " + Dns.GetHostAddressesAsync(host).Result[0].ToString());
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
}
}
ImpersonateTest.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>netcoreapp2.2;netcoreapp3.0;net47</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Net.Http.WinHttpHandler" Version="4.5.4" />
<PackageReference Include="System.Security.Principal" Version="4.3.0" />
<PackageReference Include="System.Security.Principal.Windows" Version="4.5.1" />
</ItemGroup>
<!-- Conditionally obtain references for the .NET Framework 4.7 target -->
<ItemGroup Condition=" '$(TargetFramework)' == 'net47' ">
<Reference Include="System.Net.Http" />
</ItemGroup>
</Project>
To demonstrate the repro, create a local machine account (different from the one you use to run this repro) on the Windows machine. It doesn't matter if it belongs to the "Administrators" group or not.
On .NET Framework, the repro works fine with either LOGON32_LOGON_INTERACTIVE or LOGON_TYPE_NETWORK being used to create the impersonated identity. But .NET Core shows a variety of problems with using LOGON32_LOGON_INTERACTIVE. This repro is a simplified version of the ASP.NET Core issue #29351 which is presumably using a logged on identity similar to LOGON32_LOGON_INTERACTIVE.
The problems on .NET Core are the same using .NET Core 2.2 or .NET Core 3.0 Preview 6.
There are three tests in this repro. In one case, the System.IO.FileLoadException is not even catch'able. In my repro here, I have created a secondary Windows account called "test1".
Success case:
S:\dotnet\ImpersonateTest>dotnet run -f netcoreapp2.2
(Framework: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\2.2.5)
DSHULMAN-REPRO1\test1 Impersonation
Dns.GetHostAddressesAsync(www.google.it) 2607:f8b0:400a:800::2003
DSHULMAN-REPRO1\test1 Impersonation
200 OK
DSHULMAN-REPRO1\test1 Impersonation
200 OK
Failure case:
S:\dotnet\ImpersonateTest>dotnet run -f netcoreapp2.2
(Framework: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\2.2.5)
DSHULMAN-REPRO1\test1 Impersonation
System.AggregateException: One or more errors occurred. (This is usually a temporary error during hostname resolution and means that the local server did not receive a response from an authoritative server)
---> System.Net.Sockets.SocketException: This is usually a temporary error during hostname resolution and means that the local server did not receive a response from an authoritative server
at System.Net.Dns.HostResolutionEndHelper(IAsyncResult asyncResult)
at System.Net.Dns.EndGetHostAddresses(IAsyncResult asyncResult)
at System.Net.Dns.<>c.<GetHostAddressesAsync>b__25_1(IAsyncResult asyncResult)
at System.Threading.Tasks.TaskFactory`1.FromAsyncCoreLogic(IAsyncResult iar, Func`2 endFunction, Action`1 endAction, Task`1 promise, Boolean requiresSynchronization)
--- End of inner exception stack trace ---
at System.Threading.Tasks.Task`1.GetResultCore(Boolean waitCompletionNotification)
at ImpersonateTest.Program.RunDnsTest() in S:\dotnet\ImpersonateTest\Program.cs:line 87
---> (Inner Exception #0) System.Net.Sockets.SocketException (11002): This is usually a temporary error during hostname resolution and means that the local server did not receive a response from an authoritative server
at System.Net.Dns.HostResolutionEndHelper(IAsyncResult asyncResult)
at System.Net.Dns.EndGetHostAddresses(IAsyncResult asyncResult)
at System.Net.Dns.<>c.<GetHostAddressesAsync>b__25_1(IAsyncResult asyncResult)
at System.Threading.Tasks.TaskFactory`1.FromAsyncCoreLogic(IAsyncResult iar, Func`2 endFunction, Action`1 endAction, Task`1 promise, Boolean requiresSynchronization)<---
System.Net.Http.HttpRequestException: This is usually a temporary error during hostname resolution and means that the local server did not receive a response from an authoritative server
---> System.Net.Sockets.SocketException: This is usually a temporary error during hostname resolution and means that the local server did not receive a response from an authoritative server
at System.Net.Http.ConnectHelper.ConnectAsync(String host, Int32 port, CancellationToken cancellationToken)
--- End of inner exception stack trace ---
at System.Net.Http.ConnectHelper.ConnectAsync(String host, Int32 port, CancellationToken cancellationToken)
at System.Threading.Tasks.ValueTask`1.get_Result()
at System.Net.Http.HttpConnectionPool.CreateConnectionAsync(HttpRequestMessage request, CancellationToken cancellationToken)
at System.Threading.Tasks.ValueTask`1.get_Result()
at System.Net.Http.HttpConnectionPool.WaitForCreatedConnectionAsync(ValueTask`1 creationTask)
at System.Threading.Tasks.ValueTask`1.get_Result()
at System.Net.Http.HttpConnectionPool.SendWithRetryAsync(HttpRequestMessage request, Boolean doRequestAuth, CancellationToken cancellationToken)
at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
at System.Net.Http.HttpClient.FinishSendAsyncBuffered(Task`1 sendTask, HttpRequestMessage request, CancellationTokenSource cts, Boolean disposeCts)
at ImpersonateTest.Program.RunSocketsHttpHandlerTest() in S:\dotnet\ImpersonateTest\Program.cs:line 55
Unhandled Exception: System.IO.FileLoadException: Could not load file or assembly 'System.Net.Http.WinHttpHandler, Version=4.0.3.2, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'. Access is denied.
at ImpersonateTest.Program.RunWinHttpHandlerTest()
at ImpersonateTest.Program.<>c.<Run>b__6_0() in S:\dotnet\ImpersonateTest\Program.cs:line 46
at System.Security.Principal.WindowsIdentity.<>c__DisplayClass64_0.<RunImpersonatedInternal>b__0(Object <p0>)
at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location where exception was thrown ---
at System.Security.Principal.WindowsIdentity.RunImpersonatedInternal(SafeAccessTokenHandle token, Action action)
at System.Security.Principal.WindowsIdentity.RunImpersonated(SafeAccessTokenHandle safeAccessTokenHandle, Action action)
at ImpersonateTest.Program.Run(SafeAccessTokenHandle token) in S:\dotnet\ImpersonateTest\Program.cs:line 42
at ImpersonateTest.Program.Main(String[] args) in S:\dotnet\ImpersonateTest\Program.cs:line 35
In the RunSocketsHttpHandlerTest() and RunDnsTest(), the error:
System.Net.Sockets.SocketException: This is usually a temporary error during hostname resolution and means that the local server did not receive a response from an authoritative server
is being caused by the Win32 API GetAddrInfoExW() returning WSATRY_AGAIN error. This error is occurring immediately after calling the API. .NET Core is using GetAddrInfoExW() instead of GetAddrInfoW() because the former supports async (via overlapped callback). The GetAddrInfoW() API doesn't seem to be affected. .NET Framework doesn't use GetAddrInfoExW() so it isn't affected. I suspect that GetAddrInfoExW() is returning WSATRY_AGAIN due to the same access permissions problem running in the WindowsIdentity.RunImpersonated() context.
We also have #28460 which is a related problem with impersonation where DNS resolution is not working. That's probably due to the same GetAddrInfoExW() problem here.
This seems like a compatibility break from .NET Framework in how WindowsIdentity.RunImpersonated() behaves.