7

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?

mklement0
  • 382,024
  • 64
  • 607
  • 775
jez
  • 14,867
  • 5
  • 37
  • 64
  • Your code works for me on Windows 10 from a command prompt. What OS or command shell version are you running? – AdminOfThings Feb 12 '19 at 21:05
  • @AdminOfThings But I'm guessing you see `Second argument is "two"` rather than the desired `Second argument is "two three"` – jez Feb 12 '19 at 21:08
  • When running on Windows 7 with UAC on, the code opens a command prompt window with the Second argument is "two." In my original command prompt console, it shows "two three" after closing out the pop-up console. On Windows 10, I only receive Second argument is "two three" with no extra command window. It is probably not executing as it should on my Windows 10 system then. – AdminOfThings Feb 12 '19 at 21:13
  • 1
    @AdminOfThings if you see something printed in the original console, your `goto :eof` must be missing or it's somehow getting ignored. Oops, it's missing in my listing in the question, that's why—edited. What matters is what arguments the elevated instance of the batch file, and they should be the same as they were in the unelevated version. – jez Feb 12 '19 at 21:30
  • 2
    What's `%~dpfx0`? I guess you want the full path, so `%~dpnx0` in the long form or `%~f0` in a more compact one... – aschipfl Feb 13 '19 at 10:18

3 Answers3

12
  • 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

Note:

  • The space after %CD% above, before \", is not an accident, and actually required to also make the batch file work when invoked from a drive's root directory - see this answer, which also offers a more modular alternative to the solution above, with the help of subroutines invoked with call.
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • 1
    I wish I could upvote this 10x! This is the first time I've ever seen a snippet that actually worked for special characters and spaces. – N Jones Nov 27 '19 at 18:27
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:"=\""%

  • 1
    Awesome, thank you. It's worth noting that your solution also generalizes to the case where `%*` is empty (my original does not, because you cannot pass an empty `-ArgumentList`) – jez Feb 14 '19 at 00:57
-1

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 


----------------------------------------
Noodles
  • 194
  • 1
  • 4
  • 1
    It should be `%~dp0RunAsAdmin...` as `%~dp0` expands with a trailing backslash already! – Compo Feb 13 '19 at 22:52
  • It doesn't make a difference. VBC doesn't mind doubled backslashes. One day I will run an experiment with three backslashes to test my hypothesis that it takes it as an escaped backslash. It makes the batch file paths more obvious. – Noodles Feb 13 '19 at 23:05
  • 1
    I didn't say it made a difference, but just because it doesn't in this case is not sufficient reason to leave it showing as incorrect. _I don't see the rest of your code, simply including unnecessary additional backslashes!_ – Compo Feb 13 '19 at 23:09
  • It's a template for many programs. I use long program names. This makes it easy to edit to shorter names. – Noodles Feb 13 '19 at 23:58
  • @Compo's point is valid, as an aside, even if it makes no practical difference. Re your claim of _the only approved way_: `Start-Process -Verb RunAs` _should_ work for on-demand elevation and _generally does_, but there's a _bug_ with respect to passing _double-quoted_ arguments. Requiring on-demand compilation with two helper files - using hard-coded paths, no less - is not a practical alternative. – mklement0 Feb 14 '19 at 00:12
  • There is also the issue that it only works if the user hasn't altered that registry key. Therefore there can be no guarantees that it will work. Again this is the mindset of a user not a programmer. Programmers don't pretend to be users. – Noodles Feb 14 '19 at 00:15
  • @mklement0 That path will work on all version of windows from XP to Windows 10. It is in a known location. *All UAC-compliant apps should have a requested execution level added to the application manifest.* so scripting can't do that. There are no foolproof ways around UAC except doing things the Windows' way. And you build it ONCE and reuse it. – Noodles Feb 14 '19 at 00:35
  • @mklement0 Not even a bug, just a case of extreme syntactic awkwardness, as shown by the fact that LotPings's answer successfully navigates through it. – jez Feb 14 '19 at 00:53
  • @Noodles I'd be interested to learn more about this issue: when you say "***it*** only works if the user hasn't altered ***that registry key***" can you elaborate? Whose solution are you talking about, and which registry key? – jez Feb 14 '19 at 00:53
  • @jez `HKEY_LOCAL_MACHINE\SOFTWARE\Classes\exefile\shell\runas` adds the Run As Administrator verb to EXE files. And here are some people with the problem https://answers.microsoft.com/en-us/windows/forum/all/right-click-run-as-admin-not-on-win-10/10dc98fa-d8cb-49f2-8690-c2c9f2037629 and https://www.neowin.net/forum/topic/638771-run-elevated-is-missing-from-exe-right-click/ – Noodles Feb 14 '19 at 01:07
  • So you're saying that, if that registry key happens to have been changed (or corrupted as in that thread) then the `powershell -verb runas` approach will fail, whereas your vbc + manifest approach will still work? – jez Feb 14 '19 at 01:11
  • And Viruses do it too https://www.bleepingcomputer.com/forums/t/483782/virus-that-disables-admin-rights-help/ and – Noodles Feb 14 '19 at 01:14
  • @jez Yes that is what I'm saying. And a virus, without needing admin permissions, can override that seting by creating `HKEY_CURRENT_USER\SOFTWARE\Classes\exefile\shell\runas` (as user keys override system keys). *And I repeat my point - there is only ONE correct way of elevating*. – Noodles Feb 14 '19 at 01:16
  • This explains how it works https://learn.microsoft.com/en-us/windows/security/identity-protection/user-account-control/how-user-account-control-works – Noodles Feb 14 '19 at 01:21
  • Also there is **NO** correct way to un-elevate. Some people try to simulate un-elevation by stripping privileges from the token but it has bizarre results when a user called over an administrator to enter the admins name and password. So it's something you don't try - you structure your program to work as Windows wants it to. – Noodles Feb 14 '19 at 01:29
  • Good to know. I tried your code. It compiled fine but then when calling `RunAsAdminConsole.exe whatever.bat` I got a two-second pause, no consent prompt, and “access denied” on the console output. – jez Feb 14 '19 at 03:03
  • You aren't passing a path to it. Once elevated the current directory changes to C:\windows\system32. That's Microsoft's decision to do that. – Noodles Feb 14 '19 at 03:25
  • @jez: It _is_ a bug, specific to combining `-Verb RunAs` with embedded double-quoted arguments inside a single `-ArgumentList` argument - see https://github.com/PowerShell/PowerShell/issues/8898. Indeed, LotPings' answer _works around_ the bug in a manner that _typically_ works (as since implicitly acknowledged by your accepting that answer); however, there are edge cases it doesn't handle, which my solution (hopefully) does. – mklement0 Feb 15 '19 at 02:47
  • @Noodles: Good to know that the static path works from W7 through W10 (will it be there in future versions?), but the larger point is that there _is_ a way to solve jez' problem using _built-in_ functionality, without needing to resort to creating a helper executable, which is (a) far from straightforward and requires you to either (b) embed the code for creating that executable on demand in your script (even farther from straightforward) or (c) to deploy that executable to target machines ahead of time. – mklement0 Feb 15 '19 at 03:00
  • Windows comes with Version 2, 3.5, and 4 of both x32 and x64 compilers. – Noodles Feb 15 '19 at 04:01