Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WindowsIdentity S4U logon does not achieve impersonation #28460

Open
wpbrown opened this issue Jan 19, 2019 · 18 comments
Open

WindowsIdentity S4U logon does not achieve impersonation #28460

wpbrown opened this issue Jan 19, 2019 · 18 comments
Labels
area-System.Net.Http backlog-cleanup-candidate An inactive issue that has been marked for automated closure. no-recent-activity
Milestone

Comments

@wpbrown
Copy link

wpbrown commented Jan 19, 2019

Creating a WindowsIdentity with the UPN argument to logon and impersonate a user at a remote computer (Constrained delegation for 'any protocol') is working as expected in NETFX. With NETFX no special privileges are required (e.g. SeTcbPrivilege, SeImpersonatePrivilege). To ensure NETFX is not enabling these privileges, I did not grant the rights for the NETFX test.

However in COREFX, it only works when the process enables SeTcbPrivilege.

In .NET Core the failure mode depends on how the network call is being made:

  • SocketsHttpHandler: SocketException in local machine
  • HttpClientHandler: 401 Unauthorized from remote machine

Minimal Reproduction Case:

Versions: .NET Core 2.2 vs .NET Framework 4.6.1
Program: testimper
Sample Output: log

Environment

I have created an ARM template that will deploy a fully functioning AD DS environment for testing this issue here.

Notes

My guess of why the impersonation level is 'Identification' in the working NETFX test case: that is all the token is good for on the local machine. The token can be used for impersonation at the remote machine as configured in the directory.

Related:

@wpbrown
Copy link
Author

wpbrown commented Jan 19, 2019

Comparing logon events and source code for the logon it appears everything is the same:
WindowsIdentity: COREFX vs NETFX

Looking at a network capture of the HttpClientHandler exchange, no negotiate credentials are ever sent in response to 401 from the server.

**** netfx local

An account was successfully logged on.

Subject:
	Security ID:		beaglelab\xsoakproxy
	Account Name:		xsoakproxy
	Account Domain:		beaglelab
	Logon ID:		0x2FE64F

Logon Information:
	Logon Type:		3
	Restricted Admin Mode:	-
	Virtual Account:		No
	Elevated Token:		No

Impersonation Level:		Identification

New Logon:
	Security ID:		beaglelab\user1
	Account Name:		user1
	Account Domain:		beaglelab
	Logon ID:		0x30F426
	Linked Logon ID:		0x0
	Network Account Name:	-
	Network Account Domain:	-
	Logon GUID:		{95a8a6a4-02d8-6a34-f968-5446f5592c98}

Process Information:
	Process ID:		0x33c
	Process Name:		\Device\Mup\appserv\lab\impernet\testimper-netfx.exe

Network Information:
	Workstation Name:	oakproxyserv
	Source Network Address:	-
	Source Port:		-

Detailed Authentication Information:
	Logon Process:		C
	Authentication Package:	Kerberos
	Transited Services:	-
	Package Name (NTLM only):	-
	Key Length:		0


**** netfx remote 

An account was successfully logged on.

Subject:
	Security ID:		NULL SID
	Account Name:		-
	Account Domain:		-
	Logon ID:		0x0

Logon Information:
	Logon Type:		3
	Restricted Admin Mode:	-
	Virtual Account:		No
	Elevated Token:		No

Impersonation Level:		Impersonation

New Logon:
	Security ID:		beaglelab\user1
	Account Name:		user1@beaglelab
	Account Domain:		CORP.BEAGLELAB.SPACE
	Logon ID:		0x15BBCC
	Linked Logon ID:		0x0
	Network Account Name:	-
	Network Account Domain:	-
	Logon GUID:		{5858335f-babf-c1ff-423c-a4643c42a291}

Process Information:
	Process ID:		0x0
	Process Name:		-

Network Information:
	Workstation Name:	-
	Source Network Address:	10.0.0.12
	Source Port:		49777

Detailed Authentication Information:
	Logon Process:		Kerberos
	Authentication Package:	Kerberos
	Transited Services:	
		xsoakproxy@CORP.BEAGLELAB.SPACE
	Package Name (NTLM only):	-
	Key Length:		0

