ISAPI Applications Security Issues

IIS offers FTP, Gopher and HTTP server capabilities, each running as an NT service. An NT service is a running process that is, potentially, started at system startup and may be running while users log on and off the machine. It appears to be part of the system services. As a process, it must run in the security context of some NT user account. Depending on the type of the service, the account may be a generic, privileged system account, or a specific account with particular rights. We know from the previous section that IIS runs under the almighty system user account. For other services, like some of the Microsoft BackOffice servers (Exchange or SQL), it's recommended that they are installed and run under a separate 'service' account to which the 'log on as service' privilege is added. In general, this is a good idea for these other servers since you then have finer control over the access rights of your services by just adding or removing membership to appropriate groups.

Unlike the other BackOffice servers, the 'normal mode' of operation of the IIS involves the access of server-based, operating-system-managed resources on behalf of remote client requests. We already know that the IIS accomplishes this securely through impersonation.

Let's now take a look at how this impersonation happens and what APIs are available under NT in order to exercise finer control on the thread's access rights.

By default, under NT, a newly created thread inherits the security context of its parent process. Access rights to resources accessed by the thread are checked against the process account. In a typical server like the IIS, the server process may have created a pool of worker threads to which it parcels out each request for servicing. Each worker thread services a request, perhaps producing a response to be sent back to the client and then is ready to process the next client request that has been queued. Now, each client request may require access to data (files or objects) that are accessible only to its corresponding user account. Since the server must access data on behalf of a variety of users, with different access rights, two avenues are open:

The first method is very dangerous! A bug in the server process, or a rogue request (depending on how general the request is and what checks and balances have been implemented) may allow an unauthorized user access to somebody else's private data. This method is also inconsistent with the Windows NT security model that we have discussed previously.

Fortunately, Win32 provides a number of API calls that support the second method allowing us much finer control over who accesses what.

The basic principle at work here, one that system components of NT adhere to, too, is this corollary of the general security model:

Before accessing system resources on behalf of a user, assume the identity of that user.

In this way, the server thread has exactly the same access rights as a process started by the logged on client (well, almost, since the impersonated user may not have access to networked resources). These rights are stored with the thread specific Security Access Token. These rights are checked against the ACL for the resources in question by the NT security reference monitor and access is granted or denied. Thus security decisions are kept centralized and protected within the NT executive, deep inside the kernel. The access model is also simplified since all access is reduced to the level of a process instantiated by a logged on user.

Let's look at some of the relevant functions from the security API before we go through an example of how we can use this from within our ISAPI extensions. The security API is large and complex and outside the scope of this chapter—the following subset, however, is sufficient to solve the problem at hand.

BOOL OpenThreadToken( HANDLE ThreadHandle,
                      DWORD DesiredAccess,
                      BOOL OpenAsSelf,
                      PHANDLE pTokenHandle );

OpenThreadToken() opens the Security Access Token associated with the thread identified by the thread handle. As we've discussed before, this access token encapsulates the security context of the current thread, or its parent process, and can be potentially modified and then used in order to change the security context of a running thread—either the same one, or another one. This token is the key to 'impersonation'. The function returns TRUE on success.

Let's take a look at the parameters:

Parameter Meaning
ThreadHandle This is a handle to the thread for which we want to get the security information. This implies that the calling thread must have permissions to inquire about the ThreadHandle thread.

Thread handles are returned by the CreateThread() call that creates a new thread. A running thread may get at its handle by invoking GetCurrentThread() .

 (Actually GetCurrentThread() returns a 'pseudohandle' that the thread can use to specify itself wherever a thread handle is required. If a proper thread handle is required, e.g. to pass to other threads to refer to the current thread, then DuplicateHandle() must be invoked on the pseudohandle.)

DesiredAccess This specifies an access mask with the requested types of access to the thread access token. As any other object in the system, the access token has a discretionary access control list (DACL) against which the DesiredAccess is checked.
OpenAsSelf This is an important parameter. If set to TRUE, the access check to decide whether access to the thread token will be granted or not, is done against the security context of the calling process. If the value is FALSE, the access check is against the calling thread security context.

In other words, if the thread belongs to a server process running under a privileged account, passing TRUE will perform the check against the privileged account, while a FALSE will result in a check against the current security context of the thread. If the thread is already impersonating a user account with fewer privileges, the check may fail.

PTokenHandle This is a pointer to a handle. If the call is successful, it will be set to a handle to an open access token to the thread. When the token is no longer necessary, the handle should be closed with CloseHandle().

The access rights implied by the token depend on the DesiredAccess parameter; that is, it may not encapsulate the complete set of rights for the thread.


Here are some of the most important values for the DesiredAccess parameter

DesiredAccess Value Meaning
TOKEN_ADJUST_PRIVILEGES Required to change the privileges specified in the access token.
TOKEN_DUPLICATE Required in order to duplicate an access token.
TOKEN_QUERY Required in order to inquire about the contents of an access token.
TOKEN_IMPERSONATE Required in order to get an access token that can then be used by a process or thread to impersonate the user with the rights that the access token represents.

