Security Briefs

Using Protocol Transition—Tips from the Trenches

Keith Brown

Code download available at: Security Briefs 2007_01.exe(172 KB)

Contents

A Dilemma of Privilege
Factoring Out High Privilege
Kernel Objects and Handles
Kernel Objects and ACLs
A Sample Gateway
Security Considerations

I covered protocol transition and constrained delegation in detail in a previous Security Briefs column, where I dove in deep and looked at the underlying S4U Kerberos extensions that actually make it work. For more of a layman's introduction to protocol transition, check out Item 63 from my book, The .NET Developer's Guide to Windows Security.

Now that Windows Server® 2003 is more widely deployed, I've been getting mail from readers who are trying to use protocol transition to build secure gateways into their intranets. It's a great feature when you're looking to wire up some alternate form of authentication on the front end but would like to reap the benefits of using Kerberos on the back end.

With protocol transition, a gateway (such as a Web portal) can authenticate the user with any technique that makes sense, and then establish a logon for a Windows® account, which can be impersonated and used to access resources on servers. This means you can use all of the built-in user security features in Windows on the back end, including groups, access control lists (ACLs), Authorization Manager (AzMan) roles, COM+ roles, and so on.

While mapping to Windows accounts has been a common feature of many gateways-for example, IIS supports mapping certificates onto user accounts-protocol transition offers the ability to do this without forcing the gateway to store passwords for all the accounts it uses. Normally you need a password to establish a logon session, but with protocol transition, you can bestow trust on a gateway to establish a logon session without knowing the corresponding password. The resulting session will be limited in its reach around the network: Kerberos tickets will only be issued to this session for services listed in the gateway's allowed-to-delegate-to (A2D2) list.

Consider the breach of a gateway that stores passwords versus one that uses protocol transition. When an attacker gains access to a user's password, he can impersonate that user anywhere on the network, for as long as he wants, at least until the user changes her password. But if an attacker compromises a gateway that's trusted for protocol transition, there are no passwords to find there. Sure, he can log on all users whose accounts aren't marked "Sensitive and cannot be delegated," but he will only be able to use those credentials to access services on the A2D2 list. The damage will be constrained in space and time: you can quickly shut down the attack once it's discovered, whereas an attacker who has gained access to user passwords will likely have a much easier time continuing his attack from a safe location.

The way you bestow this sort of trust on a gateway is to grant the security principal for the gateway process permission to use protocol transition, and list the specific services to which it may delegate. Figure 1 shows an example from the lab environment in which I tested the code for this column. This is the Delegation property page for a user account called MyWebIdentity, which is an otherwise low-privileged account assigned to an IIS 6.0 application pool where my sample Web portal runs. As you can see, MyWebIdentity is configured for protocol transition because I've selected the option "Trust this user for delegation to specified services only" and "Use any authentication protocol." Note that I've used the A2D2 list to constrain MyWebIdentity so that the only Kerberos tickets it can obtain via protocol transition will be for the file system on a machine in the domain called FileServer. CIFS stands for the Common Internet File System, the name of the Microsoft® file server.

Figure 1 Configuring an Account for Protocol Transition

Figure 1** Configuring an Account for Protocol Transition **(Click the image for a larger view)

By the way, you may be wondering how I got the Delegation tab to show up on a normal user account. That's because I assigned a service principal name (SPN) to the user account, which tells Active Directory® that it's designed to be used for a service, not a human. The Delegation tab will only show up for security principals that have at least one SPN assigned, which is why you normally only see it on computer accounts. If you're not sure what an SPN is, please consult "What is a Service Principal Name".

A Dilemma of Privilege

While protocol transition is a really neat feature, the basic ability to establish a logon session for a user without knowing her password is scary. Technically you don't even need to have been granted permission to do protocol transition to do this; anyone can use the S4U2Self Kerberos extension to establish a logon for a user under Windows Server 2003. You won't be able to talk to any other servers unless you've been granted permission to use protocol transition, but what's to stop a normal user from elevating her privileges with code that uses the S4U logon constructor of WindowsIdentity that I discussed in my previous column on S4U Kerberos extensions?

string admin = "administrator@mydomain.local";
using (WindowsIdentity id = new WindowsIdentity(admin)) // S4U logon
using (WindowsImpersonationContext ctx = id.Impersonate()) {
    string SAM = @"c:\windows\repair\sam";
    Console.WriteLine(File.ReadAllText(SAM));
}

