Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revision Previous revision
ms_windows_ms_sql:web_gui_for_powershell_scripts [2019/05/15 09:49] – [Еще немного полезных ссылок] adminms_windows_ms_sql:web_gui_for_powershell_scripts [2019/05/15 14:03] (current) – [Запуск процесса (powershell.exe) от имени конкретного пользователя] admin
Line 1: Line 1:
 +====== PowerShell using ASP.NET application ======
 +У меня получилось сделать все что нужно (запустить от имени пользователя оболочку powershell, передать ей скрипт и получить обратно консольный вывод с помощью информации из этих ссылок): \\
 +Описано расширение стандартного класса **Process** и его примемение: https://odetocode.com/blogs/scott/archive/2004/10/29/createprocessasuser.aspx \\
 +Вот тут написано как вызывать эти методы: http://rsdn.org/forum/dotnet/3287068.1 \\
 +\\
 +Если необходимо, чтобы **Application Pool** работал в **Integrated**, то вызов методов, которые запускают процесс, нужно обертывать в **using (WindowsImpersonationContext wic = clientId.Impersonate())**. Об этом рассказано тут: https://stackoverflow.com/questions/12966286/impersonate-domain-user-with-integrated-pipeline .\\
 +\\
 +Например, вот обработчик нажатия кнопки, который запускает **powershell**-скрипт от имени авторизованного пользователя:
 +<code>protected void ExecuteCode_Click(object sender, EventArgs e)
 +        {
  
 +            ResultBox.Text = string.Empty;
 +
 +            var current = System.Security.Principal.WindowsIdentity.GetCurrent();
 +            WindowsIdentity clientId = (WindowsIdentity)User.Identity;
 +
 +            using (WindowsImpersonationContext wic = clientId.Impersonate())
 +            {
 +                UserSpecificProcess myProcess = new UserSpecificProcess();
 +                myProcess.StartInfo.FileName = "powershell.exe";
 +                myProcess.StartInfo.Arguments = @"-File c:\temp\test.ps1";
 +                myProcess.StartInfo.UseShellExecute = false;
 +                myProcess.StartInfo.CreateNoWindow = false;
 +                myProcess.StartInfo.WorkingDirectory = @"c:\temp\";
 +                myProcess.StartAsUser();
 +                StreamReader output = myProcess.StandardOutput;
 +
 +                string line;
 +                StringBuilder builder = new StringBuilder();
 +                while ((line = output.ReadLine()) != null)
 +                {
 +                    // We use a string builder ton create our result text
 +                    builder.Append(line + "\r\n");
 +                }
 +
 +                // Encode the string in HTML (prevent security issue with 'dangerous' caracters like < >
 +                ResultBox.Text = Server.HtmlEncode(builder.ToString());
 +            }
 +        }
 +</code>
 +
 +===== Пример ASP.NET-приложения, запускающего скрипт от имени залогиненного пользователя =====
 +В итоге вот что у меня получилось. {{ :ms_windows_ms_sql:powershellexecution_source_solution.rar |Солюшн для работы в VisualStudio 2017}} и {{ :ms_windows_ms_sql:powershellexecution_published_app.rar |опубликованное приложение}}, которое имеет кнопку для запуска **powershell**-скрипта (c:\temp\test.ps1) и окошко для вывода результатов. \\
 +Протестировано на **IIS** под **Windows Server 2012R2**. **Application pool .NET 4** работает в **Integrated Mode**, от имени **Local System**. \\
 +Приложение опубликовано без каких-либо двоичных файлов. То есть изменения можно вносить прямо в код опубликованного приложения.
 +===== IIS Impersonation и делегирование прав доступа к ресурсам =====
 +На эту тему написано очень много. Обычно это именуется **impersonation double hop issue**. Имеется в виду следующее. \\
 +Когда пользователь успешно имперсонирован (олицетворен) в приложении **ASP.NET**, то он получает доступ к ресурсам **СЕРВЕРА**, на котором работает приложение от своего имени. То есть если он обращается к файлам или **SQL**-базе, размещенной на этом сервере, то всё работает. Однако, если приложение пытается получить доступ к ресурсам, размещенным не на этом сервере, а например к файлам на сетевом ресурсе или **SQL**-базе на другом сервере, то ничего не выйдет. \\
 +Для того, чтобы все работало, необходимо, чтобы СЕРВЕР на котором исполняется приложение имел право что-то делать от имени пользователя, то есть - представлять пользователя. Это называется делегированием. Пользователь, аутентифицированный на сервере приложений **ASP.NET**, делегирует свои права серверу. \\
 +Права серверу на делегирование пользователей выдаются с помощью оснастки **Active Directory Users and Computers**. Нужно открыть свойства учетной записи сервера, на котором исполняется приложение **ASP.NET**, перейти на вкладку **Delegation** и включить делегирование. \\
 +При этом, если приложение **ASP.NET** запускает локальный процесс на web-сервере (например - **powershell**), который осуществляет доступ к нелокальным ресурсам (например подключается к экземплярам **Microsoft Exchange** или запрашивает данные **Active Directory**), то делегирование не требуется. Достаточно, чтобы  имперсонированный пользователь был авторизован методом **Kerberos**, а процесс был запущен от имени этого пользователя. 
 +==== Когда Windows Authentication и делегирование работает на **localhost**, но не работает на FQDN ====
 +Еще один важный момент. \\
 +Часто можно наблюдать ситуацию, когда при доступе к **ASP.NET**-приложению непосредственно с самого сервера по адресу **localhost** все работает. И прозрачная аутентификация и делегирование. Однако, если попытаться зайти на страничку приложения по **FQDN**-имени сервера (даже с самого этого сервера), то страничка запрашивает имя пользователя и пароль (хотя при заходе по **localhost** работает прозрачная аутентификация), нормально аутентифицирует пользователя и работает имперсонация, однако делегирование не работает и доступ к ресурсам вне **web**-сервера запрещен. \\
 +Это происходит из-за того, что **FQDN**-имя сервера не внесено в список **Intranet** (локальных) сайтов в конфигурации браузера. В результате, браузер аутентифицирует пользователя не по протоколу **Kerberos**, а по протоколу **NTLM**, а протокол **NTLM** не позволяет делегирование. Об этом мне удалось прочитать тут: http://www.justskins.com/forums/iis-and-fqdn-authentication-183966.html# \\
 +А вот тут подробнее от **Microsoft**: https://blogs.msdn.microsoft.com/besidethepoint/2010/05/08/double-hop-authentication-why-ntlm-fails-and-kerberos-works/ \\
 +Вот тут: https://blogs.msdn.microsoft.com/friis/2013/01/08/asp-net-authentication-test-page/ \\
 +приведен пример тестовой страницы, которая показывает детальную информацию о методе аутентификации текущего пользователя. Утащил к себе - {{ :ms_windows_ms_sql:authpage.zip | authpage.zip}}.
 +
 +==== Настройки браузеров для корректной работы имперсонации ====
 +=== Internet Explorer ===
 +Параметр настроек **Internet Explorer**, который предписывает использовать **KERBEROS** находится в  **Internet Explorer Optinons -> Security -> Custom Level** и называется **Automatic logon with current user name and password**. Если он включен, то первым для аутентификации используется KERBEROS, а в случае неудачи - NTLM. Важно знать, что включенный параметр **Prompt for user name and password** разрешает аутентифицировать пользователей только по NTLM. === Firefox ===
 +Если для доступа к приложению используется **Firefox**, то для того, чтобы он аутентифицировался по **KERBROS** адрес сайта приложения нужно добавить в доверенные. Для этого открыввем в **Firefox** страницу **about:config**. В поле поиска вводим **negotiate**. Дважды кликаем по параметру **network.negotiate-auth.trusted-uris** и добавляем туда адрес сайта (например - **example.com**).
 +=== Chrome ===
 +**Chrome** в **windows** использует настройки **Internet Explorer**. \\
 +В других ОС передать Chrome список доверенных сайтов можно с помощью аргумента командной строки: 
 +<code>--auth-negotiate-delegate-whitelist="*.adexample.pingidentity.com"</code>
 +
 +
 +
 +
 +==== Чтобы в приложении ASP.NET работала сквозная Windows-аутентификация, имперсонация и делегирование ====
 +В итоге. Чтобы в приложении **ASP.NET** работала сквозная **Windows**-аутентификация, имперсонация и делегирование необходимо соблюдение условий:
 +  * Для приложения вцелом (**Classic Application Pool mode**) или для куска кода (**Integrated Application Pool mode**) включена имперсонация.
 +  * Для приложения отключена аутентификация анонимных пользователей, потому, что **IIS** применяет первый успешный метод аутентификации.
 +  * Принудительно включен **Kerberos** (**Windows Authentication** -> **Providers** (справа) оставитьтолько **Negotiate:Kerberos**)
 +  * Если адрес к которому обращаются пользователи отличается от имени web-сервера, то для учетки web-сервера должен быть корректно прописан **SPN**. Толково написано тут: http://winitpro.ru/index.php/2016/05/18/nastrojka-kerberos-avtorizacii-na-sajte-iis/
 +  * Для учетной записи web-сервера разрешено делегирование.
 +  * Адрес сайта, на котором размещено приложение добавлен в список **Интранет** сайтов.
 +В противном случае - не будет корректно работать делегирование и будут ошибки при доступе к сетевым ресурсам и **Active Directory** (ошибки типа **Exception calling "GetComputerSite" with "0" argument(s): "An operations error occurred."** \\
 +Траблшутинг аутентификации Kerberos толково описан тут: https://blogs.msdn.microsoft.com/friis/2009/12/31/things-to-check-when-kerberos-authentication-fails-using-iisie/
 +
 +Для одного из сервисов я описал настройку web-сервера на базе Windows Server 2016: {{ :ms_windows_ms_sql:ams_server.docx | IIS Web Server Setup for correct impersonating }}
 +
 +===== Еще немного полезных ссылок =====
 +Вот этот гайд описывает как запускать процесс от имени имперсонированного пользователя. Вторая ссылка - как получить то что выводится в стандартный вывод. \\
 +ASP.NET Run Process As Impersonated User: https://support.microsoft.com/en-us/help/889251/how-to-spawn-a-process-that-runs-under-the-context-of-the-impersonated \\
 +Вот тут написано как человек пытается получить вывод от процесса запущенного с помощью **CreateProcessAsUser**: \\
 +https://groups.google.com/forum/#!topic/microsoft.public.dotnet.framework.interop/dM6jE-3eRq4 \\
 +А вот тут представлен класс, который по идее это все делает. но у меня не получилось :(
 +https://stackoverflow.com/questions/27986399/capture-standard-output-from-createprocessasuser-in-c-sharp \\
 +\\
 +Вот тут написано как из приложения ASP.NET запустить процесс от имени импесонированного пользователя, указав имя и пароль (**Impersonate a Specific User in Code**): https://support.microsoft.com/en-us/help/306158/how-to-implement-impersonation-in-an-asp-net-application
 +\\
 +Вот тут понятные классы, которые помогают запустить процесс от имени конкретного пользователя: https://habr.com/ru/post/135299/ \\
 +
 +Вот ссылки описывают как создать проект, однако в них не работает имперсонализация пользователя. 
 +http://jeffmurr.com/blog/?p=142 \\
 +https://incoherenttruth.wordpress.com/2011/01/05/execute-powershell-cmdlets-from-asp-net-with-user-impersonation/ \\
 +Change ASP.NET Settings per pool. https://weblogs.asp.net/owscott/setting-an-aspnet-config-file-per-application-pool \\
 +https://discussions.citrix.com/topic/277547-impersonation-seems-not-to-flow-to-powershell-runspace/ \\
 +
 +====== PowerShell UI Using Jenkins ======
 +https://hodgkins.io/automating-with-jenkins-and-powershell-on-windows-part-1 \\
 +https://hodgkins.io/automating-with-jenkins-and-powershell-on-windows-part-2 \\
 +
 +====== Powershell UI Using ASP ======
 +http://waynes-world-it.blogspot.ru/2008/05/running-powersell-scripts-from-aspnet.html
 +http://elbsolutions.com/projects/making-powershell-script-run-aspx-worked-12-hour/
 +
 +Для создания **web**-страницы, которая сможет запускать **PowerShell**-скрипт потребуется:
 +  * Разместить **aspx**-файл в корневой папке web-проекта.
 +  * Разместить **web.config** в корневой папке web-проекта.
 +  * Если web-приложения работает от имени **Network Service**, то дать этой службе права на чтение.
 +  * Собственно сам скрипт **PowerShell** 
 +  * Библиотека (dll) **System.Management.Automation** в папке **.\bin** 
 +Для вывода строк следует использовать **Out-String**. Для передачи параметров следует использовать **$args**.
 +
 +===== web.config =====
 +<code><?xml version="1.0" encoding="utf-8"?>
 +<configuration>
 +    <system.net>
 + <defaultProxy enabled="false"/>
 +    </system.net>
 +    <system.web>
 + <pages validateRequest="false"/>
 + <compilation defaultLanguage="c#" debug="false"/>
 + <customErrors mode="Off"/>
 + <authentication mode="Windows"/>
 + <identity impersonate="true"/>
 +    </system.web>
 +</configuration>
 +</code>
 +
 +===== Powershell скрипт test.ps1 =====
 +<code>if ($args.count -ge 2)
 +{ $text = $args[1]}
 +write-output $text
 +</code>
 +
 +===== Страница aspx - Test.aspx =====
 +<code><%@ Page language="c#" AutoEventWireup="true" Debug="true" %>
 +<%@ Import Namespace="System" %>
 +<%@ Import Namespace="System.IO" %>
 +<%@ Import Namespace="System.Management.Automation.Runspaces" %>
 +<%@ Import Namespace="System.Management.Automation" %>
 +<%@ Import Namespace="System.Collections.ObjectModel" %>
 +
 +
 +<script language="C#" runat="server">
 +
 +// The previous lines use <%...%> to indicate script code, and they specify the namespaces to import. As mentioned earlier, the assemblies must be located in the \Bin subdirectory of the application's starting point.
 +// http://msdn.microsoft.com/en-us/library/aa309354(VS.71).aspx
 +
 +
 +private void Button3_Click(object sender, System.EventArgs e)
 +{
 +String fp = Server.MapPath(".") + "\\" + tPowerShellScriptName.Text;
 +StreamReader sr = new StreamReader(fp);
 +tPowerShellScriptCode.Text = sr.ReadToEnd();
 +sr.Close();
 +}
 +
 +private void Button2_Click(object sender, System.EventArgs e)
 +{
 +tPowerShellScriptResult.Text = RunScript(tPowerShellScriptCode.Text);
 +}
 +
 +// http://msdn.microsoft.com/en-us/library/ms714635(VS.85).aspx
 +
 +private string RunScript(string scriptText)
 +{
 +Runspace runspace = RunspaceFactory.CreateRunspace();
 +runspace.Open();
 +Pipeline pipeline = runspace.CreatePipeline();
 +
 +// Create a new runspaces.command object of type script
 +Command cmdScript = new Command(scriptText, true, false);
 +cmdScript.Parameters.Add("-t", txtInput.Text);
 +pipeline.Commands.Add(cmdScript);
 +//You could also use: pipeline.Commands.AddScript(scriptText);
 +
 +// Re-format all output to strings
 +pipeline.Commands.Add("Out-String");
 +
 +// Invoke the pipeline
 +Collection<PSObject> results = pipeline.Invoke();
 +
 +//String sresults = pipeline.Output.Count.ToString();
 +//sresults = sresults + "," + results.Count.ToString();
 +String sresults = "";
 +
 +foreach (PSObject obj in results)
 +{
 +sresults = sresults + obj.ToString();
 +}
 +
 +// close the runspace and set to null
 +runspace.Close();
 +runspace = null;
 +
 +return sresults;
 +}
 +
 +
 +</script>
 +
 +<form id="Form1" method="post" runat="server">
 +<P> <asp:Label id="Label1" runat="server" Width="104px">Parameter:</asp:Label>
 +<asp:TextBox id="txtInput" runat="server"></asp:TextBox></P>
 +<P> <asp:Button id="Button3" runat="server" Text="Load" OnClick="Button3_Click"></asp:Button> </P>
 +<P> <asp:Button id="Button2" runat="server" Text="Run" OnClick="Button2_Click"></asp:Button> </P>
 +<P> <asp:Label id="Label2" runat="server" >Relative script name:</asp:Label>
 +<asp:TextBox id="tPowerShellScriptName" Text="test.ps1" runat="server"></asp:TextBox></P>
 +<P> <asp:TextBox rows="20" columns="120" TextMode="multiline" id="tPowerShellScriptCode" runat="server"></asp:TextBox></P>
 +<P> <asp:TextBox rows="8" columns="120" TextMode="multiline" id="tPowerShellScriptResult" runat="server"></asp:TextBox></P>
 +</form>
 +</code>
 +====== Запуск процессов от имени имперсонированного пользователя ======
 +<code>using System;
 +using System.Runtime.InteropServices;
 +using System.Security.Principal;
 +using System.Text;
 +using System.Diagnostics;
 +using System.IO;
 +using System.ComponentModel;
 +using System.Reflection;
 +using Microsoft.Win32.SafeHandles;
 +
 +namespace vmware_net
 +{
 +
 +
 +    /// <summary>
 +    /// UserSpecificProcess extends the standard Process object to 
 +    /// create new processes under a different user than the calling parent process.
 +    /// Also, the standard output of the child process redirected back to the parent process.
 +    /// This is class is designed to operate inside an ASP.NET web application.
 +    /// The assumption is that the calling thread is operating with an impersonated security token.
 +    /// The this class will change the imperonated security token to a primary token, 
 +    /// and call CreateProcessAsUser.
 +    /// A System.Diagnostics.Process object will be returned with appropriate properties set.
 +    /// To use this function, the following security priviliges need to be set for the ASPNET account 
 +    /// using the local security policy MMC snap-in. CreateProcessAsUser requirement.
 +    /// "Replace a process level token"/SE_ASSIGNPRIMARYTOKEN_NAME/SeAssignPrimaryTokenPrivilege
 +    /// "Adjust memory quotas for a process"/SE_INCREASE_QUOTA_NAME/SeIncreaseQuotaPrivilege
 +    ///
 +    /// This class was designed for .NET 1.1 for operating systems W2k an higher.
 +    /// Any other features or platform support can be implemented by using the .NET reflector and 
 +    /// investigating the Process class.
 +    /// </summary>
 +    public class UserSpecificProcess : Process
 +    {
 +        [StructLayout(LayoutKind.Sequential)]
 +        public class CreateProcessStartupInfo
 +        {
 +            public int cb;
 +            public string lpReserved;
 +            public string lpDesktop;
 +            public string lpTitle;
 +            public int dwX;
 +            public int dwY;
 +            public int dwXSize;
 +            public int dwYSize;
 +            public int dwXCountChars;
 +            public int dwYCountChars;
 +            public int dwFillAttribute;
 +            public int dwFlags;
 +            public short wShowWindow;
 +            public short cbReserved2;
 +            public IntPtr lpReserved2;
 +            public IntPtr hStdInput;
 +            public IntPtr hStdOutput;
 +            public IntPtr hStdError;
 +            public CreateProcessStartupInfo()
 +            {
 +                this.cb = Marshal.SizeOf(typeof(CreateProcessStartupInfo));
 +                this.lpReserved = null;
 +                this.lpDesktop = null;
 +                this.lpTitle = null;
 +                this.dwX = 0;
 +                this.dwY = 0;
 +                this.dwXSize = 0;
 +                this.dwYSize = 0;
 +                this.dwXCountChars = 0;
 +                this.dwYCountChars = 0;
 +                this.dwFillAttribute = 0;
 +                this.dwFlags = 0;
 +                this.wShowWindow = 0;
 +                this.cbReserved2 = 0;
 +                this.lpReserved2 = IntPtr.Zero;
 +                this.hStdInput = IntPtr.Zero;
 +                this.hStdOutput = IntPtr.Zero;
 +                this.hStdError = IntPtr.Zero;
 +            }
 +        }
 +
 +        [StructLayout(LayoutKind.Sequential)]
 +        public class CreateProcessProcessInformation
 +        {
 +            public IntPtr hProcess;
 +            public IntPtr hThread;
 +            public int dwProcessId;
 +            public int dwThreadId;
 +            public CreateProcessProcessInformation()
 +            {
 +                this.hProcess = IntPtr.Zero;
 +                this.hThread = IntPtr.Zero;
 +                this.dwProcessId = 0;
 +                this.dwThreadId = 0;
 +            }
 +        }
 +
 +        [StructLayout(LayoutKind.Sequential)]
 +        public class SecurityAttributes
 +        {
 +            public int nLength;
 +            public IntPtr lpSecurityDescriptor;
 +            public bool bInheritHandle;
 +            public SecurityAttributes()
 +            {
 +                this.nLength = Marshal.SizeOf(typeof(SecurityAttributes));
 +            }
 +        }
 +
 +        [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true, ExactSpelling = true)]
 +        public static extern bool CloseHandle(HandleRef handle);
 +
 +        [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
 +        public static extern bool
 +        CreateProcess([MarshalAs(UnmanagedType.LPTStr)] string lpApplicationName, StringBuilder lpCommandLine, SecurityAttributes lpProcessAttributes, SecurityAttributes lpThreadAttributes, bool bInheritHandles, int dwCreationFlags, IntPtr lpEnvironment, [MarshalAs(UnmanagedType.LPTStr)] string lpCurrentDirectory, CreateProcessStartupInfo lpStartupInfo, CreateProcessProcessInformation lpProcessInformation);
 +
 +        [DllImport("advapi32.dll")]
 +        public static extern int LogonUserA(String lpszUserName, string lpszDomain, string lpszPassword, int dwLogonType, int dwLogonProvider, ref IntPtr phToken);
 +
 +        [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
 +        public static extern bool CreateProcessAsUserW(IntPtr token, [MarshalAs(UnmanagedType.LPTStr)] string lpApplicationName, [MarshalAs(UnmanagedType.LPTStr)] string lpCommandLine, SecurityAttributes lpProcessAttributes, SecurityAttributes lpThreadAttributes, bool bInheritHandles, int dwCreationFlags, IntPtr lpEnvironment, [MarshalAs(UnmanagedType.LPTStr)] string lpCurrentDirectory, CreateProcessStartupInfo lpStartupInfo, CreateProcessProcessInformation lpProcessInformation);
 +
 +        [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
 +        public static extern bool CreateProcessAsUser(IntPtr token, [MarshalAs(UnmanagedType.LPTStr)] string lpApplicationName, [MarshalAs(UnmanagedType.LPTStr)] string lpCommandLine, SecurityAttributes lpProcessAttributes, SecurityAttributes lpThreadAttributes, bool bInheritHandles, int dwCreationFlags, IntPtr lpEnvironment, [MarshalAs(UnmanagedType.LPTStr)] string lpCurrentDirectory, CreateProcessStartupInfo lpStartupInfo, CreateProcessProcessInformation lpProcessInformation);
 +
 +        [DllImport("kernel32.dll", CharSet = CharSet.Ansi, SetLastError = true)]
 +        public static extern IntPtr GetStdHandle(int whichHandle);
 +
 +        [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
 +        public static extern IntPtr CreateFile(string lpFileName, int dwDesiredAccess, int dwShareMode, SecurityAttributes lpSecurityAttributes, int dwCreationDisposition, int dwFlagsAndAttributes, HandleRef hTemplateFile);
 +
 +        [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
 +        public static extern IntPtr CreateNamedPipe(string name, int openMode, int pipeMode, int maxInstances, int outBufSize, int inBufSize, int timeout, SecurityAttributes lpPipeAttributes);
 +
 +        [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
 +        public static extern int GetConsoleOutputCP();
 +
 +        [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
 +        public static extern bool DuplicateTokenEx(HandleRef hToken, int access, SecurityAttributes tokenAttributes, int impersonationLevel, int tokenType, ref IntPtr hNewToken);
 +
 +        // WinNT.h ACCESS TYPES
 +        const int GENERIC_ALL = 0x10000000;
 +
 +        // WinNT.h enum SECURITY_IMPERSONATION_LEVEL
 +        const int SECURITY_IMPERSONATION = 2;
 +
 +        // WinNT.h TOKEN TYPE
 +        const int TOKEN_PRIMARY = 1;
 +
 +        // WinBase.h
 +        const int STD_INPUT_HANDLE = -10;
 +        const int STD_ERROR_HANDLE = -12;
 +
 +        // WinBase.h STARTUPINFO
 +        const int STARTF_USESTDHANDLES = 0x100;
 +
 +        // Microsoft.Win23.NativeMethods
 +        static IntPtr INVALID_HANDLE_VALUE = (IntPtr)(-1);
 +        public static HandleRef NullHandleRef = new HandleRef(null, IntPtr.Zero);
 +
 +        public const int LOGON32_LOGON_INTERACTIVE = 2;
 +        public const int LOGON32_PROVIDER_DEFAULT = 0;
 +
 +        /// <summary>
 +        /// Starts the process with the security token of the calling thread.
 +        /// If the security token has a token type of TokenImpersonation,
 +        /// the token will be duplicated to a primary token before calling
 +        /// CreateProcessAsUser.
 +        /// </summary>
 +        /// <param name="process">The process to start.</param>
 +        public void StartAsUser()
 +        {
 +            StartAsUser(WindowsIdentity.GetCurrent().Token);
 +        }
 +
 +        /// <summary>
 +        /// Starts the process with the given security token.
 +        /// If the security token has a token type of TokenImpersonation,
 +        /// the token will be duplicated to a primary token before calling
 +        /// CreateProcessAsUser.
 +        /// </summary>
 +        /// <param name="process"></param>
 +        public void StartAsUser(IntPtr userToken)
 +        {
 +            if (StartInfo.UseShellExecute)
 +            {
 +                throw new InvalidOperationException("can't call this with shell execute");
 +            }
 +
 +            // just assume that the securityToken is of TokenImpersonation and create a primary.
 +            IntPtr primayUserToken = CreatePrimaryToken(userToken);
 +
 +            CreateProcessStartupInfo startupInfo = new CreateProcessStartupInfo();
 +            CreateProcessProcessInformation processInformation = new CreateProcessProcessInformation();
 +
 +            IntPtr stdinHandle;
 +            IntPtr stdoutReadHandle;
 +            IntPtr stdoutWriteHandle = IntPtr.Zero;
 +            IntPtr stderrHandle;
 +            try
 +            {
 +                stdinHandle = GetStdHandle(STD_INPUT_HANDLE);
 +                MyCreatePipe(out stdoutReadHandle, out stdoutWriteHandle, false);
 +                stderrHandle = GetStdHandle(STD_ERROR_HANDLE);
 +
 +                //assign handles to startup info
 +                startupInfo.dwFlags = STARTF_USESTDHANDLES;
 +                startupInfo.hStdInput = stdinHandle;
 +                startupInfo.hStdOutput = stdoutWriteHandle;
 +                startupInfo.hStdError = stderrHandle;
 +
 +                string commandLine = GetCommandLine();
 +                //Set Creation Flags value 0x8000000 to hide window. To show window set it to 0
 +                int creationFlags = 0x8000000;
 +                //int creationFlags = 0;
 +                IntPtr environment = IntPtr.Zero;
 +                string workingDirectory = GetWorkingDirectory();
 +
 +                // create the process or fail trying.
 +                //if (!CreateProcessAsUserW(
 +                if (!CreateProcessAsUser(
 +                primayUserToken,
 +                null,
 +                commandLine,
 +                null,
 +                null,
 +                true,
 +                creationFlags,
 +                environment,
 +                workingDirectory,
 +                startupInfo,
 +                processInformation))
 +                {
 +                    throw new Win32Exception();
 +                }
 +            }
 +            finally
 +            {
 +                // close thread handle
 +                if (processInformation.hThread != INVALID_HANDLE_VALUE)
 +                {
 +                    CloseHandle(new HandleRef(this, processInformation.hThread));
 +                }
 +
 +                // close client stdout handle
 +                CloseHandle(new HandleRef(this, stdoutWriteHandle));
 +            }
 +            // }
 +            // get reader for standard output from the child
 +            Encoding encoding = Encoding.GetEncoding(GetConsoleOutputCP());
 +            //
 +            //StreamReader standardOutput = new StreamReader(new FileStream(stdoutReadHandle, FileAccess.Read, true, 0x1000, true), encoding);
 +            SafeFileHandle SafeStdOutReadHandle = new SafeFileHandle(stdoutReadHandle, true);
 +            StreamReader standardOutput = new StreamReader(new FileStream(SafeStdOutReadHandle, FileAccess.Read, 0x1000, true), encoding);
 +
 +            // set this on the object accordingly.
 +            typeof(Process).InvokeMember("standardOutput",
 +            BindingFlags.SetField | BindingFlags.NonPublic | BindingFlags.Instance,
 +            null, this, new object[] { standardOutput });
 +
 +            // scream if a process wasn't started instead of returning false.
 +            if (processInformation.hProcess == IntPtr.Zero)
 +            {
 +                throw new Exception("failed to create process");
 +            }
 +
 +            // configure the properties of the Process object correctly
 +            //typeof(Process).InvokeMember("SetProcessHandle",
 +            //BindingFlags.InvokeMethod | BindingFlags.NonPublic | BindingFlags.Instance,
 +            //null, this, new object[] { processInformation.hProcess });
 +            typeof(Process).InvokeMember("SetProcessId",
 +            BindingFlags.InvokeMethod | BindingFlags.NonPublic | BindingFlags.Instance,
 +            null, this, new object[] { processInformation.dwProcessId });
 +        }
 +
 +        /// <summary>
 +        /// Starts the process as user with provided username, domain name and password.
 +        /// Method loggs user with provided cretential, get token and pass it to StartAsUser method.
 +        /// </summary>
 +        public void StartAsUserWithLogon(string UserName, string Domain, string Password)
 +        {
 +            IntPtr UserToken = IntPtr.Zero;
 +            if (LogonUserA(UserName, Domain, Password, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, ref UserToken) !=0)
 +            {
 +                StartAsUser(CreatePrimaryToken(UserToken));
 +            }
 +            else
 +            {
 +                throw new Win32Exception();
 +            }
 +        }
 +
 +        /// <summary>
 +        /// Creates a primayToken out of an existing token.
 +        /// </summary>
 +        /// <param name="userToken"></param>
 +        private IntPtr CreatePrimaryToken(IntPtr userToken)
 +        {
 +            SecurityAttributes securityAttributes = new SecurityAttributes();
 +            IntPtr primaryUserToken = IntPtr.Zero;
 +            if (!DuplicateTokenEx(new HandleRef(this, userToken), GENERIC_ALL, securityAttributes, SECURITY_IMPERSONATION, TOKEN_PRIMARY, ref
 +            primaryUserToken))
 +            {
 +                throw new Win32Exception();
 +            }
 +            return primaryUserToken;
 +        }
 +
 +        /// <summary>
 +        /// Gets the appropriate commandLine from the process.
 +        /// </summary>
 +        /// <param name="process"></param>
 +        /// <returns></returns>
 +        private string GetCommandLine()
 +        {
 +            StringBuilder builder1 = new StringBuilder();
 +            string text1 = StartInfo.FileName.Trim();
 +            string text2 = StartInfo.Arguments;
 +            bool flag1 = text1.StartsWith("\"") && text1.EndsWith("\"");
 +            if (!flag1)
 +            {
 +                builder1.Append("\"");
 +            }
 +            builder1.Append(text1);
 +            if (!flag1)
 +            {
 +                builder1.Append("\"");
 +            }
 +            if ((text2 != null) && (text2.Length > 0))
 +            {
 +                builder1.Append(" ");
 +                builder1.Append(text2);
 +            }
 +            return builder1.ToString();
 +        }
 +
 +        /// <summary>
 +        /// Gets the working directory or returns null if an empty string was found.
 +        /// </summary>
 +        /// <returns></returns>
 +        private string GetWorkingDirectory()
 +        {
 +            return (StartInfo.WorkingDirectory != string.Empty) ?
 +            StartInfo.WorkingDirectory : null;
 +        }
 +
 +        /// <summary>
 +        /// A clone of Process.CreatePipe. This is only implemented because reflection with
 +        /// out parameters are a pain.
 +        /// Note: This is only finished for w2k and higher machines.
 +        /// </summary>
 +        /// <param name="parentHandle"></param>
 +        /// <param name="childHandle"></param>
 +        /// <param name="parentInputs">Specifies whether the parent will be performing the writes.</param>
 +        public static void MyCreatePipe(out IntPtr parentHandle, out IntPtr childHandle, bool parentInputs)
 +        {
 +            string pipename = @"\\.\pipe\Global\" + Guid.NewGuid().ToString();
 +
 +            SecurityAttributes attributes2 = new SecurityAttributes();
 +            attributes2.bInheritHandle = false;
 +
 +            parentHandle = CreateNamedPipe(pipename, 0x40000003, 0, 0xff, 0x1000, 0x1000, 0, attributes2);
 +            if (parentHandle == INVALID_HANDLE_VALUE)
 +            {
 +                throw new Win32Exception();
 +            }
 +
 +            SecurityAttributes attributes3 = new SecurityAttributes();
 +            attributes3.bInheritHandle = true;
 +            int num1 = 0x40000000;
 +            if (parentInputs)
 +            {
 +                num1 = -2147483648;
 +            }
 +            childHandle = CreateFile(pipename, num1, 3, attributes3, 3, 0x40000080, NullHandleRef);
 +            if (childHandle == INVALID_HANDLE_VALUE)
 +            {
 +                throw new Win32Exception();
 +            }
 +        }
 +    }
 +}
 +</code>
 +===== Примеры использования =====
 +==== Запуск процесса (powershell.exe) от имени залогиненного имперсонированного пользователя ====
 +Для доступа к ресурсам за пределами сервера нужна аутентификация kerberos (обязательно), а также включенная делегация **kerberos** для учетки, от имени которой исполняется **Application Pool**. То есть, например **AppPool Identity** - **LocalSystem**, а для учетки компьютера в AD включена делегация **kerberos**.\\
 +На уровне всего приложения **<identity impersonate="false"/>**. От имперсонированного пользователя запускается только кусок кода в using {...}.
 +<code>
 +//Get User Identity for Initializing Impersonation Context
 +var current = System.Security.Principal.WindowsIdentity.GetCurrent();
 +WindowsIdentity clientId = (WindowsIdentity)User.Identity;
 +string ScriptFilePath = @"";
 +string Arguments = @"";
 +
 +using (WindowsImpersonationContext wic = clientId.Impersonate())
 +{
 +    UserSpecificProcess myProcess = new UserSpecificProcess();
 +    myProcess.StartInfo.FileName = "powershell.exe";
 +    myProcess.StartInfo.Arguments = @"-NoLogo -NoProfile -executionpolicy bypass -File " + ScriptFilePath + " " + Arguments;
 +
 +    //ResultBuilder.Append(ScriptFilePath + " " + Arguments + "\r\n");
 +
 +    myProcess.StartInfo.UseShellExecute = false;
 +    myProcess.StartInfo.CreateNoWindow = false;
 +    myProcess.StartInfo.WorkingDirectory = ScriptWorkingDirectory;
 +    myProcess.StartAsUser();
 +    StreamReader output = myProcess.StandardOutput;
 +
 +    //wait then kill process
 +    if (!myProcess.WaitForExit(ScriptExecutionTimeout))
 +    {
 +        myProcess.Kill();
 +    }
 +
 +    string line;
 +    while ((line = output.ReadLine()) != null)
 +    {
 +        // We use a string builder ton create our result text
 +        ResultBuilder.Append(line + "\r\n");
 +    }
 +}
 +</code>
 +
 +==== Запуск процесса (powershell.exe) от имени конкретного пользователя ====
 +<code>
 +protected void RunScript()
 +{
 +    string ScriptFilePath = @"c:\temp\script.ps1";
 +    string Arguments = @"";
 +    string ScriptWorkingDirectory = @"c:\temp\";
 +    string UserName = "Username";
 +    string Domain = "DomainName_or_MachineName";
 +    string Password = "superpassword";
 +
 +    UserSpecificProcess proc = new UserSpecificProcess();
 +
 +    proc.StartInfo.UseShellExecute = false;
 +    proc.StartInfo.CreateNoWindow = false;
 +    proc.StartInfo.RedirectStandardError = true;
 +    proc.StartInfo.RedirectStandardOutput = true;
 +    proc.StartInfo.FileName = "powershell.exe";
 +    proc.StartInfo.WorkingDirectory = ScriptWorkingDirectory;
 +    proc.StartInfo.Arguments = @"-NoLogo -NoProfile -executionpolicy bypass -command " + '"' + ScriptFilePath + " " + Arguments + '"';
 +
 +    proc.StartAsUserWithLogon(UserName, Domain, Password);
 +
 +    while (!proc.HasExited && proc.Responding)
 +    {
 +        System.Threading.Thread.Sleep(1000);
 +    }
 +
 +</code>         
  • ms_windows_ms_sql/web_gui_for_powershell_scripts.txt
  • Last modified: 2019/05/15 14:03
  • by admin