passing quoted arguments from batch file to `powershell start` - self-elevation on demand - powershell

I am writing a Windows batch file that automatically escalates itself to administrative permissions, provided the user clicks "Yes" on the User Access Control dialog that appears.
I am using a technique I learned here to detect whether we already have admin rights and another from here to escalate. When appropriate, the following script, let's call it foo.bat, re-launches itself via a powershell-mediated call to runas:
#echo off
net session >NUL 2>NUL
if %ERRORLEVEL% NEQ 0 (
powershell start -wait -verb runas "%~dpfx0" -ArgumentList '%*'
goto :eof
)
echo Now we are running with admin rights
echo First argument is "%~1"
echo Second argument is "%~2"
pause
My problem is with escaping quotes in the -ArgumentList. The code above works fine if I call foo.bat one two from the command prompt, but not if one of the arguments contains a space, for example as in foo.bat one "two three" (where the second argument should be two words, "two three").
If I could even just get the appropriate behavior when I replace %* with static arguments:
powershell start -wait -verb runas "%~dpfx0" -ArgumentList 'one "two three"'
then I could add some lines in foo.bat that compose an appropriately-escaped substitute for %*. However, even on that static example, every escape pattern I have tried so far has either failed (I see Second argument is "two" rather than Second argument is "two three") or caused an error (typically Start-Process: A positional parameter cannot be found that accepts argument 'two'). Drawing on the docs for powershell's Start-Process I have tried all manner of ridiculous combinations of quotes, carets, doubled and tripled quotes, backticks, and commas, but there's some unholy interaction going on between batch-file quoting and powershell quoting, and nothing has worked.
Is this even possible?

You've run into a perfect storm of two quoting hells (cmd and PowerShell), garnished with a PowerShell bug (as of PowerShell Core 6.2.0).
To work around the bug, the batch file cannot be reinvoked directly and must instead be reinvoked via cmd /c.
LotPings' helpful answer, which takes that into account, typically works, but not in the following edge cases:
If the batch file's full path contains spaces (e.g., c:\path\to\my batch file.cmd)
If the arguments happen to contain any of the following cmd metacharacters (even inside "..."): & | < > ^; e.g., one "two & three"
If the reinvoked-with-admin-privileges batch file relies on executing in the same working directory it was originally called from.
The following solution addresses all these edge cases. While it is far from trivial, it should be reusable as-is:
#echo off
setlocal
:: Test whether this invocation is elevated (`net session` only works with elevation).
:: If already running elevated (as admin), continue below.
net session >NUL 2>NUL && goto :elevated
:: If not, reinvoke with elevation.
set args=%*
if defined args set args=%args:^=^^%
if defined args set args=%args:<=^<%
if defined args set args=%args:>=^>%
if defined args set args=%args:&=^&%
if defined args set args=%args:|=^|%
if defined args set "args=%args:"=\"\"%"
powershell -NoProfile -ExecutionPolicy Bypass -Command ^
" Start-Process -Wait -Verb RunAs -FilePath cmd -ArgumentList \"/c \"\" cd /d \"\"%CD%\"\" ^&^& \"\"%~f0\"\" %args% \"\" \" "
exit /b
:elevated
:: =====================================================
:: Now we are running elevated, in the same working dir., with args passed through.
:: YOUR CODE GOES HERE.
echo First argument is "%~1"
echo Second argument is "%~2"
pause

This is my batch for that purpose:
::ElevateMe.cmd::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
#echo off & setlocal EnableExtensions DisableDelayedExpansion
Set "Args=%*"
net file 1>nul 2>&1 || (powershell -ex unrestricted -Command ^
Start-Process -Verb RunAs -FilePath '%comspec%' -ArgumentList '/c %~f0 %Args:"=\""%'
goto :eof)
:: Put code here that needs elevation
Echo:%*
Echo:%1
Echo:%2
Pause
Sample output:
one "two three"
one
"two three"
Drücken Sie eine beliebige Taste . . .
If you want the elevated cmd to stay open, use -ArgumentList '/k %~f0 %Args:"=\""%

