PowerShell Remoting Project Home

Thursday, February 16, 2006

Groups Command in MSH: Get-WindowsIdentity cmdlet

(Added on Mar 6th) There is a follow up post on this topic here.

In Linux Bash shell, there is a handy  groups command, which takes a username as input and return user associated groups. To get similar functionality,  I wrote the Get-WindowsIdentity cmdlet, which take  a MshCredential as input, call LogonUser Win32 API and return a System.Security. Principal.WindowsIdentity object. The bad news, you got to have a username and password. Example:
>get-WindowsIdentity (get-credential)

AuthenticationType : MICROSOFT_AUTHENTICATION_PACKAGE_V1_0
ImpersonationLevel : None
IsAuthenticated    : True
IsGuest            : False
IsSystem           : False
IsAnonymous        : False
Name               : Domain\Administrator
Owner              : S-1-5-21-XXXXXXXXX-XXXXXXX-XXXX-XX
User               : S-1-5-21-XXXXXXXXX-XXXXXXX-XXXX-XX
Groups             : {S-1-5-21-XXXXXXXXX-XXXXXXX-XXXX-XX, , , , , , , }
Token              : XXXX
This cmdlet is designed for WindowsXP SP2. In Windows 2000, user required to have SeTcbPrivilege,a.k.a Act as part of the operating system, to call LogonUser. The reference (at the end of the post), provide a workaround to not call LongonUser: Use NegotiateStream. But it did not work for me.

If you are using windows server 2003 and want to check a domain account, don't use this cmdlet. Just do something like this:
>$id = new-object System.Security.Principal.WindowsIdentity("Domain\User")
This cmdlet could be useful if you are trying
1. To find out a specific user (you known his/her password) belongs to which groups

2. To check a user's password without login as that user or using runas/su.msh. If you input wrong password, you will get an error:
get Get-WindowsIdentity : Win32 API (LogonUser) Error: 1326


C# code:
using System;
using System.Security.Principal;
using System.Management.Automation;
using System.Net;
using System.Runtime.InteropServices;

namespace MSHForFun.Security
{
    public class LogonUserHelper
    {
        [DllImport("advapi32.dll", SetLastError = true)]
        static extern bool LogonUser(
          string principal,
          string authority,
          string password,
          LogonTypes logonType,
          LogonProviders logonProvider,
          out IntPtr token);
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern bool CloseHandle(IntPtr handle);
        enum LogonTypes : uint
        {
            Interactive = 2,
            Network,
            Batch,
            Service,
            NetworkCleartext = 8,
            NewCredentials
        }
        enum LogonProviders : uint
        {
            Default = 0, // default for platform (use this!)
            WinNT35,     // sends smoke signals to authority
            WinNT40,     // uses NTLM
            WinNT50      // negotiates Kerb or NTLM
        }
        public static WindowsIdentity LogonUser(MshCredential UserCre)
        {
            IntPtr token = IntPtr.Zero;
            NetworkCredential Cre = UserCre.GetNetworkCredential();
            bool result = LogonUser(
                Cre.UserName, "", Cre.Password,
                LogonTypes.Network,
                LogonProviders.Default,
                out token);
            WindowsIdentity id;
            if (result)
            {
                id = new WindowsIdentity(token);
                CloseHandle(token);
            }
            else
            {
                throw new Exception("Win32 API (LogonUser) Error: " + Marshal.GetLastWin32Error().ToString());
            }
            return id;
        }

    }

    /// <summary>
    /// Get User WindowsIdentity
    /// </summary>
    [Cmdlet("Get", "WindowsIdentity", SupportsShouldProcess = true)]
    public class GetWindowsIdentityCmd : Cmdlet
    {
        #region Parameters
        private MshCredential userCre = null;
        /// <summary>Process Arrary to work with</summary>
        [Parameter(Mandatory = true, Position = 0)]
        public MshCredential UserCredential
        {
            get { return userCre; }
            set { userCre = value; }
        }
        #endregion