**** netcore local

An account was successfully logged on.

Subject:
	Security ID:		beaglelab\xsoakproxy
	Account Name:		xsoakproxy
	Account Domain:		beaglelab
	Logon ID:		0x2FE64F

Logon Information:
	Logon Type:		3
	Restricted Admin Mode:	-
	Virtual Account:		No
	Elevated Token:		No

Impersonation Level:		Identification

New Logon:
	Security ID:		beaglelab\user1
	Account Name:		user1
	Account Domain:		beaglelab
	Logon ID:		0x494103
	Linked Logon ID:		0x0
	Network Account Name:	-
	Network Account Domain:	-
	Logon GUID:		{88242233-7b64-332c-5940-737cfedd4cef}

Process Information:
	Process ID:		0xd20
	Process Name:		\Device\Mup\appserv\lab\impercore\testimper-corefx.exe

Network Information:
	Workstation Name:	oakproxyserv
	Source Network Address:	-
	Source Port:		-

Detailed Authentication Information:
	Logon Process:		CLR
	Authentication Package:	Kerberos
	Transited Services:	-
	Package Name (NTLM only):	-
	Key Length:		0


**** netcore remote

No Logon Events.

@davidsh
Copy link
Contributor

davidsh commented Jan 19, 2019

Looking at a network capture of the HttpClientHandler exchange, no negotiate credentials are ever sent in response to 401 from the server.

What version of .NET Core are you running? There were several different bugs regarding Windows auth in HttpClient API that were fixed in servicing releases.

Please show "dotnet --info" output. And also, try latest .NET Core 2.1 or 2.2.

@wpbrown
Copy link
Author

wpbrown commented Jan 19, 2019

It's the latest release I see listed on corefx repo:

Z:\impercore>dotnet --info
.NET Core SDK (reflecting any global.json):
 Version:   2.2.102
 Commit:    96ff75a873

Runtime Environment:
 OS Name:     Windows
 OS Version:  10.0.14393
 OS Platform: Windows
 RID:         win10-x64
 Base Path:   C:\Program Files\dotnet\sdk\2.2.102\

Host (useful for support):
  Version: 2.2.1
  Commit:  878dd11e62

.NET Core SDKs installed:
  2.1.503 [C:\Program Files\dotnet\sdk]
  2.2.102 [C:\Program Files\dotnet\sdk]

.NET Core runtimes installed:
  Microsoft.AspNetCore.All 2.1.7 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.All 2.2.1 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.App 2.1.7 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 2.2.1 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 2.1.7 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.2.1 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]

@wpbrown
Copy link
Author

wpbrown commented Jan 30, 2019

More research:

HttpClientHandler (WinHTTP)
I've debugged this scenario. The COREFX code is doing everything correctly. It is handling the challenge and retrying with WINHTTP_AUTOLOGON_SECURITY_LEVEL_LOW. This appears to be a limitation in WinHTTP with impersonation. It will send the credentials with the thread access token level is Impersonate, but not Identify (as is the case for S4U2Self logon).

SocketsHttpHandler
It turns out that DNS resolution under an Identify thread access token level also fails in .NET Framework. My test above misses that because .NET Framework is using a cached lookup from the preceding non-impersonated call. In .NET Framework I can work around this for my use case by pre-resolving the hostname and putting the IP in the request. .NET Framework uses the host header for calculating the SPN so the authentication still works. This workaround doesn't work for SocketsHttpHandler because it uses request URI (which contains an IP) to calculate the SPN. The authentication then fails with 'no credential found in security package'.

I'll have to modify COREFX to see if matching the .NET framework SPN behavior will get the SocketsHttpHandler working.

@davidsh
Copy link
Contributor

davidsh commented Jan 30, 2019

.NET Framework uses the host header for calculating the SPN so the authentication still works. This workaround doesn't work for SocketsHttpHandler because it uses request URI (which contains an IP) to calculate the SPN. The authentication then fails with 'no credential found in security package'.

