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