        /// <summary>
        /// Call LogonUser Return a WindowsIdentity Object
        /// </summary>
        protected override void ProcessRecord()
        {
            if (ShouldProcess("Get WindowsIdentity of User: " + userCre.UserName))
            {
                WriteObject(LogonUserHelper.LogonUser(userCre));
            }
        }
    }
}
Reference: http://pluralsight.com/wiki/default.aspx/Keith.GuideBook/HowToGetATokenForAUser.html

Have fun!

[Edit: Monad has now been renamed to Windows PowerShell. This script or discussion may require slight adjustments before it applies directly to newer builds.]

Tags:       



Add-ProcessOwner cmdlet (update)

(Added on Mar 6th) There is a follow up post on this topic here.

Rewrite Add-ProcessOwner cmdlet and build into a MshSnapin. Most intersting thing here: Wrap an object into MshObject, add a MshNoteProperty.

MshObject Constructor:
System.Management.Automation.MshObject..ctor(Object)
MshNoteProperty Constructor:
System.Management.Automation.MshNoteProperty..ctor(String, Object)
Example to use this cmdlet:
>$ps=get-process | add-processowner
>$ps | get-member

TypeName: System.Diagnostics.Process

>$ps[0].ProcessOwner | get-member

System.Security.Principal.WindowsIdentity
# Some processOwner could be null due to security policy
//Add-ProcessOwner.cs
using System;
using System.Security.Principal;
using System.Diagnostics;
using System.Management.Automation;
using System.Runtime.InteropServices;

namespace MSHForFun.Security
{
    /// <summary>
    /// Add Process Owner infomation as NoteProperty
    /// </summary>
    [Cmdlet("add", "processowner", SupportsShouldProcess = true)]
    public class AddProcessOwnerCmd : Cmdlet
    {
        #region Parameters
        private Process[] allProcess = null;
        /// <summary>
        /// Arrary of Process to query
        /// </summary>
        [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true)]
        public Process[] ProcessArray
        {
            get { return allProcess; }
            set { allProcess = value; }
        }
        #endregion

        private SafeTokenHandle processToken = new SafeTokenHandle(IntPtr.Zero);
        private MshObject psex;
        private MshNoteProperty info;
        private WindowsIdentity id;

        /// <summary>
        /// Get WindowsIdentity for each process
        /// </summary>
        protected override void ProcessRecord()
        {
            foreach (Process ps in allProcess)
            {
                if (ShouldProcess(ps.ProcessName))
                {
                    psex = new MshObject(ps);
                    try
                    {
                        if (Win32Helper.OpenProcessToken(ps.Handle, TokenAccessLevels.Query, ref processToken))
                        {
                            if (processToken.IsInvalid)
                            {
                                WriteWarning("Invalid process token: " + ps.ProcessName);
                                info = new MshNoteProperty("ProcessOwner", null);
                                psex.Properties.Add(info);
                            }
                            else
                            {
                                id = processToken.GetWindowsIdenty();
                                WriteVerbose("Successfully get process owner information: " + ps.ProcessName);
                                info = new MshNoteProperty("ProcessOwner", id);
                                psex.Properties.Add(info);
                            }
                        }
                        else
                        {
                            WriteWarning("Unable to query process token (privilege not held): " + ps.ProcessName);
                            info = new MshNoteProperty("ProcessOwner", null);
                            psex.Properties.Add(info);
                        }
                    }
                    catch
                    {
                        WriteWarning("Unable to query process token (privilege not held): " + ps.ProcessName);
                        info = new MshNoteProperty("ProcessOwner", null);
                        psex.Properties.Add(info);
                    }                   
                    WriteObject(psex);
                }
            }
        }
    }
}
For MshSnapin
//MshSnapin.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.Management.Automation;
using System.Configuration.Install;
using System.ComponentModel;


namespace MSHForFun.Security
{
    /// <summary> Defines the properties of MonadSecurity snapin</summary>
    [RunInstaller(true)]
    public class MonadSecurity : MshSnapIn
    {
        /// <summary>Creates an instance of MonadSecurity Snapin class.</summary>
        public MonadSecurity() : base()
        {
        }
        ///<summary>The snapin name which is used for registration</summary>
        public override string Name
        {
            get
            {
                return "MSHForFun.Security";
            }
        }
        /// <summary>Gets vendor of the snapin.</summary>
        public override string Vendor
        {
            get
            {
                return "http://mshforfun.blogspot.com/";
            }
        }
        /// <summary>Gets description of the snapin. </summary>
        public override string Description
        {
            get
            {
                return "Cmdlets for windows security related jobs";
            }
        }
    }
}


