Web application gets Access Denied accessing a Named Pipe.

Recently, I was troubleshooting a problem for one of my customers. A named pipe created by a native C application was not accessible by web client. The actual product is a convention Windows application which does IPC through named pipes. Both server and client for this were Windows applications. They were trying to extend the functionality so that a Web application written in C# ASP.NET can communicate with the server over the named pipe.

Everything was  fine until they deployed the web application. Web application if running on development box was able to communicate with the named pipe, however once deployed it started getting access denied on the named pipe.  On a contrary windows application was able to communicate with the Named Pipe. 

Following is the code of the native server, C# client and web application in order.

Server:

 #include <Windows.h>
 #include <Sddl.h>
 #include <stdio.h>
  
 #pragma comment(lib,"Advapi32.lib")
  
 #define BUFSIZE 4096
  
  
 int _tmain(int argc, _TCHAR* argv[])
 {
         BOOL fConnected; 
         DWORD dwThreadId; 
         PSECURITY_DESCRIPTOR psd;
         HANDLE hPipe, hThread; 
         LPTSTR lpszPipename = TEXT("\\\\.\\pipe\\mynamedpipe"); 
         SECURITY_ATTRIBUTES SA ;
  
         CHAR WriteBuffer[20] = "";
  
  
         SA.bInheritHandle=FALSE; // Children to Inherit the handle
  
         _tprintf(TEXT("CreatePipe %s\n"), lpszPipename);
         hPipe = CreateNamedPipe( 
                 lpszPipename,             // pipe name 
                 PIPE_ACCESS_DUPLEX,       // read/write access 
                 PIPE_TYPE_MESSAGE |       // message type pipe 
                 PIPE_READMODE_MESSAGE |   // message-read mode 
                 PIPE_WAIT,                // blocking mode 
                 PIPE_UNLIMITED_INSTANCES, // max. instances  
                 BUFSIZE,                  // output buffer size 
                 BUFSIZE,                  // input buffer size 
                 0,                        // client time-out 
                 NULL /*&SA*/);                    // default security attribute 
  
         if (hPipe == INVALID_HANDLE_VALUE) 
         {
                 printf("CreatePipe failed"); 
                 return 0;
         } 
  
         _tprintf(TEXT("Waiting for connection %s\n"), lpszPipename);
         if(TRUE == (fConnected = ConnectNamedPipe(hPipe, NULL) ))
         {
                 if((GetLastError() == ERROR_PIPE_CONNECTED))
                 {
                         _tprintf(TEXT("Connected %s\n"), lpszPipename);
                 }
                 else {
                         printf("ConnectNamedPipe GLE %d\n", GetLastError()); 
                 }
         }
  
     Sleep(5000);
     for(int  i = 0; i<100; ++i){
         DWORD dwBytes = 0;
         sprintf(WriteBuffer,"%d %s",i,"ShortStr");
         printf("Writing: %s\n",WriteBuffer);
         if(!WriteFile(hPipe,WriteBuffer,10,&dwBytes,NULL))
         {
             printf("WriteFile GLE %d\n", GetLastError()); 
             break;
         }
         Sleep(1000);
     }
     CloseHandle(hPipe);
         
     //getchar();
  
         return 0;
 }

 

