2

I am having an issue calling a WCF service over net.pipe with Windows impersonation from a C# Windows service.

Background

The service reads from a queue and creates children app domains, each running a particular module per the item pulled from the queue. We call the Windows service a “JobQueueAgent” and each module a “Job”. I will use these terms going forward. A job can be configured to run as a specified user. We use impersonation inside the job’s app domain to accomplish this. The following is the flow of logic and credentials in the service:

JobQueueAgent (Windows Service – Primary User) >> Create job domain >> Job Domain (App Domain) >> Impersonate sub user >> Run job on thread with impersonation >> Job (Module – Sub User) >> Job logic

The “Primary User” and “Sub User” are both domain accounts with rights to “login as a service”.

The service runs on a virtual server running Windows Server 2012 R2.

The following is the C# impersonation code I am using:

namespace JobQueue.WindowsServices
{
    using System;
    using System.ComponentModel;
    using System.Net;
    using System.Runtime.InteropServices;
    using System.Security.Authentication;
    using System.Security.Permissions;
    using System.Security.Principal;
    internal sealed class ImpersonatedIdentity : IDisposable
    {
        [PermissionSetAttribute(SecurityAction.Demand, Name = "FullTrust")]
        public ImpersonatedIdentity(NetworkCredential credential)
        {
            if (credential == null) throw new ArgumentNullException("credential");

            if (LogonUser(credential.UserName, credential.Domain, credential.Password, 5, 0, out _handle))
            {
                _context = WindowsIdentity.Impersonate(_handle);
            }
            else
            {
                throw new AuthenticationException("Impersonation failed.", newWin32Exception(Marshal.GetLastWin32Error()));
            }
        }
        ~ImpersonatedIdentity()
        {
            Dispose();
        }
        public void Dispose()
        {
            if (_handle != IntPtr.Zero)
            {
                CloseHandle(_handle);
                _handle = IntPtr.Zero;
            }
            if (_context != null)
            {
                _context.Undo();
                _context.Dispose();
                _context = null;
            }
            GC.SuppressFinalize(this);
        }
        [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool LogonUser(string userName, string domain, string password, int logonType,int logonProvider, out IntPtr handle);

        [DllImport("kernel32.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool CloseHandle(IntPtr handle);
        private IntPtr _handle = IntPtr.Zero;
        private WindowsImpersonationContext _context;
    }
}

The Problem

Some jobs are required to make net.pipe WCF service calls to another Windows service running on the server. The net.pipe call fails when running under impersonation.

Here is the exception I get in this scenario:

Unhandled Exception: System.ComponentModel.Win32Exception: Access is denied

Server stack trace: at System.ServiceModel.Channels.AppContainerInfo.GetCurrentProcessToken() at System.ServiceModel.Channels.AppContainerInfo.RunningInAppContainer() at System.ServiceModel.Channels.AppContainerInfo.get_IsRunningInAppContainer() at System.ServiceModel.Channels.PipeSharedMemory.BuildPipeName(String pipeGuid)

The net.pipe succeeds when not running under impersonation. The net.pipe call also succeeds when the impersonated user is added to the Administrators group. This implies there is some privilege the user needs to make the call while under impersonation. We have not been able to determine what policy, privilege or access the user needs to make the net.pipe call while impersonating. It is not acceptable to make the user an administrator.

Is this a known issue? Is there a particular right the user needs to succeed? Is there a code change I can make to resolve this issue? Using WCF's net.pipe in a website with impersonate=true seems to indicate that this will not work in an ASP.NET application due to NetworkService. Not sure, but that shouldn’t apply here.

Community
  • 1
  • 1
cruikshj
  • 61
  • 7
  • You say that the net.pipe succeeds when you are not impersonating; does it succeed when running from a normal application (not a service) in the context of the user account you're trying to impersonate? In other words, are you sure that the problem is related to impersonation, and not just because the user account being impersonated lacks the necessary permissions to connect? – Harry Johnston Oct 08 '15 at 20:32
  • Also, you should try using the interactive logon type (2) rather than the service logon type (5). I don't think it will make any difference, but I'm not sure. – Harry Johnston Oct 08 '15 at 20:33
  • I can recreate the issue in a console application. Interactive logon did not work unfortunately. – cruikshj Oct 09 '15 at 13:33
  • The call succeeds when impersonation is not used. – cruikshj Oct 12 '15 at 13:45
  • However, impersonation is a requirement so the problem remains. – cruikshj Oct 13 '15 at 13:31

3 Answers3

4

With the help of Microsoft Support, I was able to resolve this issue by modifying the access rights of the thread identity (something suggested by Harry Johnston in another answer). Here is the impersonation code I am now using:

using System;
using System.ComponentModel;
using System.Net;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Security.Authentication;
using System.Security.Permissions;
using System.Security.Principal;

internal sealed class ImpersonatedIdentity : IDisposable
{
    [PermissionSet(SecurityAction.Demand, Name = "FullTrust")]
    public ImpersonatedIdentity(NetworkCredential credential)
    {
        if (credential == null) throw new ArgumentNullException(nameof(credential));

        _processIdentity = WindowsIdentity.GetCurrent();

        var tokenSecurity = new TokenSecurity(new SafeTokenHandleRef(_processIdentity.Token), AccessControlSections.Access);

        if (!LogonUser(credential.UserName, credential.Domain, credential.Password, 5, 0, out _token))
        {
            throw new AuthenticationException("Impersonation failed.", new Win32Exception(Marshal.GetLastWin32Error()));
        }

        _threadIdentity = new WindowsIdentity(_token);

        tokenSecurity.AddAccessRule(new AccessRule<TokenRights>(_threadIdentity.User, TokenRights.TOKEN_QUERY, InheritanceFlags.None, PropagationFlags.None, AccessControlType.Allow));
        tokenSecurity.ApplyChanges();

        _context = _threadIdentity.Impersonate();
    }

    ~ImpersonatedIdentity()
    {
        Dispose();
    }

    public void Dispose()
    {
        if (_processIdentity != null)
        {
            _processIdentity.Dispose();
            _processIdentity = null;
        }
        if (_token != IntPtr.Zero)
        {
            CloseHandle(_token);
            _token = IntPtr.Zero;
        }
        if (_context != null)
        {
            _context.Undo();
            _context.Dispose();
            _context = null;
        }

        GC.SuppressFinalize(this);
    }

    [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool LogonUser(string userName, string domain, string password, int logonType, int logonProvider, out IntPtr handle);

    [DllImport("kernel32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool CloseHandle(IntPtr handle);

    private WindowsIdentity _processIdentity;
    private WindowsIdentity _threadIdentity;
    private IntPtr _token = IntPtr.Zero;
    private WindowsImpersonationContext _context;


    [Flags]
    private enum TokenRights
    {
        TOKEN_QUERY = 8
    }


    private class TokenSecurity : ObjectSecurity<TokenRights>
    {
        public TokenSecurity(SafeHandle safeHandle, AccessControlSections includeSections)
            : base(false, ResourceType.KernelObject, safeHandle, includeSections)
        {
            _safeHandle = safeHandle;
        }

        public void ApplyChanges()
        {
            Persist(_safeHandle);
        }

        private readonly SafeHandle _safeHandle;
    }

    private class SafeTokenHandleRef : SafeHandle
    {
        public SafeTokenHandleRef(IntPtr handle)
            : base(IntPtr.Zero, false)
        {
            SetHandle(handle);
        }

        public override bool IsInvalid
        {
            get { return handle == IntPtr.Zero || handle == new IntPtr(-1); }
        }
        protected override bool ReleaseHandle()
        {
            throw new NotImplementedException();
        }
    }
}
cruikshj
  • 61
  • 7
1

Ah, here's the problem:

Server stack trace: at System.ServiceModel.Channels.AppContainerInfo.GetCurrentProcessToken()

When you attempt to open the pipe, the system is checking to see whether you're in an app container or not. That involves querying the process token, which the user you're impersonating doesn't have permission to do.

This seems like a bug to me. You could try opening a paid support case with Microsoft, but there's no guarantee that they will be willing to issue a hotfix or that they will be able to resolve the problem soon enough to meet your needs.

So I see two plausible workarounds:

  • Before impersonating, change the ACL on the process access token to grant TOKEN_QUERY access to the new logon token. I believe the logon token will contain a logon SID, so that would be the safest choice, but it shouldn't be too dangerous to grant access to the user account instead. To the best of my knowledge, the TOKEN_QUERY access right does not reveal any particularly sensitive information.

  • You could launch a child process in the sub-user's context instead of using impersonation. Less efficient and less convenient, but it would be a simple way of resolving the problem.

Harry Johnston
  • 35,639
  • 6
  • 68
  • 158
  • Thanks for the insight. Funny, I actually already have opened a paid support case with Microsoft. Perhaps I will get a hotfix :). I have considered the child process as a plan B, but I will try out the TOKEN_QUERY idea and see if it works. – cruikshj Oct 21 '15 at 14:32
  • @cruikshj - did you ever confirm with MSFT if this is a bug? I am encountering a similar issue except I am not doing any explicit impersonation. My client side is in an IIS web application with impersonate=true so I'm not certain that the fix you posted would work and how it would work. The code worked fine in .NET 2.0 runtime so I'm of the mind that this has to do with CAS behavior change in .NET 4.0 but as of yet, I have not been able to resolve it via workarounds listed for CAS issues. – Charles Chen May 06 '18 at 21:32
0

Looks like this is not possible without elevated (administrator) permissions.

https://social.msdn.microsoft.com/Forums/vstudio/en-US/6a26497f-0346-4929-ad42-ff4459be60e4/error-while-attempting-to-call-netpipe-wcf-service-while-impersonating?forum=wcf#6a26497f-0346-4929-ad42-ff4459be60e4

Although, the duplex contract described here may hold the key.

Client on non-admin user can't communicate using net.pipe with services

Community
  • 1
  • 1
cruikshj
  • 61
  • 7
  • The answer in that first link seems to be talking about the permissions of the WCF service host, not the permissions of the client that is attempting to contact the host. The answer in the second link says that the WCF service should be hosted in a service application, which according to your question you are already doing. So I don't think this is your problem. – Harry Johnston Oct 19 '15 at 20:57