Page 1 of 2

Execute UNC path containing literal %, in command line mode

Posted: 03 Jul 2021 20:52
by noah
Suppose there's a batch file at "\\server\share\foo %Path%bar.bat". That is, the path contains the two percent signs literally. Is there a way to execute this file from command line mode? From what I've tried among techniques from viewtopic.php?t=9124 and viewtopic.php?t=9445, there is not. Here's a Perl program exercising those:

Code: Select all

use strict;
use warnings;
use Win32::Process;

my($exe, $cmd);
$exe = q{C:\windows\system32\cmd.exe};

# Phase 7.3 says, "If in command line mode and the command is not quoted and
# does not begin with a volume specification, white-space, `,`, `;`, `=` or `+`
# then break the command token at the first occurrence of `<space>` `,` `;` or
# `=` ..."
$cmd = <<\_EOF;
:: Quoted FAILS: no way to suppress percent expansion (Phase 1)
/c ""c:\subdir\foo %Path%bar.bat""
:: Unquoted w/ volume specification WORKS.
/c "c:\subdir\foo^ ^%Path^%bar.bat"
:: Unquoted w/ UNC, w/o special first byte FAILS: Phase 7 breaks token
/c "\\localhost\c$\subdir\foo^ ^%Path^%bar.bat"
:: Unquoted w/ UNC, w/ special first byte FAILS: first byte corrupts UNC path
/c "^ \\localhost\c$\subdir\foo^ ^%Path^%bar.bat"
_EOF

chomp $cmd;

STDOUT->autoflush;
foreach my $c (split /\n/, $cmd) {
  next if $c =~ /^$/ or $c =~ /^::/;
  print "==== $c\n";
  Win32::Process::Create(my $p, $exe, $c, 1, 0, ".");
  $p->Wait(INFINITE) || die;
  print "\n";
}
One could write and execute a temporary batch file, thereby gaining access to %% escaping in Phase 1. Is there a way not involving a temporary file?

Re: Execute UNC path containing literal %, in command line mode

Posted: 04 Jul 2021 03:59
by aGerman
Is this a question about Perl, noah? I don't get from where you're trying to call the path containing percents.

Steffen

Re: Execute UNC path containing literal %, in command line mode

Posted: 04 Jul 2021 05:10
by noah
No. I found Perl convenient, but here's an equivalent batch file:

Code: Select all