Windows Client:

 using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using System.IO;
 using System.IO.Pipes;
 using System.Threading;
  
 namespace NamedPipeClientWindows
 {
     class Program
     {
         public class StateObject
         {
             public byte[] buffer ;
             public int packetnumber ;
             public Stream PipeStream ;
         }
  
         public static Object[] WaitObject = new Object[0];
         public static Object[] NumPacketMutex = new Object[0];
         public static int _maxPackets = 100;
         public static int buflen = 10;
         public static int NumPacketsToFinish = _maxPackets;
         static void Main(string[] args)
         {
             using (NamedPipeClientStream pipeClient =
            new NamedPipeClientStream(".", "mynamedpipe", PipeDirection.InOut, PipeOptions.None))
             {
                 Byte [] buffer = new Byte [100];
                 // Connect to the pipe or wait until the pipe is available.
                 Console.WriteLine("Attempting to connect to pipe...");
                 pipeClient.Connect();
                 Console.WriteLine("Connected to pipe.");
                 Console.WriteLine("There are currently {0} pipe server instances open." + pipeClient.NumberOfServerInstances);
                 long t0 = Environment.TickCount;
  
                 AsyncCallback callBack = new AsyncCallback(PipeCallBack);
                 for (int i = 0; i < _maxPackets; ++i)
                 {
                     StateObject state = new StateObject();
                     state.buffer = new Byte [100];
                     state.packetnumber = i;
                     state.PipeStream = pipeClient;
                     pipeClient.BeginRead(state.buffer, 0, 10, PipeCallBack, state);
                     Console.WriteLine("Queued WorkerThread to read {0}", i);
                 }
                 // Determine whether all packets are done being processed.  
                 // If not, block until all are finished.
                 bool mustBlock = false;
                 lock (NumPacketMutex)
                 {
                     if (NumPacketsToFinish > 0)
                         mustBlock = true;
                 }
                 if (mustBlock)
                 {
                     Console.WriteLine("All worker threads are queued. " +
                         " Blocking until they complete. numLeft: {0}",
                         NumPacketsToFinish);
                     Monitor.Enter(WaitObject);
                     Monitor.Wait(WaitObject);
                     Monitor.Exit(WaitObject);
                 }
                 long t1 = Environment.TickCount;
                 Console.WriteLine("Total time processing images: {0}ms",
                     (t1 - t0));
                 pipeClient.Close();
             }
         }
         static void PipeCallBack(IAsyncResult asyncResult)
         {
             int bytesRead = 0;
             StateObject state = (StateObject)asyncResult.AsyncState;
             Stream stream = state.PipeStream;
             bytesRead = stream.EndRead(asyncResult);
             if (bytesRead == 0)
                 throw new Exception(String.Format
                     ("In ReadInImageCallback, got the wrong number of " +
                     "bytes from the image: {0}.", bytesRead));
             String str ;
  
             System.Text.ASCIIEncoding enc = new System.Text.ASCIIEncoding();
             str = enc.GetString(state.buffer);
             Console.WriteLine(str);
             lock (NumPacketMutex)
             {
                 NumPacketsToFinish--;
                 if (NumPacketsToFinish == 0)
                 {
                     Monitor.Enter(WaitObject);
                     Monitor.Pulse(WaitObject);
                     Monitor.Exit(WaitObject);
                 }
             }
         }
  
     }
 }

 

Web client:

 using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Web;
 using System.Web.UI;
 using System.Web.UI.WebControls;
 using System.IO;
 using System.IO.Pipes;
  
 public partial class _Default : System.Web.UI.Page
 {
     protected void Page_Load(object sender, EventArgs e)
     {
  
     }
     
     protected void Button1_Click1(object sender, EventArgs e)
     {
         using (NamedPipeClientStream pipeClient =
             new NamedPipeClientStream(".", "mynamedpipe", PipeDirection.InOut, PipeOptions.None))
         {
  
             // Connect to the pipe or wait until the pipe is available.
             Label1.Text = "Attempting to connect to pipe...";
             pipeClient.Connect();
             Label1.Text = Label1.Text + "\r\nConnected to pipe.";
             Label1.Text = Label1.Text + "\r\nThere are currently {0} pipe server instances open." + pipeClient.NumberOfServerInstances;
         }
  
     }
 }

 

 

The code looks good, however to resolve this problem you would need to know a little about web applications. I am not much familiar with web application but here are few basic things which I know.

  • - A web application is hosted in IIS under a process called W3WP.EXE this process runs under the context of Network Service account.
  • - Web applications typically have a thread pool. They may or may not share the process access token depending on the authentication mechanism used for the web application.
  • - Web applications are non-interactive in nature. However, this thing is not related 
  • To resolve an access denied typically you would need to know –
  • - Who is caller asking for the access ? Typically, a thread token determines the caller.
  • - What are the access caller asked? The access mask provide in the API determines this. For example READ or WRITE or FULL CONTROL
  • - What are the security permissions present on the object ? DACL of the objects determines this.

Let’s try resolving this problem answering above questions

- Who is the caller ?

  • o Since W3WP.EXE runs in the context of Network Service.  Network Service could be a caller.
  • o If the thread pool for the web application has got a different identity due to the impersonation of the user running the web application. He will act as caller.
  • o Easy way to determine this is look at the ASP.NET code and find what kind of authentication and impersonation web application is using.
  • o Other way would be to attach a debugger to W3WP and break on access of the named pipe, then dump the thread token. This method is guaranteed to give you right information. However, it needs a debugger on the problem machine, which may be a production machine and people usually do not encourage debugger on the production server.