Here she's attacking the security account database on the local machine by temporarily upgrading her privileges to the level of domain administrator.

Clearly, this would be bad were it possible. But it's not. You see, while the attacker will be able to successfully execute the first few lines of the previous code, the security token she receives when she performs an S4U logon will be very weak and will not support impersonation. Her call to File.ReadAllText will fail because the token she received for the administrator's account can only be used to identify the administrator, not to impersonate him.

Only a highly privileged user can use the S4U logon constructor on WindowsIdentity to get a token that can be successfully used with impersonation. Under the covers, the Win32® API that this constructor calls, LsaLogonUser, checks whether the calling process's identity has SeTcbPrivilege, which SYSTEM has by default. This is the most powerful privilege in Windows, as it indicates that you are effectively part of the operating system itself, or the "trusted computing base," which is where the acronym TCB comes from. If you're already that trusted, there's nothing you can't do anyway, and the security system will then issue S4U logons that you can impersonate.

This is where my readers started running into trouble. They wanted to use protocol transition, but quickly found that unless their gateway processes were running as SYSTEM, any logons they established via this feature were useless. They couldn't impersonate them to get to back-end resources. Some people went ahead and elevated their gateways to run as SYSTEM and just felt bad about doing it, while others refused and sought other, more traditional solutions.

Factoring Out High Privilege

This dilemma can be solved by factoring the gateway process in half. There's literally only a single line of code that needs the TCB privilege:

new WindowsIdentity(userPrincipalName)

It's reckless to elevate the privilege level of the entire gateway just because this one line of code needs high privilege. Any time you have a process that accepts input from remote users, runs all the time, and runs with high privilege, you've got a dangerous situation. The solution is to remove that one line of code from the gateway program entirely, and move it into a high-privileged helper process. This is not difficult to do if you're a seasoned Win32 systems developer, but if you're new to the platform or Win32, this might just be a showstopper. So in the remainder of this column, I'm going to show you how to build a secure logon service that hosts this one line of highly sensitive code. Along the way you will learn lots of different things, like how to pass tokens around between processes using the Microsoft .NET Framework, how to extend System.Security.AccessControl, and how to use COM+ to host highly privileged processes.

Kernel Objects and Handles

Each Windows process has a handle table that references Windows kernel objects such as files, semaphores, tokens, and so on. Win32 handles are integers that index into this table, and as such, they are only meaningful in the process that opened the handle. You can't write the value of a file handle onto the clipboard, for example, and expect a different process to be able to read that value and start using it as a handle.

If you want to transfer an open handle to another process, you must use the Win32 DuplicateHandle API. Since the WindowsIdentity that the logon service is going to obtain wraps one of these handles, serializing it for communication isn't really an option. You've got to use DuplicateHandle to insert an entry into the target process's handle table.

So here's the plan. The gateway service is going to make a call out to the logon service, using some interprocess communication technique that is easy to secure. As part of this call, the gateway will supply two arguments: the name of the user to be logged on and the process ID of the gateway's process so that the logon service knows in which direction to push the resulting token handle. The logon service will then return the integer handle value that represents the index into the caller's handle table. The caller can then use this value as a Win32 handle and construct its own WindowsIdentity to wrap around the handle.

Here's a .NET interface that represents this contract:

public interface ILogonService {
    int LogonUserViaProtocolTransition(string upn, int callerPID);
}

To help you decide how to best implement this interface, consider the following requirements. First, the process that hosts this logon service must run with the TCB privilege, and the best way to do that is to run as SYSTEM. Second, the ILogonService interface must be carefully secured so that only the designated gateway can use it. Otherwise, it'll act as a privilege escalation lever that will enable any user to elevate privilege by logging on a domain account with local admin rights. Happily, there exists a technology that solves both of these problems easily, and it's built into Windows Server 2003: our good friend COM+.

Did I hear someone say COM+ was dead? Far from it! In my opinion, COM+ is the only way to fly when you're factoring out privilege in this way. Using System.EnterpriseServices, you can write a COM+ component in the managed language of your choice. Then you can leverage the COM+ hosting model to automatically host your component in a service that runs as SYSTEM. Plus, you don't have to write a single line of code for that service. Oh and don't forget the built-in role-based security in COM+, which will allow you to restrict access to the logon method on your component so that only the security principal who is authorized to run your gateway will be allowed to use the logon service.