:: Quoted FAILS: no way to suppress percent expansion (Phase 1)                                                                                                                                                                                                                
cmd /c ""c:\subdir\foo %%Path%%bar.bat""
:: Unquoted w/ volume specification WORKS.                                                                                                                                                                                                                                     
cmd /c "c:\subdir\foo^ ^%%Path^%%bar.bat"
:: Unquoted w/ UNC, w/o special first byte FAILS: Phase 7 breaks token                                                                                                                                                                                                         
cmd /c "\\localhost\c$\subdir\foo^ ^%%Path^%%bar.bat"
:: Unquoted w/ UNC, w/ special first byte FAILS: first byte corrupts UNC path                                                                                                                                                                                                  
cmd /c "^ \\localhost\c$\subdir\foo^ ^%%Path^%%bar.bat"
(The underlying use case is essentially a C program that executes an arbitrarily-named batch file via https://docs.microsoft.com/en-us/window ... teprocessa)

Re: Execute UNC path containing literal %, in command line mode

Posted: 04 Jul 2021 07:02
by aGerman
OK I see. First of all don't compare it with the processing in a Batch file. Eventually it's being more difficult because CreateProcessA will execute it in command-line mode. Basically working with percents is not a big deal. It's gonna be difficult if they enclose an existing variable name (like 'path' in your case).
There are a couple more things to consider if another language is involved. In case of C backslashes and quotes in a string have to be escaped. And what I'd like to know before doing some test is:
- Do you want to pass NULL to lpApplicationName and have everything in the lpCommandLine parameter? or
- Do you want to pass the path of cmd.exe to lpApplicationName and only have the cmd arguments in lpCommandLine?

Steffen

Re: Execute UNC path containing literal %, in command line mode

Posted: 04 Jul 2021 07:32
by noah
All else being equal, I prefer a solution that complies with the CreateProcessA() specification. It says, "To run a batch file, you must start the command interpreter; set lpApplicationName to cmd.exe and set lpCommandLine to the following arguments: /c plus the name of the batch file." That implies:

> - Do you want to pass NULL to lpApplicationName and have everything in the lpCommandLine parameter? or

No.

> - Do you want to pass the path of cmd.exe to lpApplicationName and only have the cmd arguments in lpCommandLine?

Yes. However, if changing that answer changes the goal from unachievable to achievable, I'd consider it.

Re: Execute UNC path containing literal %, in command line mode

Posted: 04 Jul 2021 07:39
by aGerman
Haha, I don't know if it would ever help. However, I know that the behavior of CreateProcessA changes, and I just want to reflect your preferences in my tests.

Steffen

Re: Execute UNC path containing literal %, in command line mode

Posted: 04 Jul 2021 09:45
by aGerman
No success :lol:

"%path%foo bar.bat" on my desktop:

Code: Select all

@echo off &setlocal
echo Works.
pause
C testcode:

Code: Select all

#include <stddef.h>
#include <windows.h>

int main(void)
{
  const char cmd_path[] = "C:\\Windows\\System32\\cmd.exe";
  char cmd_line[1024] = " /c \"\"\\\\localhost\\C$\\Users\\%username%\\Desktop\\^%path^%foo bar.bat\"\"";
  STARTUPINFOA start_info = { sizeof(STARTUPINFOA) };
  PROCESS_INFORMATION proc_info = { 0 };
  if (CreateProcessA(cmd_path, cmd_line, NULL, NULL, FALSE, 0UL, NULL, NULL, &start_info, &proc_info))
  {
    WaitForSingleObject(proc_info.hProcess, INFINITE);
    CloseHandle(proc_info.hThread);
    CloseHandle(proc_info.hProcess);
    return 0;
  }

  return 1;
}
Result:
Der Befehl ""\\localhost\C$\Users\neffe\Desktop\^%path^%foo bar.bat"" ist entweder falsch geschrieben oder konnte nicht gefunden werden.
It expands %username%. It can't expand %path^% but leaves the carets.

Using

Code: Select all

  char cmd_line[1024] = " /v:on /c \"\"\\\\localhost\\C$\\Users\\%username%\\Desktop\\%!!path%foo bar.bat\"\"";
I get
Der Befehl ""\\localhost\C$\Users\neffe\Desktop\%!!path%foo bar.bat"" ist entweder falsch geschrieben oder konnte nicht gefunden werden.
I would have expected that the pair of exclamation points are expanded to an empty string because of the /v:on option. But they are still there.

I have the indication that CreateProcess already performs an ExpandEnvironmentStrings-like operation on the lpCommandLine parameter.

Steffen

Re: Execute UNC path containing literal %, in command line mode

Posted: 04 Jul 2021 11:07
by aGerman
OK defining an additional variable containing the percentage sign did the trick.

Code: Select all

  char cmd_line[1024] = " /v:on /c \"set \"perc=%\" & \"\\\\localhost\\C$\\Users\\%username%\\Desktop\\!perc!path!perc!foo bar.bat\"\"";
For those who are lost in the mess of backslashes, it's similar to this command:

Code: Select all

cmd.exe /v:on /c "set "perc=%" & "\\localhost\C$\Users\%username%\Desktop\!perc!path!perc!foo bar.bat""
Steffen

Re: Execute UNC path containing literal %, in command line mode

Posted: 04 Jul 2021 17:35
by noah
That is a nice technique. However, that enables delayed expansion inside the batch file, not just for the command line. Just like I don't control the batch file name, I don't control whether the batch file is compatible with delayed expansion. Is there a variant that allows the batch file to start with delayed expansion set to its default?

Re: Execute UNC path containing literal %, in command line mode

Posted: 05 Jul 2021 00:24
by jeb
Hi,
yes it works without delayed expansion

Code: Select all

cmd.exe /c "for %# in (%) DO  "\\localhost\C$\Users\%username%\Desktop\%#path%#foo bar.bat""
It's not bullet proof, but it should work in the most cases.
It could fail, if a variables exist like

Code: Select all

set "# in (=FAIL"
or
set ") DO  "\\localhost\C$\Users\=FAIL TOO"
But as said, this should be uncommon :D

jeb

Re: Execute UNC path containing literal %, in command line mode

Posted: 05 Jul 2021 10:05
by aGerman
Yeah, that works in C too, jeb. Just added the @ to suppress the prompt which would also show up in the console of the C app.

Code: Select all

  char cmd_line[1024] = " /c \"for %# in (%) do @\"\\\\localhost\\C$\\Users\\%username%\\Desktop\\%#path%#foo bar.bat\"\"";
Steffen

Re: Execute UNC path containing literal %, in command line mode

Posted: 05 Jul 2021 21:04
by noah
That passed every test I had written. Thank you. I see that an environment variable named "#path" will disrupt this. While the environment and the batch file name are not hostile, I see two ways to achieve robustness against that:
  • Before calling CreateProcessA(), simulate percent expansion on cmd_line. If percent expansion does not change the string, proceed to CreateProcessA(). Otherwise, change the loop parameter name (#, A, B, ... Z, ...) and restart the simulation.
  • Inject the percent signs via an environment variable:

    Code: Select all

    char cmd_line[1024] = " /c \"set \"my_percent=\" & \"\\\\localhost\\C$\\Users\\%username%\\Desktop\\%my_percent%path%my_percent%foo bar.bat\"\"";
    SetEnvironmentVariable("my_percent", "%")
    
    (For the rest of the program, refer to viewtopic.php?f=3&t=10131#p64681.)
I am inclined to use the second strategy, for its simplicity.

Re: Execute UNC path containing literal %, in command line mode

Posted: 06 Jul 2021 00:26
by aGerman
Let's have some fun. I mean, in C code you can easily include any character into a string that even a cat on the keyboard wouldn't be able to enter as name of an environment variable. E.g. a C0 control character like byte 0x01.

Code: Select all

  char cmd_line[1024] = " /c \"for /f %\001 in (\"% \001\") do @\"\\\\localhost\\C$\\Users\\%username%\\Desktop\\%\001path%\001foo bar.bat\"\"";
This uses 0x01 as FOR variable name and inserts an unused 0x01 in the parenthesized clause to address jeb's concerns too.

Steffen

Re: Execute UNC path containing literal %, in command line mode

Posted: 06 Jul 2021 09:32
by jeb
noah wrote:
05 Jul 2021 21:04
Inject the percent signs via an environment variable:

Code: Select all

char cmd_line[1024] = " /c \"set \"my_percent=\" & \"\\\\localhost\\C$\\Users\\%username%\\Desktop\\%my_percent%path%my_percent%foo bar.bat\"\"";
SetEnvironmentVariable("my_percent", "%")

(For the rest of the program, refer to viewtopic.php?f=3&t=10131#p64681.)

I am inclined to use the second strategy, for its simplicity.
... can't work, because the expansion of my_percent is done before the set statement is executed.

But it can be done in a safe way with a simple FOR /F

Code: Select all

FOR /F "tokens=1-4 delims="  %:= in ("1=2=3=%=") do \localhost\\C$\\Users\\%username%\\Desktop\\%=path%=foo bar.bat
This defines four for-meta-variables %: %; %< and %= %= contains a single percent sign.
An expansion of %=<any string> is more or less safe, because it's not possible to define a variable with a leading equal sign.
There exist only a few predefined variables beginning with an equal sign.

Code: Select all

=::
=C:
=ExitCode
Still, this could be fail in situations like

Code: Select all

FOR /F "tokens=1-4 delims="  %:= in ("1=2=3=%=") do echo ...%=C:%=
But even this can be solved by using 0 characters of the =exitcode variable

Code: Select all

FOR /F "tokens=1-4 delims="  %:= in ("1=2=3=%=") do echo ...%=%=exitcode:~,0%c:%=

Re: Execute UNC path containing literal %, in command line mode

Posted: 06 Jul 2021 19:54
by noah
jeb wrote:
06 Jul 2021 09:32
... can't work, because the expansion of my_percent is done before the set statement is executed.
What part can't work? I agree that %my_percent% expands before the "set" takes effect, and that's essential. If events happened in the other order, the implementation would be executing a batch file having literal text "%my_percent%" in its name, which isn't the intent. My test runs show the desired effect, and an "echo %my_percent%" inside the executed batch file confirms that the "set my_percent=" takes effect before the first statement of the batch file.

Your alternatives look robust, though I haven't tested them. Regarding aGerman's alternative, I agree an 0x01 byte should be safe enough in practice.