- What is the access asked ?

  • o You can look at the code to determine this. For bidirectional named pipe it is most likely be GENERIC_READ and GENERIC_WRITE.
  • o You can break in debugger to find this. An access denied call stack typically would tell you this mask.
  • o A Process Monitor log for the process (W3WP.EXE in this case)
  • - What are the security permissions on the object.
  • o The above code of creation of the Named Pipe tells us that it has default security permissions.  Default DACL is determine by the container object or by the access token of the creator. See The Old New Thing : What is the default security descriptor? for more information.
  • o You can also dump the DACLs of the object to ensure that you know exact access permissions on the object. I have user SubInAcl tool to dump the security descriptor.

I dumped access permissions on the named pipe created on my machine by a Windows application. Default access permission of the Named Pipe on my machine look like following.

 C:\Program Files (x86)\Windows Resource Kits\Tools>subinacl.exe /onlyfile \\pdubey2\pipe\mynamedpipe /display=sddl
  
 +File \\pdubey2\pipe\mynamedpipe
 /sddl=O:S-1-5-21-2146773085-903363285-719344707-492476G:DUD:(A;;FA;;;SY)(A;;FA;;;BA)(A;;FA;;;S-15-21-2146773085-903363285-719344707-492476)(A;;FR;;;WD)(A;;FR;;;AN)
 C:\Program Files (x86)\Windows Resource Kits\Tools>subinacl.exe /onlyfile \\pdubey2\pipe\mynamedpipe
  
 =================================
 +File \\pdubey2\pipe\mynamedpipe
 =================================
 /control=0x0
 /owner             =fareast\pdubey
 /primary group     =fareast\domain users
 /audit ace count   =0
 /perm. ace count   =5
 /pace =system   ACCESS_ALLOWED_ACE_TYPE-0x0
     Type of access:
         Full Control
     Detailed Access Flags :
         FILE_READ_DATA-0x1          FILE_WRITE_DATA-0x2         FILE_APPEND_DATA-0x4
         FILE_READ_EA-0x8            FILE_WRITE_EA-0x10          FILE_EXECUTE-0x20            FILE_DELETE_CHILD-0x40
         FILE_READ_ATTRIBUTES-0x80   FILE_WRITE_ATTRIBUTES-0x100 DELETE-0x10000              READ_CONTROL-0x20000
         WRITE_DAC-0x40000           WRITE_OWNER-0x80000         SYNCHRONIZE-0x100000
 /pace =builtin\administrators   ACCESS_ALLOWED_ACE_TYPE-0x0
     Type of access:
         Full Control
     Detailed Access Flags :
         FILE_READ_DATA-0x1          FILE_WRITE_DATA-0x2         FILE_APPEND_DATA-0x4
         FILE_READ_EA-0x8            FILE_WRITE_EA-0x10          FILE_EXECUTE-0x20            FILE_DELETE_CHILD-0x40
         FILE_READ_ATTRIBUTES-0x80   FILE_WRITE_ATTRIBUTES-0x100 DELETE-0x10000              READ_CONTROL-0x20000
         WRITE_DAC-0x40000           WRITE_OWNER-0x80000         SYNCHRONIZE-0x100000
 /pace =fareast\pdubey   ACCESS_ALLOWED_ACE_TYPE-0x0
     Type of access:
         Full Control
     Detailed Access Flags :
         FILE_READ_DATA-0x1          FILE_WRITE_DATA-0x2         FILE_APPEND_DATA-0x4
         FILE_READ_EA-0x8            FILE_WRITE_EA-0x10          FILE_EXECUTE-0x20            FILE_DELETE_CHILD-0x40
         FILE_READ_ATTRIBUTES-0x80   FILE_WRITE_ATTRIBUTES-0x100 DELETE-0x10000              READ_CONTROL-0x20000
         WRITE_DAC-0x40000           WRITE_OWNER-0x80000         SYNCHRONIZE-0x100000
 /pace =everyone         ACCESS_ALLOWED_ACE_TYPE-0x0
     Type of access:
         Special acccess :  -Read
     Detailed Access Flags :
         FILE_READ_DATA-0x1          FILE_READ_EA-0x8            FILE_READ_ATTRIBUTES-0x80
         READ_CONTROL-0x20000        SYNCHRONIZE-0x100000
 /pace =anonymous logon  ACCESS_ALLOWED_ACE_TYPE-0x0
     Type of access:
         Special acccess :  -Read
     Detailed Access Flags :
         FILE_READ_DATA-0x1          FILE_READ_EA-0x8            FILE_READ_ATTRIBUTES-0x80
         READ_CONTROL-0x20000        SYNCHRONIZE-0x100000
  
  
 Elapsed Time: 00 00:00:00
 Done:        1, Modified        0, Failed        0, Syntax errors        0
 Last Done  : \\pdubey2\pipe\mynamedpipe

