Monday 13 June 2011

VBSShell - embed a powershell script in a vbscript and setting ExecutionPolicy from registry

I recently had a situation where I could deploy a vbscript to lots of machines remotely via another product but it wouldn't do powershell natively which was a shame because I had a great powershell script to do the job I wanted to achieve right then.

So I wrote this script I called VBSShell that is a vbscript wrapper for a embedded powershell script.

When executed on a machine it will extract the embedded string to a ps1 file in %temp% then execute it and capture the output.

if powershell execution policy is not >=RemoteSigned script has logic to:
- abort
- edit setting temporarily via Software\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell\ExecutionPolicy
- alternately permanently change the setting (not recommended by me!!)

I also wrote  a second script to encode a ps1 file as a second file containing the string to paste into the vbsshell wrapper because its a bit of a pain doing it by hand (because " is invalid!)

I've pasted the two scripts below


' VBSShell
' vbscript powershell wrapper.
' useful if you have a situation where you want to run a powershell on a machine via a package system that only allows you to run a vbs
' this script will
' 1) find powershell and store binary location (exit if not found)
' 2) make a 'local' ps1 file of the script content
' 3) create a batch file in .tmp to execute and > output to temp file
' 4) read script output and echo so you can see in vbscript output what the script did + errors

' note regarding powershell script security
' by default powershell will not permit ps1 scripts.
' for vbs to execute a powershell script the policy must be >="RemoteSigned" at some point typically by "Set-ExecutionPolicy RemoteSigned" in ps prompt.
' there are flags below that allow you to control the behavour of this script around this issue.
' options include:
' - exit if PS policy to restrictive and do nothing.
' - temporarily set to RemoteSigned then revert at script end
' - permanently set to RemoteSigned

' Note - debug mode
' there is a debug sub at end of script. uncomment this sub's echo to enable extended logging to STDOUT

' version
' 1.03

' error control (to ensure script ends if any ps policy change)
On Error Resume Next

' ################################# CONFIG OF PS1 CONTENT ######################################

' ENTER PS1 FILE CONTENT HERE
' note you will have to replace " with chr(34) or """" unfortunately...
' and this can be tricky as open quote should be chr(34) & " vs closing quote " & chr(34)
' you can import a file here instead but for my purpose if I could do that in the application I need this for I wouldn't have written this script!
' you can use encoder.vbs in this folder to create encoding for ps1 file.

Dim ps1Content

ps1Content= " # example.ps1 to demo encoding file " & vbcrlf & _
" # This is a powershell test script " & vbcrlf & _
"  " & vbcrlf & _
" $a = 1 + 1 " & vbcrlf & _
" " & """" & "1+ 1 = $a" & """" & " " & vbcrlf & _
"  " & vbcrlf & _
" dir | format-List " & vbcrlf & _
"  " & vbcrlf & _
" # the end " & vbcrlf & _
"exit"


' ** WARNING END ALL SCRIPTS WITH "exit" *** or you will make this script potentially hang indefinitely waiting for the ps prompt to close!!

' ################################### SETUP SOME GLOBAL OBJECTS #############################################
Const ForReading = 1, ForWriting = 2, ForAppending = 8
const HKEY_LOCAL_MACHINE = &H80000002

Set WshShell = CreateObject( "WScript.Shell")
Set oFSO = CreateObject("Scripting.FileSystemObject")

dim ThisScript,ThisPath,ScriptName,TEMPDir,epoc

' epocnow time used to stamp files
epocnow = datediff("s","01/01/1970 00:00",now)

ThisScript = wscript.ScriptFullName ' name of the script including path.
ThisPath =  Left(ThisScript, InstrRev(ThisScript, "\")) ' path where script is run - used for relative pathing
ScriptName = right(thisScript,len(thisscript)-len(thispath))

TEMPDir = WshShell.ExpandEnvironmentStrings("%TEMP%")

if right(TEMPDir,1)="\" then

else
TEMPDir=TEMPDir & "\"
end If

debug TEMPDir

' ################################## CONFIG FOR POWERSHELL #########################################

' possible values for powershell security (get/set-executionpolicy)
' "not set" == Restricted
'Restricted (default)
'AllSigned
'RemoteSigned
'Unrestricted
' this script can only succeed where >=RemoteSigned

' set this value to true to set "RemoteSigned" if more restrictive policy in place
' the vbs will set policy to RemoteSigned if required if this is true otherwise if can't proceed error
'Const PowershellOverride = false
Const PowershellOverride = true

' set this value to true to revert to previous powershell setting (if you set Powershelloverride to true)
' if true and change made will revert to previous value at end of script.
' **** RECOMMENDED SETTING IS TRUE TO AVOID PERMANENT CHANGE TO SECURITY SETTING *****
Const PowershellRevert = true

Dim powershellBin,CurrentpsPolicy,TemppsPolicy,CanGo,PsChanged
CanGo=False
PsChanged=false
CurrentpsPolicy=Null
TemppsPolicy = "RemoteSigned"

' read current powershell path
if ReadRegStringValue(HKEY_LOCAL_MACHINE,"Software\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell","Path",powershellBin) Then

wscript.echo "powershell: " & powershellBin
Else
debug "ERROR no: Software\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell-Path. Powershell not installed??"
wscript.quit(1)
End If

' read current execution policy
if ReadRegStringValue(HKEY_LOCAL_MACHINE,"Software\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell","ExecutionPolicy",CurrentpsPolicy) Then
wscript.echo "powershell: " & CurrentpsPolicy
Else
debug "ERROR can't read: Software\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell-ExecutionPolicy"
wscript.echo "Assume Restricted"
CurrentpsPolicy = "Restricted"
' wscript.quit(2)
End If

If CurrentpsPolicy = "Restricted" Then
CanGo=false
ElseIf CurrentpsPolicy = "AllSigned" Then
CanGo=false
ElseIf CurrentpsPolicy = "RemoteSigned" Then
CanGo=true
ElseIf CurrentpsPolicy = "Unrestricted" Then
CanGo=true
Else
CanGo=false
debug "ERROR unknown value for PSPolicy: " & CurrentpsPolicy
wscript.quit(3)
End If

debug CurrentpsPolicy

If CanGo = True Then

Else
If PowershellOverride = True Then
PsChanged = true
WSCRIPT.ECHO "OVERRIDE PS POLICY AS OVERRIDE FLAG SET "
If WriteRegStringValue(HKEY_LOCAL_MACHINE,"Software\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell","ExecutionPolicy",TemppsPolicy) Then
wscript.echo "Powershell Policy set to " & TemppsPolicy
Else
wscript.echo "ERROR overriding PS Policy"
wscript.quit(5)
End if
Else
wscript.echo "CANT PROCEED as powershell policy too restrictive: " & CurrentpsPolicy
wscript.quit(4)
End If

End If


' ######################################## END POWERSHELL CONFIG   #####################################

' show content for debug
debug ps1Content


' now create the local text file containing the ps1
' and the bat file

Dim ps1name, filehandle, batname, outname

ps1name = TEMPDIR & ScriptName & epocnow & ".ps1"

' debug
'wscript.echo ps1name
Set filehandle = oFSO.OpenTextFile(ps1name,ForWriting,true)
filehandle.write ps1Content
filehandle.close

outname = TEMPDIR & ScriptName & epocnow & ".tmp"
batname =  TEMPDIR & ScriptName & epocnow & ".bat"

'debug
'wscript.echo batname
'wscript.echo outname

Set filehandle = oFSO.OpenTextFile(batname,ForWriting,true)
filehandle.writeline powershellBin & " " & Chr(34) & ps1name & Chr(34) & " > " & outname
filehandle.close

' execute the ps1 which is local to set-execution policy remotesigned should allow
WshShell.Run batname,7,true
DEBUG batname

Dim Output
Set filehandle = oFSO.OpenTextFile(outname,ForReading)
Output = filehandle.readall
filehandle.close

DEBUG outname

wscript.echo "** SCRIPT EXECUTED WITH OUTPUT: " & vbcrlf & output

' if changed policy do you want to revert?
If PsChanged=True Then
If PowershellRevert=True then
WSCRIPT.ECHO "REVERT powershell execution policy BACK TO : " & CurrentpsPolicy
If WriteRegStringValue(HKEY_LOCAL_MACHINE,"Software\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell","ExecutionPolicy",CurrentpsPolicy) Then
wscript.echo "Powershell Policy set to " & CurrentpsPolicy
Else
wscript.echo "ERROR overriding PS Policy. Policy still set to " & TemppsPolicy
wscript.quit(6)
End If
else
wscript.echo "WARNING - powershell execution modified at your request from: " & CurrentpsPolicy & " to " & TemppsPolicy
End if
Else
End if

Set WshShell = nothing
Set oFSO = nothing

wscript.quit(0)

' ############## FUNCTION #########################
' if ReadRegStringValue(HKEY_LOCAL_MACHINE,"Software\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell","ExecutionPolicy",MyVariable) Then
Function ReadRegStringValue(KEY,strKeyPath,strValueName,ByRef strValue)
' returns true if value exists and false if doesn't exist
' strValue set to content of registrystring
' can return null if wmi connect fails
ERR.CLEAR
strValue=""
Set oReg=GetObject("winmgmts:{impersonationLevel=impersonate}!\\.\root\default:StdRegProv")
If Not err.number = 0 Then
debug "ERROR:  can't read registry"
ReadRegStringValue = FALSE
ERR.CLEAR
Else
debug "reading " & KEY & "\" & strKeyPath & "\" & strValueName
oReg.GetStringValue KEY,strKeyPath,strValueName,strValue
if isnull(strValue) OR err.number > 0 then
debug "ERROR:  can't read string value  " &strKeyPath & "\" & strValueName
strvalue=""
ReadRegStringValue = FALSE
else
debug "Read string value from : "& strKeyPath & "\" & strValueName  & " of " & CHR(34) & strValue & CHR(34) & " LEN: " & LEN(STRVALUE)
ReadRegStringValue = TRUE
end if
End If
End Function

Function WriteRegStringValue(KEY,strKeyPath,strValueName,strValue)
' returns true if value exists and write succeed
ERR.CLEAR
Set oReg=GetObject("winmgmts:{impersonationLevel=impersonate}!\\.\root\default:StdRegProv")
If Not err.number = 0 Then
debug "ERROR:  can't read registry"
WriteRegStringValue = FALSE
ERR.CLEAR
Else
debug "writing " & KEY & "\" & strKeyPath & "\" & strValueName
oReg.SetStringValue KEY,strKeyPath,strValueName,strValue
if not err.number = 0 then
debug "ERROR:  can't write string value  " &strKeyPath & "\" & strValueName
WriteRegStringValue = FALSE
else
debug "Wrote string value to : "& strKeyPath & "\" & strValueName  & " of " & strValue
WriteRegStringValue = TRUE
end if
End If
End Function

Sub debug (AString)
' comment me for prod!
wscript.echo "D: " & AString & " "  & err.number & " " & err.description
Err.clear
End sub




****************** encoder.vbs *****************************
' VBSShell
' vbscript powershell wrapper encoder script
' this script will convert a ps1 text file to a output file in same dir that is in right format to paste into the config section of the vbsshell script
' i.e with quotes etc

' version
' 1.01

' ENTER NAME OF THE PS1 FILE YOU WANT TO ENCODE TO PASTE INTO VBSSHELL HERE
' it should be in local folder of this script.
ps1name = "example.ps1"

Dim ps1Content
Dim ps1name, filehandle, outname,aline


' ################################### SETUP SOME GLOBAL OBJECTS #############################################
Const ForReading = 1, ForWriting = 2

Set oFSO = CreateObject("Scripting.FileSystemObject") 

dim ThisScript,ThisPath,ScriptName

ThisScript = wscript.ScriptFullName ' name of the script including path.
ThisPath =  Left(ThisScript, InstrRev(ThisScript, "\")) ' path where script is run - used for relative pathing

' prefix local path
ps1name = ThisPath & ps1name 

' debug
wscript.echo ps1name
Set filehandle = oFSO.OpenTextFile(ps1name,ForReading,true)

' set to blank
ps1Content=""

Do While filehandle.AtEndOfStream = False
aline = filehandle.readline

aline = Replace(aline,Chr(34),Chr(34) & " & " & Chr(34)& Chr(34) & Chr(34) & Chr(34)& " & " & Chr(34),1,99)
aline = Chr(34) & " " & aline & " " &  Chr(34) & " & vbcrlf & _ " & vbcrlf
ps1Content = ps1Content & aline 
Loop
filehandle.close

' ** WARNING END ALL SCRIPTS WITH "exit" *** or you will make this script potentially hang indefinitely waiting for the ps prompt to close!!
ps1Content=ps1Content & """" & "exit" & """" & vbcrlf

outname=ps1name & ".out.txt"
wscript.echo outname


' now create the local text file containing the ps1 encoded to paste
Set filehandle = oFSO.OpenTextFile(outname,ForWriting,true)
filehandle.write ps1Content
filehandle.close


2 comments:

  1. Fantastic work BTW.
    FYI: As is, the datetimestamp from the epocnow variable may cause [HKEY_CURRENT_USER\Software\Microsoft\Windows\ShellNoRoam\MUICache] to balloon with unique entries and lock up .MSI installs that scan this dir for some reason. This is important to know if you're running every 15mins in SCOM.

    ReplyDelete
  2. that's an interesting point. I only envisaged using this when I wrote it to package up as a task in scom to achieve a one time purpose. easy to comment those 4 and replace with versions without the '& epocnow &'

    ReplyDelete