After building the LogonService assembly, I popped it into the COM+ catalog by running RegSvcs.exe, opened up the Component Services admin tool and found the COM+ server package for the LogonService. To make this into a service that runs as SYSTEM, all I had to do was bring up the property sheet for this package, go to the Activation tab, and check the box that says, "Run application as an NT Service." After doing this, I flipped back to the Identity tab where the "Local System" identity option had now become available. By selecting that option, I'd finished configuring my logon service to run as SYSTEM.

Kernel Objects and ACLs

Another thing to keep in mind about Windows kernel objects is that each is secured with an access control list (ACL), including the token you'll be sending back. Technically, since you're going to be duplicating a handle back to the caller, the ACL shouldn't matter as long as the caller only uses that particular handle. But it's wise to adjust the ACL on the token to ensure that the caller's security principal has permission to use it, especially given that the caller is probably going to be using .NET wrappers like WindowsIdentity to manipulate the token handle. You might end up with a caller being denied permission to use the token you've given him.

Unfortunately, the .NET Framework doesn't yet have a class that allows you to manipulate tokens directly. WindowsIdentity is the closest thing to a Token class in the framework today. But I took this opportunity to extend the classes in System.Security.AccessControl to support reading and writing ACLs on tokens. I know I've said this in the past, but my hat is off to the folks on the Windows team who developed the AccessControl namespace. It was surprisingly easy to extend their NativeObjectSecurity to support tokens. My implementation of TokenSecurity can be found in Figure 2. (Full code is available in the download for this column.) It may look like a lot of code, but if you look closely, all it's doing is customizing the ACL for a token. The most interesting code here is the TokenRights enumeration; all the rest of the code is boilerplate.

Figure 2 Extending AccessControl to Support Token Security

public enum TokenRights : int {
    AssignPrimary       = 0x0001,
    Duplicate           = 0x0002,
    Impersonate         = 0x0004,
    Query               = 0x0008,
    QuerySource         = 0x0010,
    AdjustPrivileges    = 0x0020,
    AdjustGroups        = 0x0040,
    AdjustDefault       = 0x0080,
    AdjustSessionId     = 0x0100,
    Read            = 0x00020008,
    Write           = 0x000200E0,
    Execute         = 0x00020000,
    All             = 0x000F00FF, // TOKEN_ALL_ACCESS_P
    AllPlusSessionId= 0x000F01FF, // TOKEN_ALL_ACCESS
    MaximumAllowed  = 0x02000000,
    AccessSystemSecurity = 0x01000000,
}

public class TokenAccessRule : AccessRule {
    public TokenAccessRule(IdentityReference identity,
        TokenRights tokenRights, AccessControlType aceType)
        : base(identity, (int)tokenRights,
            false, InheritanceFlags.None,
            PropagationFlags.None, aceType) {
    }
    public TokenRights TokenRights {
        get { return (TokenRights)base.AccessMask; }
    }
}

public class TokenAuditRule : AuditRule {
    public TokenAuditRule(IdentityReference identity, 
        TokenRights tokenRights, AuditFlags auditFlags)
        : base(identity, (int)tokenRights, 
            false, InheritanceFlags.None, 
            PropagationFlags.None, auditFlags) { 
    }
    public TokenRights TokenRights {
        get { return (TokenRights)base.AccessMask; }
    }
}

public class TokenSecurity : NativeObjectSecurity {
    public TokenSecurity()
        : base(false, ResourceType.KernelObject) {
    }
    public TokenSecurity(SafeTokenHandle token)
        : this(token, 
               AccessControlSections.Owner |
               AccessControlSections.Group |
               AccessControlSections.Access) {
    }
    public TokenSecurity(SafeTokenHandle token,
        AccessControlSections includeSections)
        : base(false, ResourceType.KernelObject,
            token, includeSections) {
    }
    public override Type AccessRightType {
        get { return typeof(TokenRights); }
    }
    public override Type AccessRuleType {
        get { return typeof(TokenAccessRule); }
    }
    public override Type AuditRuleType {
        get { return typeof(TokenAuditRule); } 
    }
    public override AccessRule AccessRuleFactory(
        IdentityReference identity, int accessMask,
        bool isInherited, InheritanceFlags inheritanceFlags,
        PropagationFlags propagationFlags,
        AccessControlType aceType) {

        return new TokenAccessRule(identity,
            (TokenRights)accessMask, aceType);
    }
    public override AuditRule AuditRuleFactory(
        IdentityReference identity, int accessMask,
        bool isInherited, InheritanceFlags inheritanceFlags,
        PropagationFlags propagationFlags, AuditFlags auditFlags) {

        return new TokenAuditRule(identity, 
            (TokenRights)accessMask, auditFlags);
    }
    public void AddAccessRule(TokenAccessRule rule) {
        base.AddAccessRule(rule);
    }
    public void AddAuditRule(TokenAuditRule rule) {
        base.AddAuditRule(rule);
    }
    public void Persist(SafeTokenHandle token,
        AccessControlSections includeSections) {
        base.Persist(token, includeSections);
    }
}