The above security descriptors potentially tell me following :

  • Administrators can ask for full control.
  • I as the creator can ask for full control.
  • Everyone and  Anonymous can ask for read control.

In a typical scenario a Named Pipe server runs as a Windows Service, which would most likely be running in Local System or an Administrator’s context.  However, a web application would not run with the same token as Windows service. They would present a much lower access token to the Named Pipe to access it. In this case, token of Network Service, a domain user, everyone and anonymous.

From the above assessment it is most likely that web application is getting access denied because the DACL set on the Name Pipe is not appropriate to the design of the project. My customer wanted to provide full control to the Everyone and Anonymous and that made my job easy. Which is appropriate for a duplex pipe with only read access client will not be able to write. I changed the server code as following :

 #include <Windows.h>
 #include <Sddl.h>
 #include <stdio.h>
  
 #pragma comment(lib,"Advapi32.lib")
  
 #define BUFSIZE 4096
  
  
 int _tmain(int argc, _TCHAR* argv[])
 {
         BOOL fConnected; 
         DWORD dwThreadId; 
         PSECURITY_DESCRIPTOR psd;
         HANDLE hPipe, hThread; 
         LPTSTR lpszPipename = TEXT("\\\\.\\pipe\\mynamedpipe"); 
         SECURITY_ATTRIBUTES SA ;
  
     CHAR WriteBuffer[20] = "";
  
         TCHAR szStringSecurityDis2[512] = TEXT("");
  
         _tcscat(szStringSecurityDis2,TEXT("D:(A;;GA;;;WD)(A;;GA;;;AN)"));
  
  
         if (!ConvertStringSecurityDescriptorToSecurityDescriptor(szStringSecurityDis2,
                 SDDL_REVISION_1,
                 &psd,
                 NULL))
                 _tprintf(TEXT("ConvertStringSecurityDescriptorToSecurityDescriptor %u\n"), GetLastError());
  
  
         memset(&SA,0,sizeof(SA));
         SA.bInheritHandle = FALSE ;
         SA.lpSecurityDescriptor = psd;
         SA.nLength = sizeof(SA);
  
         SA.bInheritHandle=FALSE; // Children to Inherit the handle
  
         _tprintf(TEXT("CreatePipe %s\n"), lpszPipename);
         hPipe = CreateNamedPipe( 
                 lpszPipename,             // pipe name 
                 PIPE_ACCESS_DUPLEX,       // read/write access 
                 PIPE_TYPE_MESSAGE |       // message type pipe 
                 PIPE_READMODE_MESSAGE |   // message-read mode 
                 PIPE_WAIT,                // blocking mode 
                 PIPE_UNLIMITED_INSTANCES, // max. instances  
                 BUFSIZE,                  // output buffer size 
                 BUFSIZE,                  // input buffer size 
                 0,                        // client time-out 
                 NULL /*&SA*/);                    // default security attribute 
  
         if (hPipe == INVALID_HANDLE_VALUE) 
         {
                 printf("CreatePipe failed"); 
                 return 0;
         } 
  
         _tprintf(TEXT("Waiting for connection %s\n"), lpszPipename);
         if(TRUE == (fConnected = ConnectNamedPipe(hPipe, NULL) ))
         {
                 if((GetLastError() == ERROR_PIPE_CONNECTED))
                 {
                         _tprintf(TEXT("Connected %s\n"), lpszPipename);
                 }
                 else {
                         printf("ConnectNamedPipe GLE %d\n", GetLastError()); 
                 }
         }
  
     Sleep(5000);
     for(int  i = 0; i<100; ++i){
         DWORD dwBytes = 0;
         sprintf(WriteBuffer,"%d %s",i,"ShortStr");
         printf("Writing: %s\n",WriteBuffer);
         if(!WriteFile(hPipe,WriteBuffer,10,&dwBytes,NULL))
         {
             printf("WriteFile GLE %d\n", GetLastError()); 
             break;
         }
         Sleep(1000);
     }
     CloseHandle(hPipe);
         
     //getchar();
  
         return 0;
 }

 