An almost identical function is available in order to capture the access token of a process:

BOOL OpenProcessToken( HANDLE ProcessHandle,
                       DWORD DesiredAccess,
                       PHANDLE pTokenHandle );

If we now want to modify the privileges associated with the token, for example, in order to enable a the thread or process to perform a system shutdown, we must use the AdjustTokenPrivileges() function. Note that the function can't add new privileges to the access token but only enable or disable existing privileges.

The main parameters are the token handle in question, an array of the new privileges we want (we can specify whether we want them enabled or disabled), and a pointer to an array to set to the current set of privileges (so that we can revert to it after we're done).

BOOL AdjustTokenPrivileges( HANDLE TokenHandle,
                            BOOL DisableAllPrivileges,
                            PTOKEN_PRIVILEGES NewPriv,
                            DWORD PreviousBufferLen,
                            PTOKEN_PRIVILEGES PreviousPriv,
                            PDWORD RequirdPreviousBufferLen );
Parameter Meaning
TokenHandle Handle to the access token to be modified.
DisableAllPrivileges If set to TRUE, all privileges are revoked.
NewPriv Pointer to a TOKEN_PRIVILEGES structure. This contains a counter and an array of LUID_AND_ATTRIBUTES structures. Each one of these contains just two members: a locally unique identifier (LUID) for the privilege and a Boolean flag to be set to TRUE if the privilege should be enabled and to FALSE otherwise.
PreviousBufferLen The size of the buffer passed in the PreviousPriv argument.

The calling code must have allocated an array large enough to hold the PreviousPriv TOKEN_PRIVILEGES structure, if we want to keep hold of the current privileges before they get modified. If the size isn't sufficient, the last argument is set to the required buffer size and the call fails.

PreviousPriv If not set to NULL, it should point to an allocated buffer of size PreviousBufferLen
RequirdPreviousBufferLen Set to the size of the required buffer pointed to by PreviousPriv that can hold the current privileges.

The function will return TRUE if it has managed to modify at least some of the requested privileges.

Let's go now through an example where the thread in question must set the system time. Since this is a privileged operation, the thread must first attempt to adjust its privileges to include the SE_SYSTEMTIME_NAME privilege, change the time and then revert back to disabling the privilege.

HANDLE      hToken;      // handle to thread token
TOKEN_PRIVILEGES   tp;      // structure to hold the privileges array

// Get the current thread access token
if ( !OpenThreadToken( GetCurrentThread(),      // current thread handle
         TOKEN_ADJUST_PRIVILEGES   // we want to modify the access token privileges
         |  TOKEN_QUERY,      // ask for the existing privileges
         TRUE,  // access check against the process
         & hToken) )
   // error handling

We now need to get the LUID for the system time change privilege. A privilege has a well-known name, an LUID (which is unique on one machine while the system is up—but not necessarily between reboots) and a display string that is meaningful to the end user (in this case, for example, it could be 'Change system time'). We need to look up the LUID for this session, given the text string representing the well-known privilege name. The LUID is assigned to the first LUID_AND_ATTRIBUTES array of the token privilege structure.

LookupPrivilegeValue( NULL,      // system name – NULL for the local system
   SE_SHUTDOWN_NAME,   // string with privilege in question
   & tp.Privileges[0].Luid );      // pointer to 64-bit LUID

Enable the privilege in the token privilege structure.

tp.PrivilegeCount = 1;
tp.Privileges [0].Attributes = SE_PRIVILEGE_ENABLED;

We're now ready to attempt to adjust our thread privileges. For this example, we won't be interested in the current privilege set.

AdjustTokenPrivileges( hToken,   // token handle
      FALSE,      // do not disable all
      & tp,      // new token privilege structure
      0,      // not interested in current structure
      NULL,      // same here
      NULL );      // same here

Now we check whether all requested modifications have taken place. Checking whether the return value is TRUE isn't enough, since the function will succeed even for partial modifications. We need to invoke GetLastError() which will return ERROR_SUCCESS if all modifications succeeded and ERROR_NOT_ALL_ASSIGNED otherwise.

if ( GetLastError() != ERROR_SUCCESS )
   // handle error

The coast is clear! Let's change the system time!

SYSTEMTIME   t;

GetSystemTime( &t );
t.wHour += 1;   // add one hour to current time!
if ( ! SetSystemTime( &t ) )
   // handle error

We now need to disable the change system time privilege to bring things back to the default state.

tp.Privileges[0].Attributes =  0;   // disable it
AdjustTokenPrivileges( hToken,   // token handle
      FALSE,      // do not disable all
      & tp,      // token privilege structure reverting to original state
      0,      // not interested in current structure
      NULL,      // same here
      NULL );      // same here

if ( GetLastError() != ERROR_SUCCESS )
   // handle error

Let's move on now to the core subject of impersonation.

© 1997 by Wrox Press. All rights reserved.