passing quoted arguments from batch file to `power

2019-02-19 01:02发布

问题:

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?

回答1:

  • 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


回答2:

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:"=\""%



回答3:

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 


----------------------------------------