2

I am trying to implement the function proposed by

mklement0 on this answer

My goal is to implement this as a reusable batch subfunction

But before that I have run into a pattern of inconsistent behaviour

Mostly depending on how the script is called, where it is and where it called from

The script generally works if it is called from a folder and it is also in a folder. However if it is called from the root of a drive, it might fail with an error regarding && pipes.

For documenting the behaviours I have saved the script as the files

i:\Test privilege escalation powershell with arguments.bat

and i:\script\Test privilege escalation powershell with arguments.bat

The exact script 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

I have tried called these scripts from several locations c:\ d:\ i:, from a folder, from the drive root. I even tried multiple times and not always gotten the same results.

The full log of what happened can be found here https://pastebin.com/xCEJKE2f I will not copy the most relevant bits here

In a normal cmd.exe console, console is not privileged

C:\>"i:\Test privilege escalation powershell with arguments.bat" 1 2 3
C:\>"i:Test privilege escalation powershell with arguments.bat" 1 2 3
C:\>"i:\scripts\Test privilege escalation powershell with arguments.bat" 1 2 3
All 3 fail to The token '&&' is not a valid statement separator in this version. https://i.imgur.com/5sUHPeB.png
C:\>"i:scripts\Test privilege escalation powershell with arguments.bat" 1 2 3
Fails with The system cannot find the path specified.                            https://i.imgur.com/5sUHPeB.png
 
C:\Users\shodan>"i:\Test privilege escalation powershell with arguments.bat" 1 2 3
C:\Users\shodan>"i:Test privilege escalation powershell with arguments.bat" 1 2 3
C:\Users\shodan>"i:\scripts\Test privilege escalation powershell with arguments.bat" 1 2 3
C:\Users\shodan>"i:scripts\Test privilege escalation powershell with arguments.bat" 1 2 3
All 4 work https://i.imgur.com/DVGpJFI.png
 
d:\>"i:Test privilege escalation powershell with arguments.bat" 1 2 3
d:\>"i:\Test privilege escalation powershell with arguments.bat" 1 2 3
d:\>"i:\scripts\Test privilege escalation powershell with arguments.bat" 1 2 3
all 3 don't work, "The token '&&' is not a valid statement separator in this version." https://i.imgur.com/60NHWLh.png
d:\>"i:scripts\Test privilege escalation powershell with arguments.bat" 1 2 3
does not work The system cannot find the path specified.
 
Later attempt ??? Not the same result ?  d:\ vs D:\ ?!?
D:\>"i:\Test privilege escalation powershell with arguments.bat" 1 2 3
D:\>"i:Test privilege escalation powershell with arguments.bat" 1 2 3
D:\>"i:\scripts\Test privilege escalation powershell with arguments.bat" 1 2 3
D:\>"i:scripts\Test privilege escalation powershell with arguments.bat" 1 2 3
All four fail with "The token '&&' is not a valid statement separator in this version."  https://i.imgur.com/c9WcxCp.png
 
d:\share>"i:Test privilege escalation powershell with arguments.bat" 1 2 3
d:\share>"i:\Test privilege escalation powershell with arguments.bat" 1 2 3
d:\share>"i:\scripts\Test privilege escalation powershell with arguments.bat" 1 2 3
all 3 work             https://i.imgur.com/0be7DkQ.png
d:\share>"i:scripts\Test privilege escalation powershell with arguments.bat" 1 2 3
The system cannot find the path specified.
 
Later attempt, not the same results ?!?! again  d:\ vs D:\ ?!?
D:\share>"i:\Test privilege escalation powershell with arguments.bat" 1 2 3
D:\share>"i:Test privilege escalation powershell with arguments.bat" 1 2 3
D:\share>"i:\scripts\Test privilege escalation powershell with arguments.bat" 1 2 3
D:\share>"i:scripts\Test privilege escalation powershell with arguments.bat" 1 2 3
All four work and elevated properly ! https://i.imgur.com/EtfvRyb.png
 
I:\>"i:Test privilege escalation powershell with arguments.bat" 1 2 3
I:\>"i:\Test privilege escalation powershell with arguments.bat" 1 2 3
I:\>"i:scripts\Test privilege escalation powershell with arguments.bat" 1 2 3
I:\>"i:\scripts\Test privilege escalation powershell with arguments.bat" 1 2 3
all don't work, same error, "The token '&&' is not a valid statement separator in this version." https://i.imgur.com/HiJggQ4.png
This result tried twice, second time same result
 