The only approved way to elevate is to use a manifest. This emulates Unix's SUDO.EXE.
To run a command and stay elevated
RunAsAdminconsole <Command to run>
To elevate current cmd window or create a new elevated one
RunAsAdminconsole
From https://pastebin.com/KYUgEKQv
REM Three files follow
REM RunAsAdminConsole.bat
REM This file compiles RunAsAdminconsole.vb to RunAsAdminconsole.exe using the system VB.NET compiler.
REM Runs a command elevated using a manifest
C:\Windows\Microsoft.NET\Framework\v4.0.30319\vbc "%~dp0\RunAsAdminconsole.vb" /win32manifest:"%~dp0\RunAsAdmin.manifest" /out:"%~dp0\RunAsAdminConsole.exe" /target:exe
REM To use
rem RunAsAdminconsole <Command to run>
pause
RunAsAdmin.manifest
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity
version="1.0.0.0"
processorArchitecture="*"
name="Color Management"
type="win32"
/>
<description>Serenity's Editor</description>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
</assembly>
'RunAsAdminConsole.vb
'Change cmd /k to cmd /c to elevate and run command then exit elevation
imports System.Runtime.InteropServices
Public Module MyApplication
Public Sub Main ()
Dim wshshell as object
WshShell = CreateObject("WScript.Shell")
Shell("cmd /k " & Command())
End Sub
End Module
----------------------------------------

Related

Batch: How to focus on cmd after task is initialized