The implementation of the logon service is shown in Figure 3. The first thing you'll probably notice are the System.EnterpriseServices attributes that prepare the LogonService assembly and class for hosting in the COM+ catalog. You should pay special attention to the security attributes that require callers to be authenticated and to be in a particular role to call the logon service. (I used a role called ProtocolTransitionUsers, and I ensured that the security principal under which my gateway runs was a member of this role when I deployed the logon service and configured it in the COM+ catalog.)

Figure 3 The Logon Service

[assembly: ApplicationName("PluralsightLogonService")]
[assembly: ApplicationActivation(ActivationOption.Server)]
[assembly: ApplicationAccessControl(true,
  Authentication = AuthenticationOption.Privacy,
  ImpersonationLevel = ImpersonationLevelOption.Identify,
  AccessChecksLevel = AccessChecksLevelOption.ApplicationComponent)]

 public interface ILogonService {
    int LogonUserViaProtocolTransition(string upn, int callerPID);
}

[ComponentAccessControl(true)]
[SecureMethod]
public class LogonService : ServicedComponent, ILogonService {
    public LogonService() { }

    [SecurityRole("ProtocolTransitionUsers")]
    [SecurityRole("Marshaler")]
    public int LogonUserViaProtocolTransition(
        string upn, int callerPID) {

        SecurityIdentifier callerSID = null;
        CoImpersonateClient();
        try {
            callerSID = WindowsIdentity.GetCurrent().User;
        }
        finally {
            CoRevertToSelf();
        }
        
        using (SafeProcessHandle targetProcess = OpenProcess(
                    PROCESS_DUP_HANDLE, false, callerPID)) {
            if (targetProcess.IsInvalid) {
                Marshal.ThrowExceptionForHR(
                    Marshal.GetLastWin32Error());
            }

            // this is the line of code that requires high privilege
            using (WindowsIdentity identity =
                new WindowsIdentity(upn)) {

                SafeTokenHandle token = new SafeTokenHandle(
                    identity.Token, false);

                throwIfUserIsAdmin(token);
                grantPermission(token, callerSID);

                // push the token handle into the caller's process
                int targetHandle;
                if (!DuplicateHandle(
                        GetCurrentProcess(), token,      // source
                        targetProcess, out targetHandle, // target
                        0, false, DUPLICATE_SAME_ACCESS)) {
                    Marshal.ThrowExceptionForHR(
                        Marshal.GetLastWin32Error());
                }
                return targetHandle;
            }
        }
    }
    
    private void grantPermission(SafeTokenHandle token,
                                 SecurityIdentifier callerSID) {
        // use our extension to System.Security.AccessControl!
        TokenSecurity tokenSecurity = new TokenSecurity(token,
            AccessControlSections.Access);
        tokenSecurity.AddAccessRule(new TokenAccessRule(callerSID,
            TokenRights.All, AccessControlType.Allow));
        tokenSecurity.Persist(token, AccessControlSections.Access);
    }

    //Pinvoke stubs
    ...
}

You may also notice the calls to CoImpersonateClient and CoRevertToSelf. These are Win32 APIs that most DCOM programmers are familiar with. They allow me to discover which security principal is calling the logon service, and I capture the security identifier for the caller into a variable which I later use to grant permission on the token.

The rest of the code consists of logging the user on via the S4U logon constructor on WindowsIdentity, followed by duplicating the token back into the caller's process. Fortunately since the logon service will run as SYSTEM, it'll have permission to duplicate handles into any process it wants by default. You see, by design, SYSTEM has full permission to all objects on the system, including all processes. Note the call to throwIfUserIsAdmin. This helper method checks to see if the resulting token has local administrative privileges. This helps prevent a compromised gateway from using the logon service to gain administrative privilege, which would make this whole solution pointless!

