File permissions with FileSystemObject - CScript.exe says one thing, Classic ASP says another - asp-classic

I have a classic ASP page - written in JScript - that's using Scripting.FileSystemObject to save files to a network share - and it's not working. ("Permission denied")
The ASP page is running under IIS using Windows authentication, with impersonation enabled.
If I run the following block of code locally via CScript.exe:
var objNet = new ActiveXObject("WScript.Network");
WScript.Echo(objNet.ComputerName);
WScript.Echo(objNet.UserName);
WScript.Echo(objNet.UserDomain);
var fso = new ActiveXObject("Scripting.FileSystemObject");
var path = "\\\\myserver\\my_share\\some_path";
if (fso.FolderExists(path)) {
WScript.Echo("Yes");
} else {
WScript.Echo("No");
}
I get the (expected) output:
MY_COMPUTER
dylan.beattie
MYDOMAIN
Yes
If I run the same code as part of a .ASP page, substituting Response.Write for WScript.Echo I get this output:
MY_COMPUTER
dylan.beattie
MYDOMAIN
No
Now - my understanding is that the WScript.Network object will retrieve the current security credentials of the thread that's actually running the code. If this is correct - then why is the same user, on the same domain, getting different results from CScript.exe vs ASP? If my ASP code is running as dylan.beattie, then why can't I see the network share? And if it's not running as dylan.beattie, why does WScript.Network think it is?