We have an issue in CoreFx repo about this. SocketsHttpHandler should be able to use the 'Host' request header for the default SPN calculation instead of the authority portion of the Uri itself. In fact, .NET Framework goes even further by providing an AuthenticationManager class and CustomTargetDictionary for SPN lookup. We currently don't have in .NET Core.

@geoffkizer @mconnew

@davidsh
Copy link
Contributor

davidsh commented Jan 30, 2019

This appears to be a limitation in WinHTTP with impersonation.

According to the documentation, WinHTTP is supposed to do impersonation.

See: https://docs.microsoft.com/en-us/windows/desktop/WinHttp/winhttp-vs-wininet

image
image

I would suggest following up with the WinHTTP team about that.

@wpbrown
Copy link
Author

wpbrown commented Jan 30, 2019

Thanks for the info @davidsh. WinHTTP is working with impersonation when the access token level is Impersonation or Delegation which is most likely what they are referring to in the documentation. I'm using a very specialized scenario of S4U2Self logon without SeTCB privilege which only provides an Identification level token on the local system. I'm going to focus on getting the SocketsHttpHandler approach to work since that is the default and preferred handler in .NET Core now.

I believe #25320 is the issue regarding the SPN calculation.

@wpbrown
Copy link
Author

wpbrown commented Feb 4, 2019

As a test I patched corefx SocketsHttpHandler to use the host header for SPN calculation. I was then able to complete requests using the same DNS workaround I used for .NET Framework (pre-resolving it and using IP in the request URI).

So to summarize: the use case is making HTTP requests under an identify level access token. The case arises from using an S4U logon to acheive constrained delegation to network services while retaining least privilege on the host computer. If #25320 is addressed, then you can achieve functional parity with .NET Framework and CoreFX by using SocketsHttpHandler. It is not possible under the older (now non-default) WinHTTP-based handler due to limitations of the underlying WinHTTP library. This is probably fine.

While it would be possible to achieve parity with .NET Framework, it's still very limited support for S4U. The DNS workaround only works in my scenario because I'm making a single request and not allowing redirects. Anything triggering DNS resolution would fail. What I'm going to do for now is carry a patch to CoreFX (and deploy my app self contained) that allows me to narrow the impersonation context down to the smallest segement of code possible. Essentially you just need S4U impersonation when you acquire the SSPI Kerberos package credential handle. I know this patch is not reasonable to be merged and I'm not sure the best way to frame it as a mergeable feature, but I'm curious if anyone has any thoughts. I considered for example, a new ICredentials implementation for S4U that the handler would have to specialize for.

@davidsh
Copy link
Contributor

davidsh commented Mar 22, 2019

I looked at the .NET Framework code and how we call the Negotiate SSPI. It turns out that in .NET Framework, the WebRequest/HttpWebRequest (and thus HttpClient) objects use a default of these attributes:

m_ImpersonationLevel = TokenImpersonationLevel.Delegation;
m_AuthenticationLevel= AuthenticationLevel.MutualAuthRequested;

These default values thus cause a set of corresponding SSPI flags to be passed in. It seems the MutualAuth one happens anyways by default. But the Delegation flag is never being set when call the Negotiate SSPI.

So that is why this is probably working on .NET Framework but not on .NET Core.

@davidsh davidsh self-assigned this Mar 22, 2019
@wpbrown
Copy link
Author

wpbrown commented Apr 3, 2019

hey @davidsh. my S4U impersonation scenario above actually works. the problem for me is there is no first class support for using S4U, so it takes numerous hacks/work arounds to get the SocketsHttpHandler working under an Identify token.

It turns out that in .NET Framework, the WebRequest/HttpWebRequest (and thus HttpClient) objects use a default of these attributes:

These arguments to configure SSPI are important to a client trying to pass a token to a service, but not necessarily important to the middleware service (unless it calls yet another middleware). You're correct that SocketsHttpHandler doesn't configure them. The reason you've probably not heard many complaints is that these are only required to pass a token to a service that uses unconstrained delegation. I'm guessing not many folks in their right mind are using unconstrained delegation anymore, especially not people who are able to use .NET Core. Also, they have WinHttpHandler as a work around option.

Calling test middleware that uses unconstrained delegation to reach backend (SocketsHttpHandler fails because it doesn't setup SSPI to acquire a token with ok_as_delegate):

Calling middleware...
********************* SocketsHttpHandler...
{"IncomingUser":{"Message":"Hello","Name":"beaglelab\\user1","IsAuthenticated":true,"AuthenticationType":"Negotiate","ImpersonationLevel":"Impersonation"},"BackendUser":{"error":"System.Net.WebException: The remote server returned an error: (401) Unauthorized.\r\n   at System.Net.HttpWebRequest.GetResponse()\r\n   at testapp.Controllers.HomeController.Api() in C:\\oakproxy\\testenv\\testapp\\testappmid\\Controllers\\HomeController.cs:line 42"}}
********************* WinHttpHandler...
{"IncomingUser":{"Message":"Hello","Name":"beaglelab\\user1","IsAuthenticated":true,"AuthenticationType":"Negotiate","ImpersonationLevel":"Delegation"},"BackendUser":{"Message":"Hello","Name":"beaglelab\\user1","IsAuthenticated":true,"AuthenticationType":"Negotiate","ImpersonationLevel":"Impersonation"}}
Finished.

Calling test middleware that uses constrained delegation to reach backend (both work fine):

Calling middleware...
********************* SocketsHttpHandler...
{"IncomingUser":{"Message":"Hello","Name":"beaglelab\\user1","IsAuthenticated":true,"AuthenticationType":"Negotiate","ImpersonationLevel":"Impersonation"},"BackendUser":{"Message":"Hello","Name":"beaglelab\\user1","IsAuthenticated":true,"AuthenticationType":"Negotiate","ImpersonationLevel":"Impersonation"}}
********************* WinHttpHandler...
{"IncomingUser":{"Message":"Hello","Name":"beaglelab\\user1","IsAuthenticated":true,"AuthenticationType":"Negotiate","ImpersonationLevel":"Impersonation"},"BackendUser":{"Message":"Hello","Name":"beaglelab\\user1","IsAuthenticated":true,"AuthenticationType":"Negotiate","ImpersonationLevel":"Impersonation"}}
Finished.

davidsh referenced this issue in davidsh/corefx Jun 11, 2019
Fixed SocketsHttpHandler so that it will use the request's Host header,
if present, as part of building the Service Principal Name (SPN) when
doing Kerberos authentication. This now matches .NET Framework behavior.

Contributes to #34697 and #27745
davidsh referenced this issue in dotnet/corefx Jun 12, 2019
Fixed SocketsHttpHandler so that it will use the request's Host header,
if present, as part of building the Service Principal Name (SPN) when
doing Kerberos authentication. This now matches .NET Framework behavior.

Contributes to #34697 and #27745
@davidsh
Copy link
Contributor

davidsh commented Jun 17, 2019

@wpbrown

We recently merged PR dotnet/corefx#38465 which allows SocketsHttpHandler to use a specified 'Host:' request header as part of the SPN calculation. This PR change is available for .NET Core 3.0.

As you mentioned above, .NET Framework is only able to achieve this scenario in cases where the DNS name is pre-resolved and cached. In general, both .NET Framework and .NET Core have similar lack of first-class support for S4U and related identify tokens in this scenario.

Due to the upcoming timeline for .NET Core 3.0, moving this issue to Future.

@wpbrown
Copy link
Author

wpbrown commented Jun 18, 2019

@davidsh Thanks so much for the update. I currently build my project with a patched CoreFX2.x to hack in first class S4U2Self support to SocketsHttpHandler.

In my super specialized case (I don't need to handle redirects) I can probably now get away with pre-resolving the IP (along with a few other permission work-arounds) with stock CoreFX3.0 build, like i did with NetFX.

However I've been mulling over for sometime what would be a reasonable non-API breaking change to turn my hack in to a real .NET feature. The best approach I've thought of is a new ICredentials implementation "S4uCredentials" or some better name. The only constructor argument is a UPN. APIs taking ICredentials can add support for it over time. The first would SocketsHttpHandler. It can check is the credential type is S4uCredentials and use it to impersonate just the SSPI credential call (as I do in my hack.).

This could actually be supported on Linux easier than Windows. On Windows you must talk to LSA with LogonUser and then use SSPI under that token. On Linux there is an actual GSSAPI call specifically for S4U2Self.

Do you have any thoughts on this design be accepted?

@davidsh
Copy link
Contributor

davidsh commented Jun 20, 2019

Do you have any thoughts on this design be accepted?

Thanks for the design ideas! Generally speaking, it sounds like a nice simple design that could support this scenario. In terms of it being "accepted" in CoreFx, there are two parts to it. One is an API review if a new type (i.e. S4UCredentials) is being introduced. That would need to go through our API review process.

The second is the design/implementation/test of this. That would be in the form of a PR that could be reviewed.

We welcome contributions. From a timing perspective, I think a new API/feature won't make it into .NET Core 3.0. But it certainly could for the next release, .NET 5.

@mconnew
Copy link
Member

mconnew commented Jun 20, 2019

It seems like a simpler solution with no API changes may be possible. I'm going to summarize my understanding in case I've got something wrong. When you use SocketsHttpHandler under an impersonated identity, it fails to do the DNS lookup as the current identity is preventing it somehow (which seems odd as DNS is unauthenticated, but apparently the same happens on .NET Framework so I'm just going to accept that as it is). The solution being proposed is that a new type of credential is passed in which basically wraps the identity token and SocketsHttpHandler then uses that for impersonation while creating the NTAuthentication object that's used for the Negotiate auth.
This seems backwards to me. It would seem to me that the correct solution is to make SocketsHttpHandler function correctly under impersonation. If DNS might not be possible while impersonated, then un-impersonate while the DNS lookup happens. This could either be done in the DNS code or in SocketsHttpHandler. I believe code similar to this should work:

if (!WindowsIdentity.GetCurrent().ImpersonationLevel == TokenImpersonationLevel.None)
{
  WindowsIdentity.RunImpersonated(WindowsIdentity.GetAnonymous().AccessToken, 
    () => 
    { 
      DoDnsLookup(); 
    });
}
else
{
  DoDnsLookup();
}

That should drop the impersonated identity to successfully do the DNS lookup, then restore it again to continue execution and wouldn't require any new API's. If the impersonation is handed off to SocketsHttpHandler as a solution to the DNS problem, what do you do when you need to impersonate the call because the HttpContent isn't a static buffer and needs the impersonation when building the request body?
Another thing which feels dirty about the S4UCredentials solution is the need to drop impersonation to make the call. For example, if you have an ASP.NET Core page which does 5 things in the process of building the response and they all need to be done under impersonation (reading a file off local disk or making a SQL query for example). One of those steps is an HTTP request, but now you need to end your impersonation, make your HTTP request, then re-impersonate to complete the rest of the request.

@davidsh
Copy link
Contributor

davidsh commented Jun 20, 2019

If DNS might not be possible while impersonated

It's possible the DNS problem is due to this issue on .NET Core dotnet/corefx#38646

@wpbrown
Copy link
Author

wpbrown commented Jun 20, 2019

@mconnew So the problem with that approach is that this goes beyond the DNS resolution. The reason this is complicated even in NETFX is it's a core Windows OS issue. In the case of S4U2Self impersonation (aka Constrained Delegation Any Protocol in AD DS UI speak), if the calling process does not have SeTcbPrivilege*, then LogonUser API will give you a thread impersonation token with SECURITY_IMPERSONATION_LEVEL == SecurityIdentification. This is an extremely constrained access level that can't open files or registry entries. So all kinds of stuff breaks, DNS is just one of the things I wasn't able to work around in .NET Core. I had to put in a ton of workarounds just to get 1 simple HTTP request to run under this token even with NETFX. I had to preload assemblies, because the framework can't touch the filesystem to load assemblies. I had to disable all proxy capability because it can't read proxy settings from the registry. Windows/Win32 API was never really designed for anything to be done with this token other than ACL checks. It's an undocumented historical oddity (or a good Raymond Chen question) as far as I can tell that one of the things you can do with this token is use the SSPI API to bootstrap an S4U2Self Kerberos ticket.

If the impersonation is handed off to SocketsHttpHandler as a solution to the DNS problem, what do you do when you need to impersonate the call because the HttpContent isn't a static buffer and needs the impersonation when building the request body

I think a positive aspect of the S4UCredential approach is it doesn't interfere with any local impersonation or other stuff you may doing around the SocketsHttpHandler, within a stream or message handler or whatever. It completely isolates the impersonation to 1 Win32 API call that establishes the remote identity, which is really what the Credential property of SocketsHttpHandler is all about. The thing with S4U2Self, it's a Kerberos thing (created by Microsoft, but the AD people not the Windows people). You don't want or need to be impersonating the user on the local machine. The only goal is to impersonate the user on the remote machine. The local impersonation is purely an artifact of the LSA and SSPI API design in Windows. It would make more sense if the pszPrincipal argument of AcquireCredentialsHandle** worked for S4U2Self and no impersonation was needed. That would be more in line with how GSSAPI on Linux does it. In Linux S4U2Self is purely a Kerberos thing. In the entire exchange, the only Window thread impersonation actually necessary is during the call to AcquireCredentialsHandle which is happening via the NTAuthentication constructor.

So... rather than try make the entire SocketsHttpHandler stack work under an Identification token, which is pretty much impossible, I'm hoping to some how introduce an elegant API to scope the impersonation down to that one call. S4UCredential seemed nice because it doesn't change the public API of SocketsHttpHandler at all. Setting an S4UCredential on SocketsHttpHandler.Credentials seems more elegant and straight forward than using impersonation + setting Credentials to CredentialCache.DefaultNetworkCredentials.

@davidsh
I peeked at that issue dotnet/corefx#38646. You're hitting some of the same things I was back in January above in this issue. I spent a lot of time comparing the NETFX reference source to COREFX. Some of the differences I thought were NETFX working better, were as you discovered, the DNS caching in NETFX.

* Essentially you are running as SYSTEM. It allows you to arbitrarily impersonate any SID within the bounds of the local machine.
** I don't mean to link the CredSSP package flavor. The move of the Windows OS docs to the new docs.microsoft.com seems to have broken these multi-flavor function docs. You can't access the Kerberos version of this doc now.

@mconnew
Copy link
Member

mconnew commented Jun 20, 2019

@wpbrown, I understand now and agree about not trying to make SocketsHttpHandler work in the face of a "hostile" identity. But I do have another suggestion for you. As this won't be going into .NET Core 3.0 but would have to wait until 3.1, there is an open issue (#36896) delayed until 3.1 which I expect will get done due to a dependency in ASP.NET Core which could enable an alternative solution.
The model for HttpClient is designed be able to layer HttpMessageHandler instances. You could create a new S4UMessageHandler which derives from DelegatingHandler which stacks on top of SocketsHttpHandler. Once the NTAuthentication api's have been exposed publicly, you could handle the challenge handshake outside of SocketsHttpHandler which knows nothing about any credentials. ASP.NET Core is using the existing NTAuthentication api's via reflection as a temporary solution (hence why I said I expect that other issue to get done) here. You could prototype it today using the reflection approach that ASP.NET Core is using.

Copy link
Contributor

Due to lack of recent activity, this issue has been marked as a candidate for backlog cleanup. It will be closed if no further activity occurs within 14 more days. Any new comment (by anyone, not necessarily the author) will undo this process.

This process is part of our issue cleanup automation.

@dotnet-policy-service dotnet-policy-service bot added backlog-cleanup-candidate An inactive issue that has been marked for automated closure. no-recent-activity labels Dec 27, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-System.Net.Http backlog-cleanup-candidate An inactive issue that has been marked for automated closure. no-recent-activity
Projects
None yet
Development

No branches or pull requests

4 participants