One last thing you might notice in my sample code is that I've written some custom SafeHandle-derived classes to help manage handles. This is a good idea in general when you find yourself doing a lot of P/Invoke work. The code for these is easy to understand, and can be found in the SafeHandles.cs file in the accompanying download. Search the Internet for "Shawn Farkas SafeHandle" and you'll find a great blog entry by one of my favorite guys on the Microsoft .NET Framework team that will show you how easy it is to build your own SafeHandle classes.

A Sample Gateway

Before I show you my sample gateway, I want to emphasize something really important: it's easy to misuse protocol transition! There's no way this feature can ensure that you properly authenticate incoming users. When the domain administrator enables protocol transition for your Web application's identity, she's entrusting you with the job that the domain controller normally does-authenticating users.

It's now the gateway's job to authenticate incoming users in the strongest possible fashion, and only then to use its special privilege to perform protocol transition and log on a corresponding Windows user account to represent that user.

Because this article is focused on protocol transition, as opposed to the myriad ways you might be authenticating users on the front end, my gateway sample does not authenticate incoming users at all. It simply allows you to type in a user name and the path to a file on the network that you want to attempt to read using that user's S4U logon. You can take my example and extend it to add whatever front-end authentication scheme makes sense for you to get started using protocol transition from a low-privileged gateway process. This is worth reiterating: do not just copy and paste my gateway sample into your own app, as you'll be leaving yourself wide open to attack!

Figure 4 shows the Web form that my sample gateway uses. You specify the user principal name and a file path as input, and it calls out to the logon service to establish an S4U logon for that user, wraps the resulting handle in a WindowsIdentity, impersonates the user, and reads the file. You can play with opening files on the same machine as the gateway, or with opening files on the network. You'll need to configure the A2D2 list with all the servers you plan on reading files from, and-although this is specific to the file system, which actually goes through a daemon called the Workstation service that runs as SYSTEM on each machine-you'll need to set the A2D2 list for the machine account of the gateway, as well as the user account under which the gateway runs.

Figure 4 Sample Gateway Web App

Figure 4** Sample Gateway Web App **(Click the image for a larger view)

Figure 5 shows the code that runs the Web page, calling out to the logon service. It's important to note that when you construct a WindowsIdentity from a token handle, the WindowsIdentity constructor duplicates the incoming handle. You need to get that original handle closed. This is why I'm careful to use my SafeTokenHandle class in a C# using statement, which will cause the original token handle to be closed at the end of the using block. This is important for long running processes like the gateway, especially for sensitive handles like tokens.

Figure 5 Codebehind for the Sample Gateway Web App

public partial class _Default : System.Web.UI.Page {
    protected void btnRead_Click(object sender, EventArgs e) {
        // connect to our privileged logon service (runs in COM+)
        ILogonService logonService = new LogonService();
        int tokenHandleValue =
            logonService.LogonUserViaProtocolTransition(
                txtUser.Text, GetCurrentProcessId());

        // WindowsIdentity ctor dups the incoming handle, so we use
        // SafeTokenHandle to ensure that the original gets closed
        using (SafeTokenHandle token = new SafeTokenHandle(
            tokenHandleValue))
        using (WindowsIdentity id = new WindowsIdentity(
            token.DangerousGetHandle()))
        using (WindowsImpersonationContext wic = id.Impersonate()) {
            txtOutput.Text = File.ReadAllText(txtPath.Text);
        }
    }

    [DllImport("kernel32.dll", SetLastError = true)]
    static extern int GetCurrentProcessId();
}

Security Considerations

All security countermeasures trade off against something. This solution reduces the privilege level of the gateway process significantly; you no longer have to run your gateway as SYSTEM. Remember that an attacker who successfully takes control of a process can do anything that process can do (including calling out to the logon service to obtain protocol transition logons). An attacker running as SYSTEM can read the local security accounts database, potentially gaining access to other systems on the network once he's cracked the hashes of any weak passwords found there. Running the gateway under lower privilege provides defense-in-depth against these sorts of attacks. So what's the downside? You've introduced a second process, along with additional context switching overhead to call into it. Your solution will be more tricky to deploy, as you now have to install the logon service COM+ component. Are these trade-offs worth it for your system? Measure and decide for yourself.

Send your questions and comments for Keith to  briefs@microsoft.com.

Keith Brown is a co-founder of Pluralsight, a premier Microsoft .NET training provider. Keith is the author of Pluralsight's Applied .NET Security course, as well as several books, including The .NET Developer's Guide to Windows Security, which is available both in print and on the web.