[Edit: Monad has now been renamed to Windows PowerShell. This script or discussion may require slight adjustments before it applies directly to newer builds.]

Tags:       



Tuesday, February 14, 2006

Peek into process token: Add-ProcessOwner cmdlet

Take a look at process tab in windws task manager. You will find all process followed by a user name who start the process. Although Monad’s buildin cmdlet get-process also return information of processes running on current windows box, due to the limitation of System.Diagnostics.Process object, results does not contain process owner information.

In order to get process token, I have to call Win32API – OpenProcessToken, which return a System.IntPtr type token. This token can be used to construct a System.Security. Principal.WindowsIdentity object. (It is first time I use P/Invoke. Please tell me if I did anything wrong.)

Using add-member cmdlet, I add this WindowsIdentity object to System.Diagnostics.Process object as NoteProperty. To use it:
>$ps= Get-process –ProcessName exporler | Add-ProcessOwner.msh
>$ps | get-member

WaitForExit       Method        System.Boolean WaitForExit(Int...
WaitForInputIdle      Method       System.Boolean WaitForInputIdl...
__NounName         NoteProperty   System.String __NounName=Process
ProcessOwner      NoteProperty   System.Security.Principal.Wind...

>$ps.ProcessOwner

AuthenticationType : NTLM
ImpersonationLevel : None
IsAuthenticated    :
IsGuest            : False
IsSystem           : False
IsAnonymous        : False
Name               : Domain\tony
Owner              : S-1-5-21-854245398-XXXXXXXXXX-XXXXXX-XXX
User               : S-1-5-21-854245398--XXXXXXXXXX-XXXXXX-XXX
Groups             : {S-1-5-21-854245398--XXXXXXXXXX, , , , , }
Token              : XXXX

It is pretty sad that due to local security policy, I can not get owner of processes runing as System or Network service (which are those most intersting process). I might have to mount myself as a system service first than call OpenProcessToken. Well, that is to complicated for me.

using System;
using System.Runtime.InteropServices;
using System.Security.Principal;
using System.Collections;
using System.Diagnostics;

namespace Monad.Security
{
    public class OpenProcessTokenHelper
    {
        [Flags]
        enum TOKEN_ACCESS : uint
        {
            TOKEN_ASSIGN_PRIMARY = 0x0001,
            TOKEN_DUPLICATE = 0x0002,
            TOKEN_IMPERSONATE = 0x0004,
            TOKEN_QUERY = 0x0008,
            TOKEN_QUERY_SOURCE = 0x0010,
            TOKEN_ADJUST_PRIVILEGES = 0x0020,
            TOKEN_ADJUST_GROUPS = 0x0040,
            TOKEN_ADJUST_DEFAULT = 0x0080,
            TOKEN_ADJUST_SESSIONID = 0x0100,
            TOKEN_READ = 0x00020000 | TOKEN_QUERY,
            TOKEN_WRITE = 0x00020000 | TOKEN_ADJUST_PRIVILEGES | TOKEN_ADJUST_GROUPS | TOKEN_ADJUST_DEFAULT,
            TOKEN_EXECUTE = 0x00020000,
        };
        
        [DllImport("Advapi32.dll", SetLastError = true)]
        extern static int OpenProcessToken(IntPtr processHandle, TOKEN_ACCESS desiredAccess, out IntPtr tokenHandle);

        [DllImport("kernel32.dll", SetLastError = true)]
        extern static bool CloseHandle(IntPtr handle);

        public static WindowsIdentity GetProcessesIdentities(Process process)
        {
            try
            {
                IntPtr token = IntPtr.Zero;
                if (OpenProcessToken(process.Handle, TOKEN_ACCESS.TOKEN_QUERY, out token) == 0)
                {
                    throw new ApplicationException("Can't open process token for: " + process.ProcessName);
                }
                WindowsIdentity id = new WindowsIdentity(token);
                CloseHandle(token);
                return (id);
            }
            catch (Exception ex)
            {
                throw new ApplicationException("Open process token error", ex);
            }

        }
    }
}

#################################################
## Add-ProcessOwner.msh
## Add System.Security.Principal.WindowsIdentity object
## to System.Diagnostics.Process object as NoteProperty
## using P/invoke OpenProcessToken Win32API
##
## from http://mshforfun.blogspot.com/
################################################
begin
{
    [void][Reflection.Assembly]::LoadFile("D:\msh\OpenProcessTokenHelper.dll")
}
process
{
    if ($_)
    {
       # Only working with process object
       if ($_ -isnot [System.Diagnostics.Process]) {continue}  
       else
       {
           # Get WindowsIdentityObject
        $id=[Monad.Security.OpenProcessTokenHelper]::GetProcessesIdentities($_)
           add-member -InputObject $_ -Type NoteProperty -Name "ProcessOwner" -Value $id -ErrorAction "SilentlyContinue"
       }
     }
}

Reference: http://www.sellsbrothers.com/askthewonk/secure/default.aspx?content=howcanigetthesecurityprin.htm

Tags:    



Monday, February 13, 2006

Page load Activities increased!

My laptop was down for two days. When it came back, I was surprised to find out that visits to my blog increased dramatically. All visitors were referred from monad team blog and Lee Holmes' blog. Thanks you, guys!

Although only small changes were made, their scripts delivered whole new user experience or better security compared to my (ugly) script. As you may have noticed, my daytime job is research scientist on immunology. I set up this blog for biology oriented readers and never thought IT professionals would be major readers. So please forgive my amateurish and feel free to comment on any errors or bad ideas in my posts.

PS,I need to mention is that Blastn is program for DNA sequence analysis. So input should match [ATGCN], otherwise you will get an error.

Have Fun.

Tags:    



Wednesday, February 08, 2006

Tired of Typing Administrator’s Password Repeatedly?

An example of how to use new- SecureString, export-SecureString and import-SecureString.

If you read my previous blog entry and were convinced to logon as non-privilege user, you must have discovered the beauty and power of runas.exe or our su.msh.

But being the only user of that laptop, isn't it annoying to type Administrator's password again and again just for some routing jobs?  Why can't we just incorporate the Administrator's Password in our msh script?
Yes, we can. We are not dumb enough to put plain text password in our script. We have to store our password in an encrypted form. Monad beta3 contain three cmdlets to operate SecureString:

Name       : export-SecureString
Definition : export-SecureString [-SecureString] SecureString [[-SecureKey] Sec
             ureString] [-Verbose] [-Debug] [-ErrorAction ActionPreference] [-E
             rrorVariable String] [-OutVariable String] [-OutBuffer Int32]
             export-SecureString [-SecureString] SecureString [-Key Byte[]] [-V
             erbose] [-Debug] [-ErrorAction ActionPreference] [-ErrorVariable S
             tring] [-OutVariable String] [-OutBuffer Int32]

Name       : import-SecureString
Definition : import-SecureString [-String] String [[-SecureKey] SecureString] [
             -Verbose] [-Debug] [-ErrorAction ActionPreference] [-ErrorVariable
              String] [-OutVariable String] [-OutBuffer Int32]
             import-SecureString [-String] String [-Key Byte[]] [-Verbose] [-De
             bug] [-ErrorAction ActionPreference] [-ErrorVariable String] [-Out
             Variable String] [-OutBuffer Int32]

Name       : new-SecureString
Definition : new-SecureString [-Verbose] [-Debug] [-ErrorAction ActionPreferenc
             e] [-ErrorVariable String] [-OutVariable String] [-OutBuffer Int32]

Using export-SecureString, we can export a securestring (Administrator's Password) to a safe, persistable format (encrypted Administrator's Password). When needed, we can use import-SecureString to decrypt the output of export-securestring and restore it to a securestring.
> export-SecureString (new-SecureString)|out-file Admin.pass
Enter secret: ******

> get-content Admin.pass
01000000d08c9ddf0115d1118c7a00c04fc297eb0100000091700afb9271384c8d9d0f5fc5bbcad
d0000000002000000000003660000a8000000100000003ab5cb17d04880206425514877334d5600

(Clipped)
Now we can write a script like following:
##########################################
# Defrag.msh
# Start Disk Defragmenter as Administrator
# From http://mshforfun.blogspot.com/
# This script is provided AS IS
# Use it at your own risk
##########################################
$Password=import-SecureString (get-content D:\msh\Admin.pass)
$Windir=(get-childitem Env:windir).Value
$StartInfo = new-object System.Diagnostics.ProcessStartInfo
$StartInfo.UserName = "Admin"
$StartInfo.Password = $Password
$StartInfo.FileName = $Windir + "\system32\mmc.exe"
$StartInfo.Arguments = $Windir + "\system32\dfrg.msc"
$StartInfo.WorkingDirectory = $Windir + "\system32"
$StartInfo.LoadUserProfile = $true
$StartInfo.UseShellExecute = $false
[System.Diagnostics.Process]::Start($StartInfo)
Tada! No more prompts for password!

Something needed to mention:
1. Although I don't know the default key which export-SecureString used to encrypt password, I am pretty sure this key is account-specific and domain-specific. That is to say, another user on the same computer can not use import-SecureString to decrypt export-SecureString output.  Even a user with same account name can't decrypt it if he/she is on another computer (domain). If an unauthorized user tried to import-SecureString, he/she will get an error:
import-SecureString : Key not valid for use in specified state.
But that does not rule out bruce attack. So make sure to apply restricted access rule to you password file. It should be only readable by yourself and Administrators group.

2. Export-SecureString can take a key (a SecureString or byte[]) to encrypt SecureString. Import-SecureString will require the same key to decrypt. (Then we would have to type another password - the encrypt key) This method will output an encrypted password which can be used by another user. But this feature is yet not available in version beta 3. You will get an error:
> $key=new-securestring
Enter secret: *****
> export-securestring (new-securestring) -SecureKey $key
Enter secret: ******
export-SecureString : Cannot process argument because the value of argument "key" is invalid.
Added on 14th, Feb 2006
This is not a bug:
From Lee Holmes--When you use an encryption key on export-secureString, it must be of a length supported by the encryption algorithm. That is 128,192, or 256 bytes -- which is 8, 12, or 16 unicode characters if you type them. I'll file a doc bug to make this more clear.
Added on 14th, Feb 2006
3. Sign your script, and apply the AllSigned execution policy to prevent the script was tampered by malicious user.

4. DO NOT try to grant access to another user using this method. As you can see, if a non-privilege user got the content of Admin.pass (if Admin.pass properly encrypted for this specific user), he can do anything as an Administrator! 

There are more in depth discuss about Cryptograpy in MSH on MoW's Blog: here and here

Have Fun!

[Edit: Monad has now been renamed to Windows PowerShell. This script or discussion may require slight adjustments before it applies directly to newer builds.]

Tags:       



Saturday, February 04, 2006

Give Monad a Voice: out-voice cmdlet

This script will get objects from pipline and output as voice using Text To Speech (TTS) engine. If set $SaveToWav to $true, it will output to a serial of WAV files.

WindowsXP shipped with both speech recognition and Text To Speech Engine. TTS engine (sapi.dll) takes text as input and output that text as spoken audio. Although WinFX will contain the fully managed API for speech, at this moment, I have to use (ugly) com interop. To make things easier, I generate a managed API libaray (SpeechLib.dll) using tlbimp.exe, a tool in .NET 2.0 SDK.
set-alias tlbimp "C:\Program Files\Microsoft.NET\SDK\v2.0\bin\tlbimp.exe"
tlbimp "C:\Program Files\Common Files\Microsoft Shared\Speech\sapi.dll" `
/out:SpeechLib.dll /namespace:SpeechLib
You will see some warnings talking about type error. Just ignore them because we will not use those problemetic class today. Then we can load the libray, initialize a SpeechLib.SpVoiceClass object and call Speak method.
[Reflection.Assembly]::LoadFile("D:\msh\SpeechLib.dll")
$SpVoice = new-object SpeechLib.SpVoiceClass
$SpVoice.Speak("Hello", [SpeechLib.SpeechVoiceSpeakFlags]::SVSFDefault)
After using this cmdlet for a while, I found out that call Speak method asynchronously is a good idea. While listen to your voice output (which could be lengthy and boring), you can still get a prompt and working with MSH.
$SpVoice.Speak("Hello", [SpeechLib.SpeechVoiceSpeakFlags]::SVSFlagsAsync)
To get a .wav filename, I used System.Windows.Forms.SaveFileDialog object
$SaveFileD = new-object System.Windows.Forms.SaveFileDialog
$SaveFileD.Filter = "All files (*.*)|*.*|wav files (*.wav)|*.wav";
$SaveFileD.Title = "Save to a wave file";
$SaveFileD.FilterIndex = 2;
$SaveFileD.RestoreDirectory = true;
if ($SaveFileD.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK)
{
$FileName = $SaveFileD.FileName
}
To save to wav file, I use SpeechLib.SpFileStreamClass
$WaveFile = new-object SpeechLib.SpFileStreamClass
$WaveFile.Open($FileNameS, [SpeechLib.SpeechStreamFileMode]::SSFMCreateForWrite, $false)
$SpVoice.AudioOutputStream = $WaveFile
$SpVoice.Speak($StringToSay, [SpeechLib.SpeechVoiceSpeakFlags]::SVSFlagsAsync)
$SpVoice.WaitUntilDone([System.Threading.Timeout]::Infinite)
$WaveFile.Close()
I use $_ to get object from pipline and use out-string cmdlet to convert them to System.String object .
$StringToSay = $_ | out-string
If you want to try it, do something like
dir| select-object Name -first 4 | .\out-voice.msh
get-eventlog Application -Newest 4 | .\out-voice.msh $true
Make sure the speaker on your computer is poweron and enabled. Make sure you use select to format your results first. Otherwise you won't know what your computer are talking about. There is no guarantee that it would be fun. As I said before it could be lengthy and boring.
###############################################################
#
# out-voice.msh
# This script takes objects from pipline and output as voice using TTS engine.
# If set $SaveToWav to $true, output to WAV file(s).
#
# Usage: "Hello", "Welcome to monad world" | out-voice
#        "Hello", "Welcome to monad world" | out-voice $true
#
# from Tony http://mshforfun.blogspot.com/
#
################################################################

param ( [bool] $SaveToWav = $false )
begin
{   
    [void][Reflection.Assembly]::LoadFile("D:\msh\SpeechLib.dll")
    $SpVoice = new-object SpeechLib.SpVoiceClass
    $count = 0
    if ($SaveToWav)
    {
        [void] [Reflection.Assembly]::Load("System.Windows.Forms, Version=2.0.0.0, `
        Culture=neutral, PublicKeyToken=b77a5c561934e089")
        $SaveFileD = new-object System.Windows.Forms.SaveFileDialog
        $SaveFileD.Filter = "All files (*.*)|*.*|wav files (*.wav)|*.wav";
        $SaveFileD.Title = "Save to a wave file";
        $SaveFileD.FilterIndex = 2;
        $SaveFileD.RestoreDirectory = true;
        if ($SaveFileD.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK)
        {
        $FileName = $SaveFileD.FileName
        }
       else
      {
        "Must Specify a wav file"
        break
      }
   }
}
process
{
    if ($_)
    {
       $count++
       $StringToSay = $_ | out-string
       "$count: $StringToSay"
       if ($SaveToWav)
       {
            $FileNameS = $FileName.Replace(".wav", $count.Tostring()+".wav")
            $WaveFile = new-object SpeechLib.SpFileStreamClass
            $WaveFile.Open($FileNameS, `
            [SpeechLib.SpeechStreamFileMode]::SSFMCreateForWrite, $false)
            $SpVoice.AudioOutputStream = $WaveFile
            [void]$SpVoice.Speak($StringToSay, `
            [SpeechLib.SpeechVoiceSpeakFlags]::SVSFlagsAsync)
            [void]$SpVoice.WaitUntilDone([System.Threading.Timeout]::Infinite)
            $WaveFile.Close()
       }
       else
       {
            [void]$SpVoice.Speak($StringToSay, `
            [SpeechLib.SpeechVoiceSpeakFlags]::SVSFlagsAsync)
       }
    }
    else
    {
       "null object"
    }
}
end
{
  "Number of objects successfully output: $count"
}
##############################################
Reference: Code4Fun, C# Corner

Have Fun!

[Edit: Monad has now been renamed to Windows PowerShell. This script or discussion may require slight adjustments before it applies directly to newer builds.]

Tags:       



Thursday, February 02, 2006

Survival as a Non-Administrator: A Monad view

DON'T ROOT AROUND!

Remember the security rule NO.1 in unix world? It is also 100% rule NO.1 in windows world. Do you get some worms, viruses, spywares or adware in you windows box? You should blame yourself first if you logged in as an Adminstrator EVERYTIME EVERYDAY. It is you who let those malware bypass all windows security check and write themselves directly in system folder and registry. Do you really need Administrator privilege to read Emails or surf the internet? Or do you really need Administrator privilege to compile your code? If you want to write secure code, login as a limited user and find out how you code works. Don't rely on testing afterwards.

Reality check: But calm down man! Don't point to that logout button so soon! There are plenty of ways to regain your power without logout. You can control your computer as Administrator and return back to your non-Administrator environment after you are done.

Since this is a blog about MSH, I am going to talk about some solutions in MSH.

1. -Credential Parameter
It is good to see that (although still in beta stage) monad designers kept "running at least privilege" in mind. For example, they plan to implemetation -Credential parameter for file/registry provider. There are some buildin cmdlet using -Credential parameter. For example those Item cmdlet:

Name       : clear-item
Definition : clear-item [-Path] String[] [-Force] [-Filter String] [-Include St
             ring[]] [-Exclude String[]] [-Credential MshCredential] [-Verbose]
              [-Debug] [-ErrorAction ActionPreference] [-ErrorVariable String]
             [-OutVariable String] [-OutBuffer Int32] [-WhatIf] [-Confirm]

Name       : copy-item
Definition : copy-item [-Path] String[] [[-Destination] String] [-Container] [-
             Force] [-Filter String] [-Include String[]] [-Exclude String[]] [-
             Recurse] [-PassThru] [-Credential MshCredential] [-Verbose] [-Debu
             g] [-ErrorAction ActionPreference] [-ErrorVariable String] [-OutVa
             riable String] [-OutBuffer Int32] [-WhatIf] [-Confirm]

Name       : get-item
Definition : get-item [-Path] String[] [-Filter String] [-Include String[]] [-E
             xclude String[]] [-Force] [-Credential MshCredential] [-Verbose] [
             -Debug] [-ErrorAction ActionPreference] [-ErrorVariable String] [-
             OutVariable String] [-OutBuffer Int32]

Name       : invoke-item
Definition : invoke-item [-Path] String[] [-Filter String] [-Include String[]]
             [-Exclude String[]] [-Credential MshCredential] [-Verbose] [-Debug
             ] [-ErrorAction ActionPreference] [-ErrorVariable String] [-OutVar
             iable String] [-OutBuffer Int32] [-WhatIf] [-Confirm]

Name       : move-item
Definition : move-item [-Path] String[] [[-Destination] String] [-Force] [-Filt
             er String] [-Include String[]] [-Exclude String[]] [-PassThru] [-C
             redential MshCredential] [-Verbose] [-Debug] [-ErrorAction ActionP
             reference] [-ErrorVariable String] [-OutVariable String] [-OutBuff
             er Int32] [-WhatIf] [-Confirm]

Name       : new-item
Definition : new-item [-Path] String[] [-Type String] [-Value Object] [-Force]
             [-Credential MshCredential] [-Verbose] [-Debug] [-ErrorAction Acti
             onPreference] [-ErrorVariable String] [-OutVariable String] [-OutB
             uffer Int32] [-WhatIf] [-Confirm]

Name       : remove-item
Definition : remove-item [-Path] String[] [-Filter String] [-Include String[]]
             [-Exclude String[]] [-Recurse] [-Force] [-Credential MshCredential
             ] [-Verbose] [-Debug] [-ErrorAction ActionPreference] [-ErrorVaria
             ble String] [-OutVariable String] [-OutBuffer Int32] [-WhatIf] [-C
             onfirm]

Name       : rename-item
Definition : rename-item [-Path] String [-Name] String [-Force] [-PassThru] [-C
             redential MshCredential] [-Verbose] [-Debug] [-ErrorAction ActionP
             reference] [-ErrorVariable String] [-OutVariable String] [-OutBuff
             er Int32] [-WhatIf] [-Confirm]


Name       : set-item
Definition : set-item [-Path] String[] [[-Value] Object] [-Force] [-PassThru] [
             -Filter String] [-Include String[]] [-Exclude String[]] [-Credenti
             al MshCredential] [-Verbose] [-Debug] [-ErrorAction ActionPreferen
             ce] [-ErrorVariable String] [-OutVariable String] [-OutBuffer Int3
             2] [-WhatIf] [-Confirm]


Unfortunately, if you try to use them now, you will get an error.

invoke-item : Dynamic parameters for the Cmdlet cannot be retrieved. Invocation of MakePath on the 'FileSystem' provider failed for path ''. Cannot call method. The provider does not support the use of credentials.

According to Abhishek Agrawal [MSFT] : "FileSystem and Registry provider do not supprt Credential parameter for V1. We plan to address this in V2. For now you will need to start an elevated MSH window using runas.exe (or Process.Start api) and carry out the required operations there. "

So you have to wait new version of monad which can utilize the -credential parameter for FileSystem and Registry provider.

2. System.Diagnostics.Process.Start() method
.NET framework 2.0 provide three new overloads of System.Diagnostics.Process.Start() method which (might use CreateProcessAsUser Win32 API to) start a process as different user:

public static Process Start ( ProcessStartInfo startInfo)
public static Process Start ( string fileName, string userName, SecureString password, string domain)
public static Process Start (string fileName, string arguments, string userName, SecureString password, string domain)

They take string type username and SecureString type password. (ProcessStartInfo.Password is a SecureString) SecureString is a new creature in .NET 2.0. How we can get an instance of SecureString? Using get-credential cmdlet we can easily and securely package username and password and then pass them to System.Diagnostics.Process.Start() method. See my previous blog entry for details.

Armed with this tool, we can do almost anything in MSH command line. For example, set file system access control rule, kill a process, modify registry or start a service.

Remember you need to type:
exit
to stop this msh.exe process. You don't want to leave a shell with Administrator privilege to someone else, do you?

3. Have to use GUI?
Well, I have to admit that sometimes command line is just not good enough. For example, you want to disable wireless network connection. There is no way to use runas.exe or do shift-click tricks. Even if you started msh.exe as an Administrator, invoking ncpa.cpl will not give you "Network Connections" in control panel. The only choice is to use other command line tools like: devcon.exe (from microsoft) or network.exe (from Wingnut Software) If you know how to do this in MSH script, please tell me, please!

ps: I found this funny story. The author had same problem as mine.

But there is always a work around: With help of monad, we can easily get a GUI shell(explorer.exe) as Administrator.

1) using su.msh to get a msh prompt as Adminstrator
2) run following commands
stop-process -ProcessName explorer
explorer.exe
Welcome to Administrator's Explorer! Go to control panel and disable wireless network connection as you wish. But remember:

a) Explorer.exe will reload itself quickly after been killed, so you better write those command in msh script.

b) After you finish your work, you need to return to your original explorer environment. You can't kill the explorer.exe process within task manager because you are a limited user (remember?). Instead, you can
stop-process -ProcessName explorer
in the msh as Adminstrator
What? you have already closed that window? Run su.msh again!
then you either wait explorer.exe to reload itself or start explorer (Do this as a non-Administrator!) by
explorer.exe

Added on 26th, Feb 2006
You can change registry to start exporler.exe in seperate process (so there is no need to kill your original explorer.exe process):
First, run su.msh
Then modify registry:
set-property HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced -property SeparateProcess -value 1 -force

Without reboot, we can now start new explorer.exe windows as Administrator:
explorer.exe

You can even use System.Diagnostics.Process.Start() method to start a explorer.exe process from your msh runing as limited user.
Added on 26th, Feb 2006

Have Fun!

[Edit: Monad has now been renamed to Windows PowerShell. This script or discussion may require slight adjustments before it applies directly to newer builds.]

Tags: