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
- Why there was not access failure for the web application on the development machine ?
- 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.
This 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