I saw a comment on Twitter today about a limitation in PowerShell. Specifically the ability to grant the logon as a service right to a user account. Manually, if you use the Services management console and specify the user, Windows will automatically grant that right. But if you are trying to do this from a command line that is a bit more challenging. There has been an old resource kit tool called NTRights which can easily get the job done. And I have no problem calling a command line tool that is designed for a special purpose. But, you first need to get a hold of that utility.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
Another option, which I've also used in the past, is to use some scripting language to modify the local security policy on the fly. Typically this involves creating a database entry and then calling SECEDIT. I came across a PowerShell script that does this but you could easily do it with VBScript. There's nothing uniquely PowerShell about it.
Granting this privilege requires some arcane (at least to me) API calls. In my research I found several examples. Then I came across this great link. The author, Morgan, has a number of options for achieving this task. Unfortunately, his PowerShell solution requires a third party DLL. But fortunately, he also has a C# solution.
Why is this fortunate? Because PowerShell is a management engine with great depth. You can run commands interactively, you can create scripts and advanced functions, or you can leverage languages like C#. I can use Morgan's C# class definition and add it to PowerShell.
#http://www.morgantechspace.com/2013/11/Set-or-Grant-Logon-As-A-Service-right-to-User.html $class=@" public class LsaWrapper { // Import the LSA functions [DllImport("advapi32.dll", PreserveSig = true)] private static extern UInt32 LsaOpenPolicy( ref LSA_UNICODE_STRING SystemName, ref LSA_OBJECT_ATTRIBUTES ObjectAttributes, Int32 DesiredAccess, out IntPtr PolicyHandle ); [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)] private static extern long LsaAddAccountRights( IntPtr PolicyHandle, IntPtr AccountSid, LSA_UNICODE_STRING[] UserRights, long CountOfRights); [DllImport("advapi32")] public static extern void FreeSid(IntPtr pSid); [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true, PreserveSig = true)] private static extern bool LookupAccountName( string lpSystemName, string lpAccountName, IntPtr psid, ref int cbsid, StringBuilder domainName, ref int cbdomainLength, ref int use); [DllImport("advapi32.dll")] private static extern bool IsValidSid(IntPtr pSid); [DllImport("advapi32.dll")] private static extern long LsaClose(IntPtr ObjectHandle); [DllImport("kernel32.dll")] private static extern int GetLastError(); [DllImport("advapi32.dll")] private static extern long LsaNtStatusToWinError(long status); // define the structures private enum LSA_AccessPolicy : long { POLICY_VIEW_LOCAL_INFORMATION = 0x00000001L, POLICY_VIEW_AUDIT_INFORMATION = 0x00000002L, POLICY_GET_PRIVATE_INFORMATION = 0x00000004L, POLICY_TRUST_ADMIN = 0x00000008L, POLICY_CREATE_ACCOUNT = 0x00000010L, POLICY_CREATE_SECRET = 0x00000020L, POLICY_CREATE_PRIVILEGE = 0x00000040L, POLICY_SET_DEFAULT_QUOTA_LIMITS = 0x00000080L, POLICY_SET_AUDIT_REQUIREMENTS = 0x00000100L, POLICY_AUDIT_LOG_ADMIN = 0x00000200L, POLICY_SERVER_ADMIN = 0x00000400L, POLICY_LOOKUP_NAMES = 0x00000800L, POLICY_NOTIFICATION = 0x00001000L } [StructLayout(LayoutKind.Sequential)] private struct LSA_OBJECT_ATTRIBUTES { public int Length; public IntPtr RootDirectory; public readonly LSA_UNICODE_STRING ObjectName; public UInt32 Attributes; public IntPtr SecurityDescriptor; public IntPtr SecurityQualityOfService; } [StructLayout(LayoutKind.Sequential)] private struct LSA_UNICODE_STRING { public UInt16 Length; public UInt16 MaximumLength; public IntPtr Buffer; } /// //Adds a privilege to an account /// Name of an account - "domain\account" or only "account" /// Name ofthe privilege /// The windows error code returned by LsaAddAccountRights public long SetRight(String accountName, String privilegeName) { long winErrorCode = 0; //contains the last error //pointer an size for the SID IntPtr sid = IntPtr.Zero; int sidSize = 0; //StringBuilder and size for the domain name var domainName = new StringBuilder(); int nameSize = 0; //account-type variable for lookup int accountType = 0; //get required buffer size LookupAccountName(String.Empty, accountName, sid, ref sidSize, domainName, ref nameSize, ref accountType); //allocate buffers domainName = new StringBuilder(nameSize); sid = Marshal.AllocHGlobal(sidSize); //lookup the SID for the account bool result = LookupAccountName(String.Empty, accountName, sid, ref sidSize, domainName, ref nameSize, ref accountType); //say what you're doing Console.WriteLine("LookupAccountName result = " + result); Console.WriteLine("IsValidSid: " + IsValidSid(sid)); Console.WriteLine("LookupAccountName domainName: " + domainName); if (!result) { winErrorCode = GetLastError(); Console.WriteLine("LookupAccountName failed: " + winErrorCode); } else { //initialize an empty unicode-string var systemName = new LSA_UNICODE_STRING(); //combine all policies var access = (int) ( LSA_AccessPolicy.POLICY_AUDIT_LOG_ADMIN | LSA_AccessPolicy.POLICY_CREATE_ACCOUNT | LSA_AccessPolicy.POLICY_CREATE_PRIVILEGE | LSA_AccessPolicy.POLICY_CREATE_SECRET | LSA_AccessPolicy.POLICY_GET_PRIVATE_INFORMATION | LSA_AccessPolicy.POLICY_LOOKUP_NAMES | LSA_AccessPolicy.POLICY_NOTIFICATION | LSA_AccessPolicy.POLICY_SERVER_ADMIN | LSA_AccessPolicy.POLICY_SET_AUDIT_REQUIREMENTS | LSA_AccessPolicy.POLICY_SET_DEFAULT_QUOTA_LIMITS | LSA_AccessPolicy.POLICY_TRUST_ADMIN | LSA_AccessPolicy.POLICY_VIEW_AUDIT_INFORMATION | LSA_AccessPolicy.POLICY_VIEW_LOCAL_INFORMATION ); //initialize a pointer for the policy handle IntPtr policyHandle = IntPtr.Zero; //these attributes are not used, but LsaOpenPolicy wants them to exists var ObjectAttributes = new LSA_OBJECT_ATTRIBUTES(); ObjectAttributes.Length = 0; ObjectAttributes.RootDirectory = IntPtr.Zero; ObjectAttributes.Attributes = 0; ObjectAttributes.SecurityDescriptor = IntPtr.Zero; ObjectAttributes.SecurityQualityOfService = IntPtr.Zero; //get a policy handle uint resultPolicy = LsaOpenPolicy(ref systemName, ref ObjectAttributes, access, out policyHandle); winErrorCode = LsaNtStatusToWinError(resultPolicy); if (winErrorCode != 0) { Console.WriteLine("OpenPolicy failed: " + winErrorCode); } else { //Now that we have the SID an the policy, //we can add rights to the account. //initialize an unicode-string for the privilege name var userRights = new LSA_UNICODE_STRING[1]; userRights[0] = new LSA_UNICODE_STRING(); userRights[0].Buffer = Marshal.StringToHGlobalUni(privilegeName); userRights[0].Length = (UInt16) (privilegeName.Length*UnicodeEncoding.CharSize); userRights[0].MaximumLength = (UInt16) ((privilegeName.Length + 1)*UnicodeEncoding.CharSize); //add the right to the account long res = LsaAddAccountRights(policyHandle, sid, userRights, 1); winErrorCode = LsaNtStatusToWinError(res); if (winErrorCode != 0) { Console.WriteLine("LsaAddAccountRights failed: " + winErrorCode); } LsaClose(policyHandle); } FreeSid(sid); } return winErrorCode; } } "@ Add-Type -MemberDefinition $class -Name LSAWrapper -UsingNamespace System.Text -namespace Win32Util
Using Add-Type I can load it into my PowerShell session. Once loaded, I can create an object based on the class.
$lsawrapper = new-object Win32Util.LSAWrapper+LsaWrapper
The class name looks a bit funny but it must be because of how the class is defined. I'm not much of a developer type to know if this could be done any differently, but it works. With the class I can now grant an account the necessary privilege.
$lsawrapper.SetRight("jh-win81-ent\svcTest","SeServiceLogonRight")
The method writes a result to the pipeline.
PS C:\> $w.SetRight("jh-win81-ent\svcTest","SeServiceLogonRight") LookupAccountName result = True IsValidSid: True LookupAccountName domainName: JH-WIN81-ENT 0
The class has a number of Console.WriteLine commands I could remove if I didn't want this level of detail. But it is possible to do this in PowerShell, because of how flexible the engine and language can be. I'm not implying this is easy. In fact, if I were to take this to the next step I'd build some advanced functions around it so it would at least be easy to use for other people. This is a great example of why you should learn PowerShell and how it can impact your career.
Update
Based on a comment I've revised the original class.
$class=@" using System.Text; using System; using System.Runtime.InteropServices; public static class LsaWrapper { // Import the LSA functions [DllImport("advapi32.dll", PreserveSig = true)] private static extern UInt32 LsaOpenPolicy( ref LSA_UNICODE_STRING SystemName, ref LSA_OBJECT_ATTRIBUTES ObjectAttributes, Int32 DesiredAccess, out IntPtr PolicyHandle ); [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)] private static extern long LsaAddAccountRights( IntPtr PolicyHandle, IntPtr AccountSid, LSA_UNICODE_STRING[] UserRights, long CountOfRights); [DllImport("advapi32")] public static extern void FreeSid(IntPtr pSid); [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true, PreserveSig = true)] private static extern bool LookupAccountName( string lpSystemName, string lpAccountName, IntPtr psid, ref int cbsid, StringBuilder domainName, ref int cbdomainLength, ref int use); [DllImport("advapi32.dll")] private static extern bool IsValidSid(IntPtr pSid); [DllImport("advapi32.dll")] private static extern long LsaClose(IntPtr ObjectHandle); [DllImport("kernel32.dll")] private static extern int GetLastError(); [DllImport("advapi32.dll")] private static extern long LsaNtStatusToWinError(long status); // define the structures private enum LSA_AccessPolicy : long { POLICY_VIEW_LOCAL_INFORMATION = 0x00000001L, POLICY_VIEW_AUDIT_INFORMATION = 0x00000002L, POLICY_GET_PRIVATE_INFORMATION = 0x00000004L, POLICY_TRUST_ADMIN = 0x00000008L, POLICY_CREATE_ACCOUNT = 0x00000010L, POLICY_CREATE_SECRET = 0x00000020L, POLICY_CREATE_PRIVILEGE = 0x00000040L, POLICY_SET_DEFAULT_QUOTA_LIMITS = 0x00000080L, POLICY_SET_AUDIT_REQUIREMENTS = 0x00000100L, POLICY_AUDIT_LOG_ADMIN = 0x00000200L, POLICY_SERVER_ADMIN = 0x00000400L, POLICY_LOOKUP_NAMES = 0x00000800L, POLICY_NOTIFICATION = 0x00001000L } [StructLayout(LayoutKind.Sequential)] private struct LSA_OBJECT_ATTRIBUTES { public int Length; public IntPtr RootDirectory; public readonly LSA_UNICODE_STRING ObjectName; public UInt32 Attributes; public IntPtr SecurityDescriptor; public IntPtr SecurityQualityOfService; } [StructLayout(LayoutKind.Sequential)] private struct LSA_UNICODE_STRING { public UInt16 Length; public UInt16 MaximumLength; public IntPtr Buffer; } /// //Adds a privilege to an account /// Name of an account - "domain\account" or only "account" /// Name ofthe privilege /// The windows error code returned by LsaAddAccountRights public static long SetRight(String accountName, String privilegeName) { long winErrorCode = 0; //contains the last error //pointer an size for the SID IntPtr sid = IntPtr.Zero; int sidSize = 0; //StringBuilder and size for the domain name var domainName = new StringBuilder(); int nameSize = 0; //account-type variable for lookup int accountType = 0; //get required buffer size LookupAccountName(String.Empty, accountName, sid, ref sidSize, domainName, ref nameSize, ref accountType); //allocate buffers domainName = new StringBuilder(nameSize); sid = Marshal.AllocHGlobal(sidSize); //lookup the SID for the account bool result = LookupAccountName(String.Empty, accountName, sid, ref sidSize, domainName, ref nameSize, ref accountType); //say what you're doing Console.WriteLine("LookupAccountName result = " + result); Console.WriteLine("IsValidSid: " + IsValidSid(sid)); Console.WriteLine("LookupAccountName domainName: " + domainName); if (!result) { winErrorCode = GetLastError(); Console.WriteLine("LookupAccountName failed: " + winErrorCode); //return winErrorCode; } else { //initialize an empty unicode-string var systemName = new LSA_UNICODE_STRING(); //combine all policies var access = (int) ( LSA_AccessPolicy.POLICY_AUDIT_LOG_ADMIN | LSA_AccessPolicy.POLICY_CREATE_ACCOUNT | LSA_AccessPolicy.POLICY_CREATE_PRIVILEGE | LSA_AccessPolicy.POLICY_CREATE_SECRET | LSA_AccessPolicy.POLICY_GET_PRIVATE_INFORMATION | LSA_AccessPolicy.POLICY_LOOKUP_NAMES | LSA_AccessPolicy.POLICY_NOTIFICATION | LSA_AccessPolicy.POLICY_SERVER_ADMIN | LSA_AccessPolicy.POLICY_SET_AUDIT_REQUIREMENTS | LSA_AccessPolicy.POLICY_SET_DEFAULT_QUOTA_LIMITS | LSA_AccessPolicy.POLICY_TRUST_ADMIN | LSA_AccessPolicy.POLICY_VIEW_AUDIT_INFORMATION | LSA_AccessPolicy.POLICY_VIEW_LOCAL_INFORMATION ); //initialize a pointer for the policy handle IntPtr policyHandle = IntPtr.Zero; //these attributes are not used, but LsaOpenPolicy wants them to exists var ObjectAttributes = new LSA_OBJECT_ATTRIBUTES(); ObjectAttributes.Length = 0; ObjectAttributes.RootDirectory = IntPtr.Zero; ObjectAttributes.Attributes = 0; ObjectAttributes.SecurityDescriptor = IntPtr.Zero; ObjectAttributes.SecurityQualityOfService = IntPtr.Zero; //get a policy handle uint resultPolicy = LsaOpenPolicy(ref systemName, ref ObjectAttributes, access, out policyHandle); winErrorCode = LsaNtStatusToWinError(resultPolicy); if (winErrorCode != 0) { Console.WriteLine("OpenPolicy failed: " + winErrorCode); //return winErrorCode; } else { //Now that we have the SID an the policy, //we can add rights to the account. //initialize an unicode-string for the privilege name var userRights = new LSA_UNICODE_STRING[1]; userRights[0] = new LSA_UNICODE_STRING(); userRights[0].Buffer = Marshal.StringToHGlobalUni(privilegeName); userRights[0].Length = (UInt16) (privilegeName.Length*UnicodeEncoding.CharSize); userRights[0].MaximumLength = (UInt16) ((privilegeName.Length + 1)*UnicodeEncoding.CharSize); //add the right to the account long res = LsaAddAccountRights(policyHandle, sid, userRights, 1); winErrorCode = LsaNtStatusToWinError(res); if (winErrorCode != 0) { Console.WriteLine("LsaAddAccountRights failed: " + winErrorCode); //return winErrorCode; } LsaClose(policyHandle); } FreeSid(sid); } return winErrorCode; } } "@
I'll admit this is beyond what I normally do in PowerShell but I'm willing to learn new things and did with this challenge. With this class it is much easier to add the type and invoke the method.
PS C:\> Add-Type -TypeDefinition $class PS C:\> [lsawrapper]::SetRight("jh-win81-ent\svctest","SeServiceLogonRight") LookupAccountName result = True IsValidSid: True LookupAccountName domainName: JH-WIN81-ENT 0
Here are some tricks to use for API utility functions that make them easier to use.
Change the beginning to:
$class=@”
using System.Text;
using System;
using System.Runtime.InteropServices;
public static class LsaWrapper{
Notice I addeded the references and the “static” declaration. This makes generation of the type cleaner.
Now change the function to static:
public static long SetRight(String accountName, String privilegeName)
Now add the type like this:
Add-Type $class
And call it like this:
[LsaWrapper]::SetRight(‘jh-win81-ent\svcTest’,’SeServiceLogonRight’)
I combine dozens of these into one class. They are easy to load and easy to use.
Excellent. I was trying to use the class definition as I found it. Clearly it wasn’t written with PowerShell in mind.
Thanks for this, it’s great – I can now ditch ntrights.exe for this job. Just one note, it requires at least PowerShell v3 otherwise it complains about the var keyword. From what I can gather this keyword is only supported from .NET 3.5 and later, hence PowerShell needs to be targeting at least that version.
Thanks for that detail. I didn’t test this on anything other than my Windows 8.1 system.
I am the owner/creator/maintainer of the [Carbon](http://get-carbon.org) project, a DevOps module we use to automate the setup of our servers. The code in your post reminded me that I had written something similar.
In fact, I have four privilege-related functions
* [Get-Privilege](http://get-carbon.org/help/Get-Privilege.html)
* [Grant-Privilege](http://get-carbon.org/help/Grant-Privilege.html)
* [Revoke-Privilege](http://get-carbon.org/help/Revoke-Privilege.html)
* [Test-Privilege](http://get-carbon.org/help/Test-Privilege.html)
Enjoy and thanks for the great posts!
thanks. I will have to take a look.
Just need to make sure that your CLR environment can use the .NET 4.0 runtimes: http://stackoverflow.com/questions/2094694/how-can-i-run-powershell-with-the-net-4-runtime