Say my program is as follows
#ECHO Off
start "" "Notepad.exe"
pause
After it opens notepad the windows focus is on notepad so whatever I type is on there, but I want the focus to go back to cmd; is this possible? How?
The specific program i'm trying to run it on, is below:
#ECHO OFF
title SortV8
SETLOCAL ENABLEDELAYEDEXPANSION
cd "C:\img\Unsorted"
:SORT
FOR /R %%f IN (*.jpg *.png *.gif *.jpeg) DO (
echo Current file is: %%f
start "" "D:\Downloads\ImageGlass_Kobe_8.6.7.13_x64\ImageGlass.exe" %%f
powershell.exe -NoProfile -Command "(New-Object -ComObject WScript.Shell).AppActivate((Get-CimInstance Win32_Process -Filter \"ProcessId = $PID\").ParentProcessId)"
CHOICE /N /C 1234 /M "PICK A NUMBER (1 (Photo), 2 (Video), or 3(Delete), 4 (Terminate Program)"%1
IF ERRORLEVEL 4 goto END
IF ERRORLEVEL 3 echo "To the trash it goes!" && move %%f "C:\img\Delete"
IF ERRORLEVEL 2 echo "Video" && move %%f "C:\img\Videos"
IF ERRORLEVEL 1 echo "Photo" && move %%f "C:\img\Photos"
taskkill /IM ImageGlass.exe
cls
GOTO SORT
)
:END
taskkill /IM ImageGlass.exe
cls
echo "Goodbye!"
pause
:eol
You can use PowerShell via its CLI to reactivate your console window, but note that the execution overhead is not insignificant.
Note: This solution is not fully robust, but may work well enough in practice:
#ECHO Off
:: Asynchronously launch Notepad, which steals the focus
:: (Notepad becomes the active window).
start "" "Notepad.exe"
:: Reactivate this console window with the help of PowerShell.
powershell.exe -NoProfile -Command "(New-Object -ComObject WScript.Shell).SendKeys('%%{tab}')"
pause
The above simulates user input, namely an Alt-Tab keypress, in order to reactivate the current console window.
This relies on:
Notepad's window already having been activated (which is likely, given the execution overhead of calling powershell.exe)
the user not manually activating other windows while the batch file runs.
The following variant is more robust, but has a prerequisite that is unlikely to be met:
Rather than sending keystrokes, it properly tries to reactivate the current console window via its process ID.
However, this programmatic window activation only succeeds if the effective SPI_GETFOREGROUNDLOCKTIMEOUT value as reported by the SystemParametersInfo WinAPI function WinAPI function in the current OS uses session is 0, which is not the case by default.
While it is possible in principle to set this value to 0 on demand with PowerShell code, executing such code from cmd.exe (a batch file) inexplicably fails with an Access denied error (whereas direct execution of the same code works fine from PowerShell) - I haven't found a way around this (if someone has an explanation or solution, please tell us).
Certain third-party software - notably AutoHotkey - itself sets the value to 0 once it has run in a session, so if you happen to have a run-on-login AutoHotkey script (it doesn't matter what it does), the code below will work. That said, if you have AutoHotkey already installed, you could try to solve the problem at hand with a specific AutoHotkey script.
#ECHO Off
:: Asynchronously launch Notepad, which steals the focus
:: (Notepad becomes the active window).
start "" "Notepad.exe"
:: !! SEE LIMITATION DISCUSSED ABOVE
:: Reactivate this console window with the help of PowerShell.
powershell.exe -NoProfile -Command "$null = (New-Object -ComObject WScript.Shell).AppActivate((Get-CimInstance Win32_Process -Filter \"ProcessId = $PID\").ParentProcessId)"
pause

Use PowerShell variables in cmd command not working

I am having difficulty with executing a command line in cmd from Powershell. The problem is I can't seem to use my global variables in the command line when cmd is opened from Poweshell. Below is the code I am working with but to no avail. Can someone please provide guidance on this issue?
Start-Process -FilePath "C:\Windows\System32\cmd.exe" -verb RunAs -ArgumentList {/k set Name="$cmdname" set Item="$cmditem" setlocal EnableDelayedExpansion echo "%Name%" }
Thanks,
Roger
The syntax is awkward for sure:
Start-Process -FilePath "C:\Windows\System32\cmd.exe" -ArgumentList "/k set Name=$cmdname & set Item=$cmditem & call echo %name%"
Some of the reasoning here is:
cmd: additional commands need to be separated by &
cmd: the set command takes everything after = until special characters like &
cmd: setlocal EnableDelayedExpansion doesn't really apply here. Use call to delay instead.
delayed variable syntax is also !var! rather than %var%
Powershell: using brackets in -ArgumentList {stuff} sends as a literal string, and variables aren't expanded

Multiple inputs into new prompt & Powershell -run as and -nonewwindow issue

Here is what I currently do, file 1:
powershell.exe -command "Start-Process cmd -ArgumentList '/c cd C:\ && DiskZero.cmd'-Verb runas"
And file 2 "DiskZero.cmd":
#echo off
(echo rescan
echo sel disk 1
echo cle all
echo cre part prim
echo for fs=ntfs quick label=Intenso
echo assign letter=E
) | diskpart
pause
It works as intended, however, there is two files, what I want to do is make it so there's only one file.
I can't manage to find how to input multiple lines of code into a new elevated command prompt with only one script, so instead I'm trying to do it with powershell:
start cmd -nonewwindow works
start cmd -ver runas works
however start cmd -nonewwindow -ver runas doesn't work
What I was hoping to do was this in powershell:
start cmd -nonewwindow -ver runas
#echo off
(echo rescan
echo sel disk 1
echo cle all
echo cre part prim
echo for fs=ntfs quick label=Intenso
echo assign letter=E
) | diskpart
pause
Can anyone help me solve the start cmd -nonewwindow -ver runas issue OR input multiple lines of code into a new elevated command prompt with only one file, please?
Can anyone help me solve the start cmd -nonewwindow -verb runas issue
Unfortunately, there is no solution: Windows fundamentally does not allow you to run an elevated process (run as admin, requested with -Verb RunAs) directly in a non-elevated process' console window - that is why Start-Process syntactically prevents combining -NoNewWindow with -Verb RunAs.
OR input multiple lines of code into a new elevated command prompt with only one file, please?
While there is a solution, it'll be hard to maintain:
You can pass the lines of your second batch file (the one you want to eliminate) to cmd /c on a single line, joined with &:
Note: To facilitate side effect-free experimentation, the original diskpart command was replaced with findstr -n ., which merely prints the lines received via stdin, preceded by their line number.
powershell.exe -command "Start-Process -Verb RunAs cmd '/c cd C:\ && (echo rescan&echo sel disk 1&echo cle all&echo cre part prim&echo for fs=ntfs quick label=Intenso&echo assign letter=E) | findstr -n .&pause'"
That no space char. precedes each & is deliberate, because trailing whitespace in echo commands is significant, i.e. it becomes part of the output; however, it should be fine to place a space char. after each & (as well as before, if the preceding command ignores trailing whitespace).
A better solution is to create a temporary helper batch file from your batch file, pass its path to the PowerShell command, and delete it afterwards:
#echo off
:: Determine the path for a temporary batch file...
:: Note: %~snx0 uses the short (8.3) name of the batch file, so as
:: to ensure that the temp. file path has no spaces, which
:: obviates the need for complex double-quoting later.
set "tmpBatchFile=%TEMP%\~%~snx0"
:: ... and fill it with the desired commands.
:: Note how metacharacters - ( ) | ... - must be ^-escaped.
(
echo #echo off
echo ^(echo rescan
echo echo sel disk 1
echo echo cle all
echo echo cre part prim
echo echo for fs=ntfs quick label=Intenso
echo echo assign letter=E
echo ^) ^| findstr -n .
echo pause
) > "%tmpBatchFile%"
:: Now you can let the elevated cmd.exe process that PowerShell launches
:: execute the temp. batch file.
:: Note: -Wait ensures that the PowerShell call blocks until the elevated
:: cmd.exe window closes.
powershell.exe -command "Start-Process -Wait -Verb RunAs cmd '/c cd C:\ & %tmpBatchFile%'"
:: Delete the temp. batch file.
:: Note: If you do NOT use -Wait above, you'll have to defer deleting
:: the batch file until after the elevated cmd.exe window closes,
:: which you'll have to do manually.
del "%tmpBatchFile%"

Call batch file with elevated privileges via PowerShell and retrieve exit code

My Windows Batch shall be started by the user without administrator privileges. At some step, it shall call itself with elevated privileges. I have learned that this is possible using the PowerShell's runas feature (batch.bat ⭢ PowerShell ⭢ batch.bat). This works like a charm.
Unfortunately, I am not able to receive the exit code from the elevated batch execution. I always get 1, although there is not any error message. I have no idea at which return the exit code gets lost, 1st (batch back to PowerShell) or 2nd (PowerShell back to batch).
I believe, I have tried all of the plenty suggested answers from similar questions, but apparently I am unable to get it going. I need advice.
MVE which should indicate that the elevated batch returns 0:
#echo off
echo param=%~1
openfiles /local >nul 2>&1
IF %ERRORLEVEL% EQU 0 (
echo elevated, exit 0
pause
exit 0
) ELSE (
echo not elevated. trying to elevate.
powershell start-process -wait -verb runas '%0' -argumentlist /foo
echo powershell returned %errorlevel%.
)
Nota bene (edited to eliminate misunderstanding): while the non-elevated call (by the user) does not require any parameter, the elevated call introduces an additional parameter '/foo'. This makes things worse for me because I did not find a solution to not lose this parameter. However, this appears to be a rather unusual use case.
To solve the argument problem, you could use
powershell start-process -wait -verb runas '%0' -argumentlist '/additional-arg %*'
The exit code problem:
The first problem is the line
echo powershell returned %errorlevel%.
This can't work, because it's inside a code block and %errorlevel% will be expanded even before powershell will be called and therefore it is always 1 - the result of openfiles /local ...
But even with delayed expansion, I got always 0, probably because it's the exitcode of the successful runas, that it was able to start your batch.
You could use a work around and store the exitcode in a temporary file
#echo off
setlocal EnableDelayedExpansion
echo param=%*
openfiles /local >nul 2>&1
IF %ERRORLEVEL% EQU 0 (
echo elevated, exit 13
pause
echo 13 > "%temp%\_exitcode.tmp"
rem *** using 14 here to show that it doesn't be stored in errorlevel
exit 14
) ELSE (
echo not elevated. trying to elevate.
powershell start-process -wait -verb runas '%0' -argumentlist '/additional-arg %*'
set /p _exitcode= < "%temp%\_exitcode.tmp"
del "%temp%\_exitcode.tmp"
echo powershell returned !_exitcode!, lvl !errorlevel!.
)
You aren't putting the PowerShell commands to execute in quotes, and you would do well to use the full path as well as include any arguments to the script. A generic way to invoke this, so it could be copied across scripts, your PowerShell invocation should look like so:
powershell -c "if([bool]'%*'){ Start-Process -Wait -Verb runas '%~dpnx0' -ArgumentList ('%*' -split '\s+') } else { Start-Process -Wait -Verb runas '%~dpnx0' }"
For your needs above, this could be simplified since you know you have arguments passed into the batch file to process:
powershell -c "Start-Process -Wait -Verb runas '%~dpnx0' -ArgumentList '/foo'
%~dpnx0 - Automatic batch variable, this is the full path to the current script, including the script name
%* - Automatic batch variable, this is all arguments passed into the script.
('%*' -split '\s'): This is a PowerShell expression takes the space-delimited %* variable and splits it on continuous whitespace, returning an array. For simplicity this does have a shortcoming in that it will split on spaces in between double quotes, but the regex can be tuned to account for that if needed.
This answer is worth a read for other automatic batch variables you may find use for in the future.

Removal of Office

I'm currently trying to run a batch file as a startup script to detect and remove whatever version of office a user has installed and then to install Office 365. I have the install working however, when I attempted to uninstall Office 2013 I received the following error:
Input Error: Can not find script file "C:\Windows\OffScrub_O15msi.vbs"
The Offscrub file is in the same location as the script, is someone able to tell me why it's looking in C:\Windows for it?
Update
Please find my current script which now works for Office 2013, I previously added the line Remove2016Installs $true when using -Command to remove Office 2016, this worked. Since using -File to work around my initial problem I've been unable to get the script to remove Office 2016 and would like some advice on how to do this, I've read that whatever command is after -File needs to be the last which I believe might be why it's failing.
My full script is below:
start "----NOTICE----" cmd.exe /t:ec /Q /k "echo OFFICE 365 IS BEING INSTALLED. THIS WINDOW WILL CLOSE WHEN COMPLETE&&prompt $h"
#echo off
pushd "%~dp0"
powershell.exe -executionpolicy bypass -NoExit -File "Remove-PreviousOfficeInstalls.ps1"
popd
reg Query "HKLM\Hardware\Description\System\CentralProcessor\0" | find /i "x86" > NUL && set OS=32BIT || set OS=64BIT
if %OS%==32BIT "\\domain\SYSVOL\domain\Policies\{Policy Number}\Machine\Scripts\Startup\setup.exe" /configure "\\domain\SYSVOL\domain\Policies\{Policy Number}\Machine\Scripts\Startup\configuration-Office365-x86.xml"
if %OS%==64BIT "\\domain\SYSVOL\domain\Policies\{Policy Number}\Machine\Scripts\Startup\setup.exe" /configure "\\domain\SYSVOL\domain\Policies\{Policy Number}\Machine\Scripts\Startup\configuration-Office365-x64.xml"
taskkill /IM cmd.exe /FI "WINDOWTITLE EQ ----NOTICE----"
taskkill /IM cmd.exe /FI "WINDOWTITLE EQ Administrator: ----NOTICE----"
echo %date% %time% Setup ended with error code %errorlevel%. >> %LogLocation%\%computername%.txt
Update Finished
There's a line that calls the Powershell script Remove-PreviousOfficeInstalls, this is a file from GitHub that is very popular for the removal of whichever Office version you have installed.
I can run this command if say I copy these files to the desktop and amend the locations in the scripts, I'm not sure what this reference to C:\Windows is though when run from \domain\SYSVOL\domain\Policies{Policy Number}\Machine\Scripts\Startup\?
If you run a default instances of PowerShell it always starts in a certain directory. It depends on how and who it is started by. For instance an administrative PowerShell usually starts in C:\Windows\System32. If you use any paths that are not absolute they're applied relative to this directory.
To work around this you need to change the directory it's using. For instance by using cd to change the directory. My guess would be that your script Remove-PreviousOfficeInstalls.ps1 contains a relative call to the VBS.
An easy fix would be to run a script block instead of a single command and just cd to \\domain\SYSVOL\domain\Policies\'{Policy Number}'\Machine\Scripts\Startup\ prior to running the ps1.
The PowerShell help you can view by following [this] link or running powershell -h has the following information in regards to using the -Command switch.
...
Script blocks must be enclosed in braces ({}). You can specify a script block only when running PowerShell.exe in PowerShell. If you want to use a script block when running from another shell you must use the format:
"& {}"
...
The other important parameter for your use case is -File.
Runs the specified script in the local scope ("dot-sourced"), so that the functions and variables that the script creates are available in the current session. Enter the script file path and any parameters.
...
Your batch contains the following line:
powershell.exe -executionpolicy bypass -Command "\\domain\SYSVOL\domain\Policies\'{Policy Number}'\Machine\Scripts\Startup\Remove-PreviousOfficeInstalls.ps1 -Remove2016Installs $true"
What you do is run a single command to invoke a script with a parameter. The problems is that said script checks its locations based on certain function and with your invocation that location is wrongly detected.
There are multiple ways to fix this. One would be to change the directory before invoking the script. To do this you'd need to use a script block as the parameter for -Command. An example for this would be:
powershell.exe -Command "& {Write-Output 'Hello'; Write-Output 'World';}"
As you can see there are two independent Write-Output commands being run. You'd change this to a cd \\domain\SYSVOL\domain\Policies\'{Policy Number}'\Machine\Scripts\Startup\ and the invocation of your script. As a bonus you wouldn't need to put the whole path in front of the script anymore.
The other option would be to run powershell -File with your current invocation of the script. That should also mean that the script is read from the file and the corresponding parameters are populated accordingly.
If neither of these options work you will have to check what $PSScriptRoot is being populated with and/or what the return of (Get-Item -Path ".\").FullName is as those are the two commands used to determine the location of the script that's being executed. To do this you could use a script block.
thanks for your help regarding this. My resolution was to use the following bat command:
`start "----NOTICE----" cmd.exe /t:ec /Q /k "echo OFFICE 365 IS BEING INSTALLED. THIS WINDOW WILL CLOSE WHEN COMPLETE&&prompt $h"`
#echo off
pushd "%~dp0"
powershell.exe -executionpolicy bypass -File Remove-PreviousOfficeInstalls.ps1 -Remove2016Installs
popd
reg Query "HKLM\Hardware\Description\System\CentralProcessor\0" | find /i "x86" > NUL && set OS=32BIT || set OS=64BIT
if %OS%==32BIT "\\Server\Folder\Folder\setup.exe" /configure "\\Server\Folder\Folder\configuration-Office365-x86.xml"
if %OS%==64BIT "\\Server\Folder\Folder\setup.exe" /configure "\\Server\Folder\Folder\configuration-Office365-x64.xml"
taskkill /IM cmd.exe /FI "WINDOWTITLE EQ ----NOTICE----"
taskkill /IM cmd.exe /FI "WINDOWTITLE EQ Administrator: ----NOTICE----"
echo %date% %time% Setup ended with error code %errorlevel%. >> %LogLocation%\%computername%.txt
I had to amend the Remove-PreviousOfficeInstalls powershell script to include the switch command:
[Parameter(ValueFromPipelineByPropertyName=$true)]
[switch]$Remove2016Installs = $false,
This then did exactly what I was after, it detected the current version of Office, removed it and installed the correct bit version of Office 365 for that PC\Laptop.
Thanks for all your help