Following addition creates a security descriptor which grants full control to  Everyone and to the Anonymous.

 TCHAR szStringSecurityDis2[512] = TEXT("");
  
         _tcscat(szStringSecurityDis2,TEXT("D:(A;;GA;;;WD)(A;;GA;;;AN)"));
  
  
         if (!ConvertStringSecurityDescriptorToSecurityDescriptor(szStringSecurityDis2,
                 SDDL_REVISION_1,
                 &psd,
                 NULL))
                 _tprintf(TEXT("ConvertStringSecurityDescriptorToSecurityDescriptor %u\n"), GetLastError());
  
  
         memset(&SA,0,sizeof(SA));
         SA.bInheritHandle = FALSE ;
         SA.lpSecurityDescriptor = psd;
         SA.nLength = sizeof(SA);

 

The changed security descriptor is presented as following.

 

 C:\Program Files (x86)\Windows Resource Kits\Tools>subinacl.exe /onlyfile \\pdubey2\pipe\mynamedpipe /display=sddl
  
 +File \\pdubey2\pipe\mynamedpipe
 /sddl=O:S-1-5-21-2146773085-903363285-719344707-492476G:DUD:(A;;FA;;;WD)(A;;FA;;;AN)
 C:\Program Files (x86)\Windows Resource Kits\Tools>subinacl.exe /onlyfile \\pdubey2\pipe\mynamedpipe
  
 =================================
 +File \\pdubey2\pipe\mynamedpipe
 =================================
 /control=0x0
 /owner             =fareast\pdubey
 /primary group     =fareast\domain users
 /audit ace count   =0
 /perm. ace count   =2
 /pace =everyone         ACCESS_ALLOWED_ACE_TYPE-0x0
     Type of access:
         Full Control
     Detailed Access Flags :
         FILE_READ_DATA-0x1          FILE_WRITE_DATA-0x2         FILE_APPEND_DATA-0x4
         FILE_READ_EA-0x8            FILE_WRITE_EA-0x10          FILE_EXECUTE-0x20            FILE_DELETE_CHILD-0x40
         FILE_READ_ATTRIBUTES-0x80   FILE_WRITE_ATTRIBUTES-0x100 DELETE-0x10000              READ_CONTROL-0x20000
         WRITE_DAC-0x40000           WRITE_OWNER-0x80000         SYNCHRONIZE-0x100000
 /pace =anonymous logon  ACCESS_ALLOWED_ACE_TYPE-0x0
     Type of access:
         Full Control
     Detailed Access Flags :
         FILE_READ_DATA-0x1          FILE_WRITE_DATA-0x2         FILE_APPEND_DATA-0x4
         FILE_READ_EA-0x8            FILE_WRITE_EA-0x10          FILE_EXECUTE-0x20            FILE_DELETE_CHILD-0x40
         FILE_READ_ATTRIBUTES-0x80   FILE_WRITE_ATTRIBUTES-0x100 DELETE-0x10000              READ_CONTROL-0x20000
         WRITE_DAC-0x40000           WRITE_OWNER-0x80000         SYNCHRONIZE-0x100000
  
  
 Elapsed Time: 00 00:00:00
 Done:        1, Modified        0, Failed        0, Syntax errors        0
 Last Done  : \\pdubey2\pipe\mynamedpipe

Now when the security descriptor has right access permissions the problem got resolved. Web application was able to access the named pipe.

This blog is not complete yet, there are few questions unanswered

  1. Why there was not access failure for the web application on the development machine ?
  2. Why not the Windows client did get the access failure.

Here is the explanation:

1) Visual Studio provides a special mechanism to run the web applications on development machines, see: Web Servers in Visual Web Developer. This process however runs in the context of logged in user, not in context of Network Service.

clip_image001This means that the creator of the named pipe and the caller asking for the permission are same, and there is no reason for access denied. 

2) Similar to WebDev.WebServer.Exe a windows application would also run in the context of the logged in user’s context and it is less likely to get an access denied.

This post is not intend to resolve all access denied issues on a named pipe, it is just intended to brief you an approach you can use to resolve the problem.

- Prateek