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, dotnet/runtime#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
```c#
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);
}
}
}
}
</details>
<details>
<summary>
ImpersonateTest.csproj
</summary>
```xml
<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 dotnet/runtime#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 dotnet/runtime#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.
cc: @kouvel @stephentoub @danmosemsft
@Tratcher Can you clarify how ASP.NET Core creates the impersonated context token described by the repro in dotnet/runtime#29351.
ASP.NET Core does not do any impersonation, a user has to do it manually by calling WindowsIdentity.RunImpersonated. Do you mean how the WindowsIdentity is created? It's created from a handle provided by Http.Sys, IIS, or NTAuthentication.
Do you mean how the WindowsIdentity is created? It's created from a handle provided by Http.Sys, IIS, or NTAuthentication.
Yes. Thanks for clarifying that ASP.NET Core doesn't control how the identity token handle is created. It is made from http.sys. So,this further reinforces the severity of this issue in that WindowsIdentity.RunImpersonated() is behaving differently on .NET Core compared with .NET Framework. Issue dotnet/runtime#29351 is being caused as a direct result of this issue.
@brentschmaltz ?
@Tratcher wanted to but couldn't get to this. I will have to pick it up after the 4th holiday.
@brentschmaltz did you get chance to look at it? Are you going to fix it for 3.0? (i.e. in next few days)
@Tratcher is it blocker for 3.0?
cc @danmosemsft
The problems on .NET Core are the same using .NET Core 2.2 or .NET Core 3.0 Preview 6.
Not blocking for 3.0 as this wasn't a regression. It may be a patch candidate for 2.2 (and 3.0) depending on the fix.
Moving to 5.0 since I don't believe this currently meets the bar.
I have a similar issue in that exceptions thrown inside the Action supplied to RunImpersonated can't be caught. May be this is related?
Looks like it is working for IP address but not working on hostname.
@davidsh you mentioned that GetAddrInfoExW is making an async call where before we didn't. I wonder if GetAddrInfoExW is sensitive to the impersonation token of the calling thread, and the DNS code isn't flowing the impersonation token to the continuation/EndGetHostAddresses thread? Related: https://github.com/dotnet/corefx/issues/24977. It's not clear to me why we're blaming RunImpersonated for this when we said that we didn't call GetAddrInfoExW on .NETFramework. Is it possible that the implementation of GetHostAddressesAsync
https://github.com/dotnet/corefx/pull/26850 never worked under impersonation? /cc @stephentoub
Is it possible that the implementation of GetHostAddressesAsync
dotnet/corefx#26850 never worked under impersonation?
It's possible but I haven't done any further investigation on this.
I'm not 100% positive it's the same issue, but it sounds similar.
We are trying to access an SMB-Share while beeing impersonated and get AccessDenied-Exceptions on the share itself.
Inspecting the problem with ProcMon and WireShark showed, that there is no attempt made to get the S4U-Token in such a way it would be acceptable by the target server.
An older .NET Full application doing something similar shows no such problems.
The older application will make an KRB5 request which fails due to pre-auth and then go on and issue an KRB5 request which proceeds and creates the needed token.
The newer application will do the first KRB5 request and then NOT gather to neccessary token.
Furthermore WireShark does not see any atempt to access the target SMB-share, while ProcMon Shows a failed attempt.
We can probably reduce the application to a minimal repro, if that would help.
I second this issue. I'm having to rewrite our .NET Core application back to .NET Framework solely because of this bug - our entire implementation relies on this working.
FYI: We removed async from our impersonated calls with no luck.
We can probably reduce the application to a minimal repro, if that would help.
Yes, that would be helpful. It would allow us to diagnose it faster.
Okay I have an application which repros the problem, but the environment needs to be properly configured (btw. running locally is also messy, since it won't load DLLs correctly on non admin accounts).
What do you need:
1) a Server for running the repro.
2) A FileServer (Windows Auth Enabled) "\fs01.domain.com"
3) A gMSA, which is able to delegate to the FileServer, which also has "ActAsPartOfOS" and all necessary privileges
4) A user account you want to impersonate ([email protected])
5) PsExec to run the repro app as gMSA on the server with the parameters: "\fs01.domain.com" "[email protected]".
The unexpected error for me was:
System.UnauthorizedAccessException: Access to the path '\fs02.corp.deusername$' is denied.
If you'd like to: my machine is setup and I can access it with you via Teams or something similar.
WireShark et al are also installed, to further investigate and we can set neccessary trace-points, Collectors and what not.
Windows Authentication in .net core does indead not seem to work as in full framework.
When will we see updates to this? we are porting an app to .net core 3.1 which heavily uses windows impersonation and also uses the policy "act as part of the operating system" so that it can process without having to have all passwords, but for now we are forced to enter all passwords...
@bartonjs do you know what the root cause is?
@brentschmaltz I don't, no. But I don't know anything about impersonation. I believe that the hope is that you would tell us :smile:.
@bartonjs i will finally have some time to look into this in this qtr.
Hi!
Any updates regarding this issue?
Im experiencing the same issue using impersonation (WindowsIdentity.RunImpersonated) with ASP.net Core 3.1 while trying to impersonate a request using a HttpClient (I have had some success doing this using a modified version of WebClient.. however.. that one ended up with a problem resolving DNS which has been described previously in this issue).
This currently prevents a big client for my company to develop a new application using .Net Core, so for now it seems like we will use Framework instead.
And as alaitang previously mentioned, the DNS problem seems to be related to a DNS lookup somehow.. since the exception is gone as soon as you target an IP as host, instead of a DNS (However.. that got me stuck with the access denied issue instead)
i am using .net core 3.1.18 web api hosted in IIS. i have load balancer hosting the api on two windows server. i am writing a file since i need to access the file from from two server i am using UNC path for folder sharing.
i need to access a shared location and i am running the code using WindowsIdentity.RunImpersonated.
apppool is running and Network service and Network service has access on shared location.
i am facing the issues not able to access the path.
is there any update on or how i can handle this scenario.
Seems odd that Impersonation has such a core bug and that it just keeps getting kicked down the road. (As I understand it, the 6.0.0 milestone is basically saying that this has to wait another year for a hope of a fix.)
This is a workaround for .Net Core 3.1:
Environment.SetEnvironmentVariable("DOTNET_SYSTEM_NET_HTTP_USESOCKETSHTTPHANDLER", "0");
Alternativly you can do this:
AppContext.SetSwitch("System.Net.Http.UseSocketsHttpHandler", false);
But that setting (System.Net.Http.UseSocketsHttpHandler
) was removed from .Net 5.0. As such, if you need impersonation, and are affected by this issue, you cannot upgrade to .Net 5.0.
cc @karelz for comment above.
@karelz, while your at it you might consider looking at https://github.com/dotnet/runtime/issues/29935#issuecomment-571383680
I wonder if GetAddrInfoExW is sensitive to the impersonation token of the calling thread, and the DNS code isn't flowing the impersonation token to the continuation/EndGetHostAddresses thread
Environment.SetEnvironmentVariable("DOTNET_SYSTEM_NET_HTTP_USESOCKETSHTTPHANDLER", "0");
@Vaccano I have tried this with 3.1 and 5 without any success. You can see my GitHub issue here: #45165
@BChip - I am realizing that the workaround I indicated does not always work.
Using that setting (UseSocketsHttpHandler = false
) I had my RunImpersonated
integration tests running fine on my Windows 10 machine.
But when I sent them to my build server they failed with an error I have not seen (_Error 5 calling WinHttpOpen, 'Access is denied.'_). My build server is running Windows Server 2012 R2.
After having so many issues with it, I am realizing that impersonation is just not ready for real production use in the .Net Core product line.
And considering this issue has been open for over a year and has just been slotted into a milestone that is a year away, it has become clear that impersonation is not something the .Net Product Team considers an important feature.
Though it is a significantly poorer solution, I am going to have to rework all my tests to not make use of the RunImpersonated
method. I will warn any of my developers that need impersonation away from .Net Core / .Net 5. Basically, if you need impersonation, you should stay with the full .Net Framework 4.x.
@Vaccano yes same feeling. We want to move on with .net core / .net 5 but there are showstoppers.
Besides the poor impersonation we experience also issues with XPS printing. We have to stay on .NET Framework 4.8. Oh boy..
@Vaccano even I am very disappointed. My scenario is very simple share a file between tow iis server.
I think we should understand that .net core designed to work in cloud scenarios. This is general guideline from MSFT: do not give attention to features that work only in on-primes. Impersonation is one of such.
I believe GetAddrInfoExW
does more checks than GetAddrInfoW
I pulled all the native methods out
C#
Interop.Advapi32.ImpersonateLoggedOnUser(tokenin);
GetAddrInfoAsync();
Interop.Advapi32.RevertToSelf();
it always returns 11002
I think we should understand that .net core designed to work in cloud scenarios. This is general guideline from MSFT: do not give attention to features that work only in on-primes. Impersonation is one of such.
if .NET 5 is the successor of .NET Framework + .NET Core and major releases of .NET will come each year .NET 5/.NET 6/.NET 7/...
i'm sure we have to move to this .NET. but if impersonation is not working as it is in .NET Framework, we can't keep using "old" tech just because one thing is not properly working as the team has focus on "fancy pancy" things instead of the core workings.
As said there are also other problems and where improvements could be made, e.g. XPS. I know this is not so sexy and less used by community but please put also focus on it. You are really giving us lots of extra headaches and hard weeks.
@Vaccano i can feel your pain.
We'll try to have someone investigate.
I've tested with netframework 4.7
The Impersonate does not work with GetAddrInfoExW
Would the process running the impersonation be required to be running as a Windows Service?
From my experience you need two privileges:
1) ActAsPartOfOperatingSystem (enables login without password)
2) ImpersonateUser (this one is granted to IIS-Processed automatically)
I'm testing in Windows 10.
It seems if you call LogonUser with LOGON32_LOGON_BATCH
with a user that is admin, the process has to be running in an elevated mode (as admin) in Windows 10 in order to not throw exceptions after impersonation.
The user seems to need some permission to call GetAddrInfoExW
which I am unable to find out, so I just made it admin
So you have to have the process in elevated mode, impersonating an admin user to call GetAddrInfoExW
successfully
I realized there are 2 issues being discussed.
Environment.SetEnvironmentVariable("DOTNET_SYSTEM_NET_HTTP_USESOCKETSHTTPHANDLER", "0");
2. Permission issues around `GetAddrInfoExW ` for the impersonating user
In .Net 5.0, HttpClient now have sync methods, which you can use to test and bypass the async dns issue
Edit:
for file access issue with sUserPrincipalName
https://docs.microsoft.com/en-us/windows/win32/api/ntsecapi/nf-ntsecapi-lsalogonuser
The caller is required to have SeTcbPrivilege only if one or more of the following is true:
A Subauthentication package is used.
KERB_S4U_LOGON is used, and the caller requests an impersonation token.
The LocalGroups parameter is not NULL.
So, we have to do something similar to what framework does but rather than enabling `SeTcbPrivilege` we should check if it is enabled and call `LsaRegisterLogonProcess` if it is. (Is `LsaRegisterLogonProcess` even necessary?)
```C#
// Try to get an impersonation token.
try {
// Try to enable the TCB privilege if possible
try {
privilege = new Privilege("SeTcbPrivilege");
privilege.Enable();
}
catch (PrivilegeNotHeldException) { }
IntPtr dummy = IntPtr.Zero;
status = Win32Native.LsaRegisterLogonProcess(ref Name, ref logonHandle, ref dummy);
if (Win32Native.ERROR_ACCESS_DENIED == Win32Native.LsaNtStatusToWinError(status)) {
// We don't have the Tcb privilege. The best we can hope for is to get an Identification token.
status = Win32Native.LsaConnectUntrusted(ref logonHandle);
}
}
catch {
// protect against exception filter-based luring attacks
if (privilege != null)
privilege.Revert();
throw;
}
finally {
if (privilege != null)
privilege.Revert();
}
if (status < 0) // non-negative numbers indicate success
throw GetExceptionFromNtStatus(status);
Edit2:
Would the following work?
C#
//enable Tcb privilege
var wi = new WindowsIdentity("domain");
//disable Tcb privilege
In my case, we grant SeTcbPrivilege and SeImpersonate to the executing process identity.
In .NET 4.x, The impersonation done via var wi = new WindowsIdentity("[email protected]")
and wi.Impersonate()
then works without having pc administrator rights (you mentioned admin rights somewhere, which our process identity does not and should not have).
Most privileges are disabled and requires enabling before calling Apis that require them. In framework that is being done for you however in core it seems they decided not to enable the tcb privilege for the user. Having the privilege lets you enable it but most are not enabled by default
In windows 10, it seems you can't use tcb privilege without running in an elevated mode. You only have access to 5 privileges in non elevated mode.
For the S4U login
the main difference between framework and core is the enabling of SeTcbPrivilege
Framework: https://referencesource.microsoft.com/#mscorlib/system/security/principal/windowsidentity.cs,979
Core: https://github.com/dotnet/runtime/blob/c8a97ff77957202323b64b8513f363366010343f/src/libraries/System.Security.Principal.Windows/src/System/Security/Principal/WindowsIdentity.cs#L122-L124
https://github.com/dotnet/runtime/blob/c8a97ff77957202323b64b8513f363366010343f/src/libraries/System.Security.Principal.Windows/src/System/Security/Principal/WindowsIdentity.cs#L209-L214
in LsaLogonUser
it says
If using a service account, the account must have SeTcbPrivilege set on the local computer to get an impersonation token.
Otherwise, the identity token is used.
In windows 10, it seems you can't use tcb privilege without running in an elevated mode
This would imply, my .NET 4.7 app cannot run on windows 10, but it does. Or am I getting that wrong?
OTOH: I don't care about impersonation to be true. I use kerberos constrained delegation, perhaps this woul work anyway?
Edit: this probably doesn't work. Someone has tried. Your issue is probably related to https://github.com/dotnet/runtime/issues/28460
===============================================================
I have no idea, the user rights thing on windows is quite complicated.
You can try enabling SeTcbPrivilege
and see if it works. I don't really see other differences between framework and core in WindowsIdentity
.
```C#
TogglePrivilege("SeTcbPrivilege", true);
var wi = new WindowsIdentity("user@domain");
TogglePrivilege("SeTcbPrivilege", false);
you could compare the login logs in the event viewer between core and framework
<details>
<summary>privilege</summary>
```C#
internal class Privileges
{
public static uint TOKEN_QUERY = 0x00000008;
public static uint TOKEN_ADJUST_PRIVILEGES = 0x00000020;
private const int ANYSIZE_ARRAY = 1;
[DllImport("kernel32.dll", SetLastError = true)]
static extern SafeHandle GetCurrentProcess();
[DllImport("advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool OpenProcessToken(SafeHandle ProcessHandle, UInt32 DesiredAccess, out SafeHandle TokenHandle);
[DllImport("advapi32.dll", SetLastError = true)]
static extern bool LookupPrivilegeValue(string lpSystemName, string lpName, out LUID lpLuid);
[DllImport("advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool AdjustTokenPrivileges(SafeHandle TokenHandle,
[MarshalAs(UnmanagedType.Bool)]bool DisableAllPrivileges,
ref TOKEN_PRIVILEGES NewState,
UInt32 BufferLengthInBytes,
ref TOKEN_PRIVILEGES PreviousState,
out UInt32 ReturnLengthInBytes);
[StructLayout(LayoutKind.Sequential)]
public struct LUID
{
public UInt32 LowPart;
public Int32 HighPart;
}
public struct TOKEN_PRIVILEGES
{
public int PrivilegeCount;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = ANYSIZE_ARRAY)]
public LUID_AND_ATTRIBUTES[] Privileges;
}
[StructLayout(LayoutKind.Sequential)]
public struct LUID_AND_ATTRIBUTES
{
public LUID Luid;
public UInt32 Attributes;
public const UInt32 SE_PRIVILEGE_ENABLED_BY_DEFAULT = 0x00000001;
public const UInt32 SE_PRIVILEGE_ENABLED = 0x00000002;
public const UInt32 SE_PRIVILEGE_REMOVED = 0x00000004;
public const UInt32 SE_PRIVILEGE_USED_FOR_ACCESS = 0x80000000;
}
public static bool TogglePrivilege(string privilege, bool enable)
{
LUID luid = new LUID();
using SafeHandle hProcess = GetCurrentProcess();
SafeHandle hToken;
if (!OpenProcessToken(hProcess, TOKEN_QUERY | TOKEN_ADJUST_PRIVILEGES, out hToken)) return false;
using (hToken)
{
if (!LookupPrivilegeValue(null, privilege, out luid)) return false;
LUID_AND_ATTRIBUTES luAttr = new LUID_AND_ATTRIBUTES { Luid = luid, Attributes = enable ? LUID_AND_ATTRIBUTES.SE_PRIVILEGE_ENABLED : 0 };
TOKEN_PRIVILEGES tp = new TOKEN_PRIVILEGES { PrivilegeCount = 1, Privileges = new LUID_AND_ATTRIBUTES[1] };
tp.Privileges[0] = luAttr;
TOKEN_PRIVILEGES oldState = new TOKEN_PRIVILEGES();
if (!AdjustTokenPrivileges(hToken, false, ref tp, (UInt32)Marshal.SizeOf(tp), ref oldState, out UInt32 returnLength))
{
var err = Marshal.GetLastWin32Error();
return false;
}
var rc = Marshal.GetLastWin32Error();
return rc == 0;
}
}
}
@GrabYourPitchforks do you have context on why the code differs in use of TCB privilege (and whether it is the cause of this issue). I could not find discussion of why this is done differently in Core -- the change goes back as far as when Core was initially populated so I'd need to find the internal sources from before hten.
Actually according to https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2003/cc738207(v=ws.10)
WindowsIdentity calling LsaLogonUser should be sufficient. And TcbPrivilege is something that depends on the use case
Also to bring us back to this issue:
The issue here is that when you impersonate a user async dns doesn't work. It seems to need some extra privilege.
@stephentoub and I took a look at this issue offline and I wanted to share the results.
The FileLoadException
issue is a result of the impersonated user not having read permission to the resource being accessed. The same issue can be reproduced in .NETFramework. When using impersonation in an application you need to make sure the impersonated user has access to read the application binaries as those binaries may be loaded by the runtime while the thread is impersonating. You can also avoid this sort of problem by "warming up" the codepath: loading everything as a regular user before impersonating. Since this isn't a new issue let's table that aspect of the original report.
The primary issue here is with GetHostAddressesAsync
. This method was changed in https://github.com/dotnet/corefx/commit/d3ff31e3b9e8c7b1e6003196650bae4a658ef889 (.NETCore 2.1) to use GetAddrInfoExW
to make it actually async. There seems to be a problem with GetAddrInfoExW
that whenever it's run under impersonation it will fail. We were able to reproduce the same failure by porting a portion of this code to .NETFramework. This was previously correctly noted by @wzchua https://github.com/dotnet/runtime/issues/29935#issuecomment-734823538.
This appears to be due to the async handling in ws2_32:
> KernelBase.dll!OpenThreadToken()
ws2_32.dll!WinsockThreadpool_CreateWorkContext()
ws2_32.dll!WinsockThreadpool_SubmitWork()
ws2_32.dll!NSSUBJOB::TrySubmit()
ws2_32.dll!NSJOB::SubmitChild()
ws2_32.dll!NSJOB::ForEachChild()
ws2_32.dll!NSJOB::TrySubmit()
ws2_32.dll!NSMASTERJOB::TrySubmit()
ws2_32.dll!GetAddrInfoExW()
[Managed to Native Transition]
impersonate.dll!Program.Main.AnonymousMethod__0_0()
This will call OpenThreadToken(…, OpenAsSelf=false, …) which fails with ACCESS_DENIED, which ws2_32 will remap to WSATRY_AGAIN. We'll follow up with the Windows owners of ws2_32.dll to understand if this is expected or not.
Given this behavior I think we should avoid calling ws2_32's async methods when run under impersonation. Changing the area path as appropriate.
Tagging subscribers to this area: @dotnet/ncl
See info in area-owners.md if you want to be subscribed.
Issue Details
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, dotnet/runtime#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
```c#
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);
}
}
}
}
</details>
<details>
<summary>
ImpersonateTest.csproj
</summary>
```xml
<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 dotnet/runtime#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 dotnet/runtime#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.
Author: | davidsh |
---|---|
Assignees: | - |
Labels: | `area-System.Net`, `tenet-compatibility` |
Milestone: | 6.0.0 |
we should avoid calling ws2_32's async methods when run under impersonation
Call the synchronous method as fallback when the asynchronous one returns WSATRY_AGAIN
?
I will take a look. that may be good strategy if ws2_32 is fixed in the future. I can also look at how quickly this fails - the second try approach may be cheap.
Call the synchronous method as fallback when the asynchronous one returns WSATRY_AGAIN ?
I like that approach (calling the sync method asynchronously as a fallback just as we already do if the overlapped support isn't available). It's simple, avoids penalizing the common case, and automatically becomes a nop if/when the underlying issue is addressed. There are other causes of WSATRY_AGAIN, but they should be rare, and it should be inconsequential if we effectively retry once when they occur.
Most helpful comment
We'll try to have someone investigate.