I:\scripts>"i:Test privilege escalation powershell with arguments.bat" 1 2 3
I:\scripts>"i:\Test privilege escalation powershell with arguments.bat" 1 2 3
I:\scripts>"i:\scripts\Test privilege escalation powershell with arguments.bat" 1 2 3
All 3 work
I:\scripts>"i:scripts\Test privilege escalation powershell with arguments.bat" 1 2 3
does not work The system cannot find the path specified.
This result tried twice, second time same result
   
 
Double click on I:\Test privilege escalation powershell with arguments.bat
Does not work, console flashes briefly and disappear too fast to see what the error message was
It should pause, but doesn't !
 
Double clicking on I:\scripts\Test privilege escalation powershell with arguments.bat
works https://i.imgur.com/lFMD2MI.png
 
Drag and dropping a bunch of files onto I:\scripts\Test privilege escalation powershell with arguments.bat
works, dropped files are arguments https://i.imgur.com/nUXgWbA.png https://i.imgur.com/PtMT91d.png
 
Drag and dropping files onto 
https://i.imgur.com/AMMPXzf.png
Does not work, console flashes briefly and disappear too fast to see what the error message was
It should pause, but doesn't !
 
In a PRIVILEGE console
 
C:\Windows\system32>"i:Test privilege escalation powershell with arguments.bat" 1 2 3
C:\Windows\system32>"i:\Test privilege escalation powershell with arguments.bat" 1 2 3
C:\Windows\system32>"i:\scripts\Test privilege escalation powershell with arguments.bat" 1 2 3
C:\Windows\system32>"i:scripts\Test privilege escalation powershell with arguments.bat" 1 2 3
All 4 work https://i.imgur.com/60owFpJ.png
 
C:\>"i:Test privilege escalation powershell with arguments.bat" 1 2 3
C:\>"i:\Test privilege escalation powershell with arguments.bat" 1 2 3
C:\>"i:\scripts\Test privilege escalation powershell with arguments.bat" 1 2 3
C:\>"i:scripts\Test privilege escalation powershell with arguments.bat" 1 2 3
All 4 work  https://i.imgur.com/sgT3EUx.png
 
D:\>"i:Test privilege escalation powershell with arguments.bat" 1 2 3
D:\>"i:\Test privilege escalation powershell with arguments.bat" 1 2 3
D:\>"i:\scripts\Test privilege escalation powershell with arguments.bat" 1 2 3
D:\>"i:scripts\Test privilege escalation powershell with arguments.bat" 1 2 3
All 4 work  https://i.imgur.com/TJyNGy4.png
 
I:\>"i:Test privilege escalation powershell with arguments.bat" 1 2 3
I:\>"i:\Test privilege escalation powershell with arguments.bat" 1 2 3
I:\>"i:\scripts\Test privilege escalation powershell with arguments.bat" 1 2 3
I:\>"i:scripts\Test privilege escalation powershell with arguments.bat" 1 2 3
All 4 work  https://i.imgur.com/S1mxGsF.png

So my question is, why does this script behave differently based on where it is called from, where it is and how (i:\filename.bat vs i:filename.bat). Also this case where everything seemed the same, but the result was not the same.

Lastly, my goal is to turn this script into a re-usable subfunction and it should be in the following format.

@echo off
 
Call :IsAdmin IsAdmin
if not %IsAdmin%==true Call :Elevate %*
 
if %IsAdmin%==true echo Admin privileges found
if %IsAdmin%==true echo First argument is "%~1"
if %IsAdmin%==true echo Second argument is "%~2"
if %IsAdmin%==true echo third argument is "%~3"
if %IsAdmin%==true echo fourth argument is "%~4"
if %IsAdmin%==true echo fifth argument is "%~5"
if %IsAdmin%==true echo sixth argument is "%~6"
if %IsAdmin%==true echo seventh argument is "%~7"
if %IsAdmin%==true echo eigth argument is "%~8"
if %IsAdmin%==true echo nineth argument is "%~9"
if %IsAdmin%==true pause
 
:END
GoTo :EOF
 
:IsAdmin
  set %1=false
  net session >nul 2>&1
  if %ERRORLEVEL% == 0 set %1=true
GoTo :EOF
 
:Elevate
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
mklement0
  • 382,024
  • 64
  • 607
  • 775
Shodan
  • 1,065
  • 2
  • 13
  • 35

1 Answers1

2

The problem occurs when the batch file happens to be called from a drive's root directory, because %CD% then expands to a value that ends in \, which breaks PowerShell's command-line parsing, because it interferes with the escaped " (\") that follows it.

A pragmatic workaround is to simply append a space before the following \", which still works, because cmd.exe ignores trailing spaces in file-system paths:

In other words: change %CD%\" to %CD% \"

I've also updated the linked answer accordingly.