Your problem is clear. In the current implementation you have only impersonation of users and no delegation. I don't want to repeat information already written by Stephen Martin. I only want to add at least three solutions. The classical way of delegation which Stephen Martin suggests is only one way. You can read some more ways here: http://msdn.microsoft.com/en-us/library/ff647404.aspx#paght000023_delegation. I see three practical ways of you solving your problem:
Convert the impersonation token of the user to a token with delegation level of impersonation or to a new primary token. You can do this with respect of DuplicateToken or DuplicateTokenEx.
Use S4U2Self (see http://msdn.microsoft.com/en-us/magazine/cc188757.aspx and http://msdn.microsoft.com/en-us/library/ms998355.aspx) to receive a new token from the old one with respect of one simple .NET statement WindowsIdentity wi = new WindowsIdentity(identity);
You can access another server with respect of one fixed account. It can be a computer account on an account of the application pool of the IIS. It can be another fixed defined account which one will only use for access to the file system.
It is important to know which version of Windows Server you have on the server where IIS is running and which Domain Function Level you have in Active Directory for your Domain (you see this in "Active Directory Domain and Trusts" tool if you select your domain and choose "Raise Domain Functional Level"). It is also interesting to know under which account the application pool of the IIS runs.
The first and the third way will always work. The third way can be bad for your environment and for the current permission in the file system. The second one is very elegant. It allows control of which servers (file server) are accessed from IIS. This way has some restrictions and it needs some work to be done in Active Directory.
Because you use classic ASP, a small scriptable software component must be created to support your implementation.
Which way do you prefer?
UPDATED based on the question from comment: Because you use classic ASP you can not use a Win32 API directly, but you can write a small COM component in VB6 or in .NET which use APIs which you need. As an example you can use code from http://support.microsoft.com/kb/248187/en. But you should do some other things inside. So I explain now which Win32 API can help you to do everything what you need with tokens and impersonation.
First of all a small explanation about impersonation. Everything works very easy. There are always one primary token under which the process runs. To any thread another token (thread token) can be assigned. To do this one needs to have a token of a user hUserToken and call API ImpersonateLoggedOnUser(hUserToken);.
To go back to the original process token (for the current thread only) you can call RevertToSelf() function. The token of user will be received and already impersonated for you by IIS, because you so configured your Web Site. To go back to the original process token you should implement calling of the function RevertToSelf() in your custom COM component. Probably, if you need to do nothing more in the ASP page, it will be enough, but I recommend you be more careful and save current users token in a variable before operation with files. Then you make all operations with file system and at the end reassign users token back to the current thread. You can assign an impersonation token to a thread with respect of SetThreadToken(NULL,hUserToken);. To give (save) current thread token (user token in your case) you can use OpenThreadToken API. It must work.
UPDATED 2: Probably the usage of RevertToSelf() function at the end of one ASP page would be already OK for you. The corresponding C# code can be so:
Create a new Project in C# of the type "Class Library" with the name LoginAdmin. Paste the following code inside
using System;
using System.Runtime.InteropServices;
namespace LoginAdmin {
[InterfaceTypeAttribute (ComInterfaceType.InterfaceIsDual)]
public interface IUserImpersonate {
[DispId(1)]
bool RevertToSelf ();
}
internal static class NativeMethods {
[DllImport ("advapi32.dll", SetLastError = true)]
internal static extern bool RevertToSelf ();
}
[ClassInterface (ClassInterfaceType.AutoDual)]
public class UserImpersonate : IUserImpersonate {
public UserImpersonate () { }
public bool RevertToSelf () {
return NativeMethods.RevertToSelf();
}
}
}
Check in project properties in "Build" part "Register for COM interop". In "Signing" part of the project check Sign the assembly and in "Choose a strong name key file" choose <New...>, then type any filename and password (or check off "protect my key..."). At the end you should modify a line from AssemblyInfo.cs in Properties part of the project:
[assembly: ComVisible (true)]
After compiling this project you get two files, LoginAdmin.dll and LoginAdmin.tlb. The DLL is already registered on the current computer. To register if on the other computer use RegAsm.exe.
To test this COM DLL on a ASP page you can do following
<%# Language="javascript" %>
<html><body>
<% var objNet = Server.CreateObject("WScript.Network");
Response.Write("Current user: ");Response.Write(objNet.UserName);Response.Write("<br/>");
Response.Write("Current user's domain: ");Response.Write(objNet.UserDomain);Response.Write("<br/>");
var objLoginAdmin = Server.CreateObject("LoginAdmin.UserImpersonate");
var isOK = objLoginAdmin.RevertToSelf();
if (isOK)
Response.Write("RevertToSelf return true<br/>");
else
Response.Write("RevertToSelf return false<br/>");
Response.Write("One more time after RevertToSelf()<br/>");
Response.Write("Current user: ");Response.Write(objNet.UserName);Response.Write("<br/>");
Response.Write("Current user's domain: ");Response.Write(objNet.UserDomain);Response.Write("<br/>");
var fso = Server.CreateObject("Scripting.FileSystemObject");
var path = "\\\\mk01\\C\\Oleg";
if (fso.FolderExists(path)) {
Response.Write("Yes");
} else {
Response.Write("No");
}%>
</body></html>
If the account used to run the IIS application pool has access to the corresponding network share, the output will be look like following
Current user: Oleg
Current user's domain: WORKGROUP
RevertToSelf return true
One more time after RevertToSelf()
Current user: DefaultAppPool
Current user's domain: WORKGROUP
Yes

Under impersonation you can only access securable resources on the local computer you cannot access anything over the network.
On Windows when you are running as an impersonated user you are running under what is called a Network token. This token has the user's credentials for local computer access but has no credentials for remote access. So when you access the network share you are actually accessing it as the Anonymous user.
When you are running a process on your desktop (like CScript.exe) then you are running under an Interactive User token. This token has full credentials for both local and remote access, so you are able to access the network share.
In order to access remote resources while impersonating a Windows user you must use Delegation rather then Impersonation. This will involve some changes to your Active directory to allow delegation for the computer and/or the users in your domain. This can be a security risk so it should be reviewed carefully.

Related

Sccess some folder on file share based on user authenticated

I have an asp.net web application with forms authentication and users (credentials) are checked against active directory, username is actually samAccountName attribute from AD.
Now I need to enable users to get access to some files which are located on file share, where each user has his own folder.
First proof of concept works like this:
appPool in IIS is configured to run under some domain user, and this user was given R/W access to file share and all user folders
when the user logs into web app only content of the folder on the path "\\myFileServer\username" is visible to him. And same when uploading files they get stored to "\\myFileServer\username".
While this works, doesn't seem to be secure at all. First issue is that user under which application pool runs has access to folders from all users. And even bigger concern is that only username determines to which folder you have access.
So my question is what is the correct/better way to doing this ? I was reading about impersonating the user, but this is not advised anymore if I understood correctly ? And I don't have Windows authentications since the web application must be accessible from internet.
I recommend not running the application under a user account, but creating an application specific account under which it runs with the proper R/W rights, and separate the person who gives these rights from the development team.
Within the application's authentication: after you receive a GET/POST request, you can verify the path to which the current user would read/write data, and cross-reference this with the path the user is authorized to read/write from. If these are incorrect, return a 401 NOT AUTHORIZED response, else, carry on the operation as you do now.
If your endpoints are protected properly, and the application runs under its own account, I don't see any harm in the setup itself. This still however gives the developers a way, through the application, to indirectly access other user's files. Based on how tight these checks must be, you could add additional controls, (like only allowing the application to connect from the production server, and only allowing server transport in a controlled way).
From the Description of your Problem i think Custom HttpHandlers are the right choice for you. You didn't mention what type of files will be present in your Folder , for brevity i will answer by assuming it will be having PDF files.
As you were mentioning that your application will be having different users so for this you need to use .NET built-in authentication manager and role provider. With a simple security framework setup, we'll place a PDF file in the web application, behind a web.config protected folder.then create a custom HTTP handler to restrict access on the static document to only those users who should be allowed to view it.
A sample HTTP Handler:
public class FileProtectionHandler : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
switch (context.Request.HttpMethod)
{
case "GET":
{
// Is the user logged-in?
if (!context.User.Identity.IsAuthenticated)
{
FormsAuthentication.RedirectToLoginPage();
return;
}
string requestedFile =
context.Server.MapPath(context.Request.FilePath);
// Verify the user has access to the User role.
if (context.User.IsInRole("User"))
{
SendContentTypeAndFile(context, requestedFile);
}
else
{
// Deny access, redirect to error page or back to login
//page.
context.Response.Redirect("~/User/AccessDenied.aspx");
}
break;
}
}
}
Method SendContentTypeAndFile :
private HttpContext SendContentTypeAndFile(HttpContext context, String strFile)
{
context.Response.ContentType = GetContentType(strFile);
context.Response.TransmitFile(strFile);
context.Response.End();
return context;
}
private string GetContentType(string filename)
{
// used to set the encoding for the reponse stream
string res = null;
FileInfo fileinfo = new FileInfo(filename);
if (fileinfo.Exists)
{
switch (fileinfo.Extension.Remove(0, 1).ToLower())
{
case "pdf":
{
res = "application/pdf";
break;
}
}
return res;
}
return null;
}
Last step is that you need to configure this HTTP Handler in the webconfig ,
and You can see the more info here
Here is the complete Source Code
You're architecture (and assumptions) seem good for a low/mid security level, but if the nature of your data is very sensitive (medical, etc) my biggest concern about security would be controlling the user sessions.
If you're using forms authentication then you're storing the authenticated identity in a cookie or in a token (or if you're using sticky sessions then you're sending the session Id, but for the case it's the same). The problem arises if user B has phisical access to the machine where user A works. If user A leaves it's workplace (for a while or forever) and he doesn't explicitly close it's session in your web app, then his identity has been left around, at least until his cookie/token expires, and user B can use it since the identity system of ASP.NET hasn't performed a SignOut. The problem is even worse if you use tokens for authorization, because in all the infamous Microsoft implementations of the Identity System you're responsible of providing a way to invalidate such tokens (and make them dissapear from the client machine) when the user signs out, since they would stay valid until it's expiration. This can be addressed (but no completely thus not very satisfactorily for high security requirements) issuing short living refresh tokens, but that's another story, and I don't know if it's your case. If you're going with cookies then when user A signs out it's cookie is invalidated and removed from the request/response cicle, so this problem is mitigated. Anyway you should ensure that your users close their sessions in your web app or/and configure the cookies with short lives or short sliding expirations.
Other security concerns may be related with CSRF, wich you can prevent using the Antiforgery Token infrastructure of ASP.NET, but these kind of attacks are methods that are very far away from the tipical user (I don't know anything about the nature of your user and if your app is exposed to public on internet or it's only accesible on an intranet), but If you worry for such specialised attacks and have so sensitive data, maybe you should go with something more complex than forms authentication (two factor, biometrical, etc)

Setting ASP.Net Permissions - Access is denied. (Exception from HRESULT: 0x80070005 (E_ACCESSDENIED))

I have an MVC 4 application that allows a user to change their Active Directory password through a password reset functionality page. I have the following piece of code that sets the new password:
DirectoryEntry de = sr.GetDirectoryEntry();
de.Invoke("SetPassword", new object[] { newPassword });
de.Properties["LockOutTime"].Value = 0;
Upon trying to submit the form with the new password details I am having the following error written to the application event log:
0x80070005 (E_ACCESSDENIED))
I have set the Identity property of the Application Pool to NetworkService and had thought that this would have resolved the issue of connecting. Is there anything else further I need to ensure so that my ASPNET application can connect to the AD.
tl;dr
In our case, this started happening randomly. Turns out it's because our self-signed SSL certificate had expired. After creating a new one in IIS, the problem was resolved.
Explanation
This thread lead me to the cause.
I will briefly recap what SetPassword does here so you can see why you need it. That particular ADSI method really bundles 3 methods under the covers. It first tries to set the password over a secure SSL channel using LDAP. Next, it tries to set using Kerberos set password protocol. Finally, it uses NetUserSetInfo to attempt to set it.
The main problem is that only the first two methods will generally respect the credentials that you put on the DirectoryEntry. If you provide the proper credentials and an SSL channel for instance, the LDAP change password mechanism will work with those credentials...
If you check the NetUserSetInfo method, you will notice there is no place to put a username/password for authorization. In other words, it can only use the unmanaged thread's security context. This means that in order for it to work, it would have to impersonate the username/password combination you provided programmatically first...
LDAP over SSL is apparently the best way to go (and was the method that we had been using), and it appears (clarifications welcome) that once our self-signed SSL certificate had expired, it skipped Kerberos and fell back to NetUserSetInfo, which failed because it was not using the credentials we provided. (Or it just failed on Kerberos, as the poster said he had never seen the credentials passed in for Kerberos)
So after creating a new self-signed certificate (for COMPUTER.DOMAIN.local), the problem was resolved.
Here is the code (in case anyone's looking for it):
DirectoryEntry myDE = new DirectoryEntry(#"LDAP://OU=GroupName,DC=DOMAIN,DC=local");
myDE.Username = "administrator";
myDE.Password = "adminPassword";
DirectoryEntries myEntries = myDE.Children;
DirectoryEntry myDEUser = myEntries.Find("CN=UserName");
myDEUser.Invoke("SetPassword", new object[] { "NewPassword" });
myDEUser.Properties["LockOutTime"].Value = 0;
// the following 2 lines are free =)
myDEUser.Properties["userAccountControl"].Value = (int)myDEUser.Properties["userAccountControl"].Value | 0x10000; // don't expire password
myDEUser.Properties["userAccountControl"].Value = (int)myDEUser.Properties["userAccountControl"].Value & ~0x0002; // ensure account is enabled
myDEUser.CommitChanges();

Incorrect windows identity in Cassini-Dev when hosted in Windows Service

I am hosting CassiniDev 4.0 in my windows service running an MVC 3.0 configuration site for my service.
I have the web.config setup to use windows authentication. When I look at the HttpContext.User in the web site, it shows the identity that the service is running under, not the itentity of the user making the request. The User.AuthenticationType is NTLM, which is correct, BTW.
This seems pretty clearly to be a bug, but wanted to run it by the community to see if there is some configuration I am missing.
It seems like it might be a variation on this issue postedlast week:
SecurityIdentifiers in Cassini-dev's NTLM authentication
This is definitely a bug in Cassini Dev. It looks like this method is returning the wrong token: Request.GetUserToken(). The code:
public override IntPtr GetUserToken()
{
return _host.GetProcessToken();
}
And here _host.GetProcessToken() is a pointer to a security token belonging to the user who owns the Cassini process, it is not the token belonging to the user that's logged in. What needs to happen is the NtlmAuth object needs to pass the security token back to the Request object so that it can be returned when this method is called instead of the host's token. Not really sure what the best way to do this is but you can see in the NtlmAuth class, the security token is acquired here:
IntPtr phToken = IntPtr.Zero;
if (Interop.QuerySecurityContextToken(ref _securityContext, ref phToken) != 0)
{
return false;
}
phToken is the security token but it needs to get back to the Request object and not call Interop.CloseHandle(phToken); later in that method, where it frees the token. Note that CloseHandle() needs to be called on the token eventually, otherwise a new one will be issued for every request made by a logged in user but unused ones will never get freed. One possible place to do this is in the Request object, which subclasses SimpleWorkerRequest and you can override the EndOfRequest method to call CloseHandle() on the security token.

Starting Process from ASP.NET

I'm running an ASP web application that should start a Powershell script on the server. To run this Powershell script a lot of Domain rights are needed. So I run the apppool under a user that has all the rights.
But when I start the powershellscript I alway get the that the access is denied.
Has any one an idea how to solve the problem?
When I start a process as described, is the process running under the usercontext of the app pool or under the usercontext of the user which is logged in in the ASP.NET web application?
I'ver tried two methods
1.
string cmdArg = "C:\\Scripts\\test.ps1 " + username;
Runspace runspace = RunspaceFactory.CreateRunspace();
runspace.Open();
Pipeline pipeline = runspace.CreatePipeline();
pipeline.Commands.AddScript(cmdArg);
pipeline.Commands[0].MergeMyResults(PipelineResultTypes.Error, PipelineResultTypes.Output);
Collection<PSObject> results = pipeline.Invoke();
runspace.Close();
StringBuilder stringBuilder = new StringBuilder();
foreach (PSObject obj in results)
{
stringBuilder.AppendLine(obj.ToString());
string test = Environment.UserName;
}
return results[0].ToString();
2.
string cmdArg = "C:\\Scripts\\test.ps1 " + username;
Process myProcess = new Process();
ProcessStartInfo myProcessStartInfo = new ProcessStartInfo("powershell.exe",cmdArg);
myProcessStartInfo.UseShellExecute = false;
myProcessStartInfo.RedirectStandardOutput = true;
myProcess.StartInfo = myProcessStartInfo;
myProcess.Start();
StreamReader myStreamReader = myProcess.StandardOutput;
myProcess.WaitForExit();
string myString = myStreamReader.ReadLine();
return myString;
Ok, you think running the Apppool with these grand permissions is not best practise.
What about puting a webservice between? The webservice is in an appdomain that is only reachable from localhost?
Update
Ok, I've written an asp.net webservice. The webservice runs in an applicationpool with all rights but is only reachable from localhost. The webservice contains the code to start the script. The ASP MVC3 webapplication is running in a applicationpool with nearly no rights.
But when the webmethod is executed I always get an error that tell me, that I haven't enought rights. I tried to set the impersonate in the webconfig false, but without success.
Does anyone know how to solve this probleme?
Update:
I've read out the current user who execute the powershell when I start it from the webservice. I says it is the user who've got all rights. But the ps throws Errors like: you can't start a method with value null.
Then I've tried to run the ps with runsas as a low level user. I get the same errors.
Then I've tried to run the ps with the same user as in the webservice and everything worked!
Is there anyone who could explain this phenomenon?
And what is the different between my code above and a runas? (same user context)
thanks a lot!
Starting a new process in a HTTP request is not great for performance and it may also be a security risk. However, before ASP.NET and other modern web servers was available the only way to serve content (besides static files) was to execute a program in a separate process.
The API for doing this called the Common Gateway Interface (CGI) and still supported by IIS. If configured correctly you can execute a program on each request.
I'm not sure that you can use CGI to execute a script but then you can create an ISAPI filter that will execute PowerShell on files having extension .ps1. This is basically how for instance php.exe is executed in a new process when a file with extension .php is requested.
Enabling executable content can and should be done on a folder-by-folder basis to limit the security risk. In general you should avoid mixing different kinds of content, ie. it should not be possible to both view a script and also execute it.
If you intention is to be able to remotely run PowerShell scripts and not much else it should also be easy to write a small HTTP server in PowerShell completely removing IIS and ASP.NET from the equation.
I suppose this merely depends on the impersonation settings, if impersonation is enabled, then the currently logged in user is used, otherwise the app pool user

ASP.Net - Using Basic Authentication without having Windows Users

We have an ASP.Net web application running on IIS6 that manages its own database of users.
The site itself just allows anonymous access and all authentication/security is managed using our application itself.
We have a page that contains an HTML table of data that we import into Excel and is then used for Reporting purposes. The page currently has no security implemented.
We need to add security to this page so that should these spreadsheets fall into the wrong hands then the data cannot be "Refreshed" without supplying a username / password.
If I set this page to not allow Anonymouse access then I can use Basic/Windows authentication with Windows Users in order to secure this page. Then when Excel refreshes the data the password dialog box pops up.
The problem is that I need to be able to secure this page based on the Users within our database and they will not be Windows users. I also need to do it in such a way that allows Excel to manage the authentication which excludes any Form based authentication.
Anyone got any ideas? Is it possible to get IIS to look elsewhere for it's Basic Authentication?
Ok, so I've found two solutions to this problem. One thanks to Zhaph - Ben Duguid's answer which is an HttpModule that allows ASP.Net to fully manage the authentication.
The second solution, and the one that I am going with, is thanks to this question/answer.
HTTP Authentication (Basic or Digest) in ASP Classic via IIS
I've stripped this down and have a simple test harness that seems to be working well. In this example, instead of a database call, it merely checks that the username and password match and considers that authenticated.
using System;
using System.Text;
namespace AuthenticationTests
{
public partial class _Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
string authorisationHeader = Request.ServerVariables["HTTP_AUTHORIZATION"];
if (authorisationHeader != null && authorisationHeader.StartsWith("Basic ", StringComparison.InvariantCultureIgnoreCase))
{
string authorizationParameters = Encoding.Default.GetString(Convert.FromBase64String(authorisationHeader.Substring("Basic ".Length)));
string userName = authorizationParameters.Split(':')[0];
string password = authorizationParameters.Split(':')[1];
if (userName == password) //Perform your actual "login" check here.
{
//Authorised!
//Page loads as normal.
}
else
{
Unauthorised();
}
}
else
{
Unauthorised();
}
}
private void Unauthorised()
{
Response.AddHeader("WWW-Authenticate", "Basic");
Response.Status = "401 Unauthorized";
Response.End();
}
}
}
As you've got a custom database of users, I'd recommend looking at building a quick membership provider that talks to your database schema.
MSDN has a good example on "How to: Sample Membership Provider".
You can then use the standard access control mechanisms of ASP.NET to lock down the page, require authentication, etc, along with controls like Login, LoginStatus and others to provide much of the UI you need.
Edit to add
A quick search found the following, which might help:
Web Service Security - Basic HTTP Authentication without Active Directory
Where Greg Reinacker presents "a fully working sample in 100% managed code demonstrating the use of HTTP Basic authentication, using a separate credential store (in this case, a XML file, although this would be easy to change to a database or LDAP store)."
I'm not an expert but I thought that the point of Basic was that it was Windows Authentication. Can you run a script to synchronise your DB users with your Active Directory?
If it's a corporate AD, you could consider having a second AD just for your app and synchronising users from both your corporate AD and your DB. If you don't need to synchronise passwords (e.g. build a pwd-mgmt page in your site) you could just use scripts or C# or something. If you want something more sophisticated with built-in password synchronisation, you could look at ILM 2007 (soon to be FIM 2010).
Is the page an .html file or an .aspx file?
If it's an .aspx, you should keep this page under anonymous access and check for authentication in the page logic itself
I've written a library named FSCAuth that may help with this. It trivially can be set up for just basic authentication without Active Directory. It will instead read your user data out of a database/file/wherever(there is even a memory-only UserStore)
It is BSD licensed at Binpress

Resources