Skip to content

Commit 9f19a8c

Browse files
committed
Harden against cmd.exe hijacking
As is, whenever `proc_open()` needs to invoke the shell, cmd.exe is looked up in the usual executable search path. That implies that any cmd.exe which is placed in the current working directory (which is not necessarily what is reported by `getcwd()` for ZTS builds), will be used. This is a known attack vector, and Microsoft recommends to always use the fully qualified path to cmd.exe. To prevent any cmd.exe in the current working directory to be used, but to still allow users to use a drop in replacement for cmd.exe, we search only the `PATH` for cmd.exe (and pass the fully qualified path to `CreateProcessW`), instead of relying on automatic executable search by passing the base name only. [1] <https://msrc.microsoft.com/blog/2014/04/ms14-019-fixing-a-binary-hijacking-via-cmd-or-bat-file/>
1 parent a7e32a2 commit 9f19a8c

File tree

2 files changed

+54
-3
lines changed

2 files changed

+54
-3
lines changed

ext/standard/proc_open.c

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -701,22 +701,74 @@ static void init_process_info(PROCESS_INFORMATION *pi)
701701
memset(&pi, 0, sizeof(pi));
702702
}
703703

704+
/* on success, returns length of *comspec, which then needs to be efree'd by caller */
705+
static size_t find_comspec_nt(wchar_t **comspec)
706+
{
707+
zend_string *path = NULL;
708+
wchar_t *pathw = NULL;
709+
wchar_t *bufp = NULL;
710+
DWORD buflen = MAX_PATH, len = 0;
711+
712+
path = php_getenv("PATH", 4);
713+
if (path == NULL) {
714+
goto out;
715+
}
716+
pathw = php_win32_cp_any_to_w(ZSTR_VAL(path));
717+
if (pathw == NULL) {
718+
goto out;
719+
}
720+
bufp = emalloc(buflen * sizeof(wchar_t));
721+
do {
722+
len = SearchPathW(pathw, L"cmd.exe", NULL, buflen, bufp, NULL);
723+
if (len == 0) {
724+
goto out;
725+
}
726+
if (len < buflen) {
727+
break;
728+
}
729+
buflen = len;
730+
bufp = erealloc(bufp, buflen * sizeof(wchar_t));
731+
} while (1);
732+
*comspec = bufp;
733+
734+
out:
735+
if (path != NULL) {
736+
zend_string_release(path);
737+
}
738+
if (pathw != NULL) {
739+
free(pathw);
740+
}
741+
if (bufp != NULL && bufp != *comspec) {
742+
efree(bufp);
743+
}
744+
return len;
745+
}
746+
704747
static zend_result convert_command_to_use_shell(wchar_t **cmdw, size_t cmdw_len)
705748
{
706-
size_t len = sizeof(COMSPEC_NT) + sizeof(" /s /c ") + cmdw_len + 3;
749+
wchar_t *comspec;
750+
size_t len = find_comspec_nt(&comspec);
751+
if (len == 0) {
752+
php_error_docref(NULL, E_WARNING, "Command conversion failed");
753+
return FAILURE;
754+
}
755+
len += sizeof(" /s /c ") + cmdw_len + 3;
707756
wchar_t *cmdw_shell = (wchar_t *)malloc(len * sizeof(wchar_t));
708757

709758
if (cmdw_shell == NULL) {
759+
efree(comspec);
710760
php_error_docref(NULL, E_WARNING, "Command conversion failed");
711761
return FAILURE;
712762
}
713763

714-
if (_snwprintf(cmdw_shell, len, L"%hs /s /c \"%s\"", COMSPEC_NT, *cmdw) == -1) {
764+
if (_snwprintf(cmdw_shell, len, L"%s /s /c \"%s\"", comspec, *cmdw) == -1) {
765+
efree(comspec);
715766
free(cmdw_shell);
716767
php_error_docref(NULL, E_WARNING, "Command conversion failed");
717768
return FAILURE;
718769
}
719770

771+
efree(comspec);
720772
free(*cmdw);
721773
*cmdw = cmdw_shell;
722774

ext/standard/tests/general_functions/proc_open_cmd.phpt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ if (($num = stream_select($read, $write, $except, 1000)) === false) {
1818
fpassthru($stream);
1919
}
2020
}
21-
@unlink("cmd.exe"); // TODO remove when fix has been applied
2221
?>
2322
--EXPECTF--
2423
resource(%d) of type (process)

0 commit comments

Comments
 (0)