As for your desire to modularize the code with subroutines that you call with call, including placing the elevating powershell.exe call in an Elevate subroutine:

  • Don't try to pass the arguments through via %* to the :Elevate subroutine. Instead, capture them in a variable at the top level of the batch file, which the subroutine also sees and can operate on.

  • The reason that the %* arguments pass-through via call must be avoided is that it makes cmd.exe invariably - and inexplicably - double any ^ characters in the arguments.

    • This long-standing bug, which is unlikely to get fixed, is described in detail under "Phase 6) CALL processing/Caret doubling" in this answer, which describes cmd.exe's parsing and its quirks in extensive detail.

Here's a streamlined version of your code:

  • Note the simplification of the :IsAdmin subroutine to use the exit code from the net session command to communicate success (0) vs. failure (non-zero), which enables the || operator (as well as &&) to operate on that exit code.

  • The non-elevated invocation launches the elevated incarnation and waits for its completion, and then exits. This means that any code after the line that starts with call :IsAdmin ... is only ever executed in the elevated incarnation.

@echo off & setlocal
 
:: If not already running elevated, self-elevate and exit.
:: Note: A helper variable must be used to capture %*,
::       because passing %* to a subroutine doubles ^ chars.
call :IsAdmin || set args=%* && (call :Elevate & exit /b)

:: Getting here means this is (now) an elevated session.

::Print the arguments received.
echo Now running elevated; arguments received:
echo.  %*
pause

:: End of the main body. 
goto :EOF

:: === Helper subroutines

:: Test if this session is elevated.
:: `net session` only succeeds and therefore reports exit code 0 
:: in an elevated session.
:IsAdmin
  net session >NUL 2>&1
goto :EOF
 
 :: Perform self-elevation, passing all arguments through.
:Elevate
  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:"=\"\"%"
  :: Note: 
  ::  * To not make the non-elevated incarnation wait for the
  ::    elevated one to complete, remove -Wait      
  ::  * To keep the elevated session open until explicitly exited by the user,
  ::    use /k instead of /c
  powershell -NoProfile -ExecutionPolicy Bypass -Command ^
    " Start-Process -Wait -Verb RunAs -FilePath cmd -ArgumentList \"/c \"\" cd /d \"\"%CD% \"\" ^&^& \"\"%~f0\"\" %args% \"\" \" "
goto :EOF

As an aside:

  • Normally, environment variables are inherited by child processes, and, given that all cmd.exe variables (created with SET) are invariably environment variables, there's at least a hypothetical concern about "polluting" a child process' environment with auxiliary variables from the caller (in this case, %args%).

  • While this could be addressed in general, there is no need to do so here, because elevated child processes - for security reasons - do not inherit their (non-elevated) caller's environment.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • I ran the above script in all the permutation, it works https://pastebin.com/bqATQFkv However, will it always double the carrets ? Because if yes what about adding to the function if defined args set args=%args:^^^^=^^% I just tested and it also works https://i.imgur.com/g09GIbv.png – Shodan Apr 22 '23 at 07:13
  • Oh wait, actually set args=%args:^^^^=^^% doesn't work ... Well, only partially hmmm. I thought it be nice to have just Call :Elevate %* as a single command. I figure I could call IsAdmin from inside the :Elevate function and even no longer need the IF – Shodan Apr 22 '23 at 07:19
  • I see from reading the cmd.exe parse article, for unquoted and quoted arguments, only the quoted arguments get automatically dedoubled. So it would take code to determine parts of %* that are quoted and only apply caret dedoubling to those parts. – Shodan Apr 22 '23 at 07:35
  • And currently, with set args=%args:^^^^=^^% , why do single caret get doubled but not de-doubled. Yes four carets, turn into eight carets and then get dedouble back to four. Why is set args=%args:^^^^=^^% inconsistent ?! – Shodan Apr 22 '23 at 07:36
  • @Shodan, I don't think you'll be able to undo the doubling logic in all cases, and I suggest bypassing it altogether, using an aux. variable. Please see my update, which now uses a single line in the main body, and simplifies your `:IsAdmin` subroutine. – mklement0 Apr 22 '23 at 21:55
  • 1
    Oh wow I love this ! I didn't know how to directly use the output of a subroutine like you did with IsAdmin, that's going to be very useful ! I think this is pretty much perfect, only change I'm going to do to my private version is that I'm going to call args ElevatedArgs to make it a unique variable name. Although that shouldn't matter as you pointed out the local variables are lost in the elevation. But that way you don't even have to ask yourself "does it matter if I used args elsewhere" – Shodan Apr 23 '23 at 01:24
  • 1
    Yes, I accept the answer, I also want to make sure everyone who searches google for "how to elevate my batch file" finds this answer, I searched far and wide and I think this one is the best if you're trying to do it cleanly. I also found other methods of privilege escalation, such as using runas and also creating a vbscript that re-launches the batch file with an elevated process. I'll try to word a question which will make it to google's #1 and also present all the alternatives. – Shodan Apr 24 '23 at 01:43