![190c0c2274d470e079f2b7690c2b968c.png](https://i-blog.csdnimg.cn/blog_migrate/a9908ad800e972c18efa109d5d347993.jpeg)
前言
针对不同平台/语言下的命令执行是不相同的,存在很大的差异性。因此,这里对不同平台/语言下的命令执行函数进行深入的探究分析。
文章开头会对不同平台(Linux、Windows)下:终端的指令执行、语言(PHP、Java、Python)的命令执行进行介绍分析。后面,主要以PHP语言为对象,针对不同平台,对命令执行函数进行底层深入分析,这个过程包括:环境准备、PHP内核源码的编译、运行、调试、审计等,其它语言分析原理思路类似。
该系列分析文章主要分为四部分,如下:
- 第一部分:命令执行底层原理探究-PHP (一)
针对不同平台(Linux、Windows)下:终端的指令执行、语言(PHP、Java、Python)的命令执行进行介绍分析。
- 第二部分:命令执行底层原理探究-PHP (二)
主要以PHP语言为对象,针对不同平台,进行环境准备、PHP内核源码的编译、运行、调试等。
- 第三部分:命令执行底层原理探究-PHP (三)
针对Windows平台下,PHP命令执行函数的底层原理分析。
- 第四部分:命令执行底层原理探究-PHP (四)
针对Linux平台下,PHP命令执行函数的底层原理分析。
本文《 命令执行底层原理探究-PHP (三) 》主要讲述的是第三部分:针对Windows平台下,PHP命令执行函数的底层原理分析。
PHP for Windows
针对Windows平台下:PHP命令执行函数的底层分析。
命令执行底层分析
针对命令执行函数的底层分析,这里主要采用两种手段去分析:静态审计(静态审计内核源码)、动态审计(动态调试内核源码)。
静态审计
PHP命令执行函数有很多
systemexecpassthrushell_execproc_openpopenpcntl_execescapeshellargescapeshellcmd 、、、、
大部分命令执行函数于ext/standard/exec.c源码中实现
/* {{{ proto string exec(string command [, array &output [, int &return_value]]) Execute an external program */PHP_FUNCTION(exec){ php_exec_ex(INTERNAL_FUNCTION_PARAM_PASSTHRU, 0);}/* }}} *//* {{{ proto int system(string command [, int &return_value]) Execute an external program and display output */PHP_FUNCTION(system){ php_exec_ex(INTERNAL_FUNCTION_PARAM_PASSTHRU, 1);}/* }}} *//* {{{ proto void passthru(string command [, int &return_value]) Execute an external program and display raw output */PHP_FUNCTION(passthru){ php_exec_ex(INTERNAL_FUNCTION_PARAM_PASSTHRU, 3);}/* }}} *//* {{{ proto string shell_exec(string cmd) Execute command via shell and return complete output as string */PHP_FUNCTION(shell_exec){ FILE *in; char *command; size_t command_len; zend_string *ret; php_stream *stream; ZEND_PARSE_PARAMETERS_START(1, 1) Z_PARAM_STRING(command, command_len) ZEND_PARSE_PARAMETERS_END();#ifdef PHP_WIN32 if ((in=VCWD_POPEN(command, "rt"))==NULL) {#else if ((in=VCWD_POPEN(command, "r"))==NULL) {#endif php_error_docref(NULL, E_WARNING, "Unable to execute '%s'", command); RETURN_FALSE; } stream = php_stream_fopen_from_pipe(in, "rb"); ret = php_stream_copy_to_mem(stream, PHP_STREAM_COPY_ALL, 0); php_stream_close(stream); if (ret && ZSTR_LEN(ret) > 0) { RETVAL_STR(ret); }}/* }}} */
观察上面代码部分,可以发现system、exec、passthru这三个命令执行函数调用函数一样,皆为php_exec_ex()函数,不同点只在于调用函数的第二个参数mode不同0、1、3作为标识。而shell_exec函数则是调用VCWD_POPEN()函数去实现。
下面以system()命令执行函数执行whoami指令为例:
system('whoami');
借助源码审查工具Source Insight【导入php7.2.9源码项目】进行底层函数跟踪分析
![2bbedc63fac897f875ed1fee4880be6a.png](https://i-blog.csdnimg.cn/blog_migrate/8d4ece5470aa441362a9fdeb337ce510.jpeg)
首先找到php中system()函数声明处:extstandardexec.c:263
PHP_FUNCTION(system){ php_exec_ex(INTERNAL_FUNCTION_PARAM_PASSTHRU, 1);}
很明显system函数由php_exec_ex()函数实现,跟进同文件下找到php_exec_ex()函数实现【在Source Insight下面可以使用Ctrl+鼠标左键点击定位函数位置】:extstandardexec.c:209
static void php_exec_ex(INTERNAL_FUNCTION_PARAMETERS, int mode) /* {{{ */{ char *cmd; size_t cmd_len; zval *ret_code=NULL, *ret_array=NULL; int ret; ZEND_PARSE_PARAMETERS_START(1, (mode ? 2 : 3)) Z_PARAM_STRING(cmd, cmd_len) Z_PARAM_OPTIONAL if (!mode) { Z_PARAM_ZVAL_DEREF(ret_array) } Z_PARAM_ZVAL_DEREF(ret_code) ZEND_PARSE_PARAMETERS_END_EX(RETURN_FALSE); if (!cmd_len) { php_error_docref(NULL, E_WARNING, "Cannot execute a blank command"); RETURN_FALSE; } if (strlen(cmd) != cmd_len) { php_error_docref(NULL, E_WARNING, "NULL byte detected. Possible attack"); RETURN_FALSE; } if (!ret_array) { ret = php_exec(mode, cmd, NULL, return_value); } else { if (Z_TYPE_P(ret_array) != IS_ARRAY) { zval_ptr_dtor(ret_array); array_init(ret_array); } else if (Z_REFCOUNT_P(ret_array) > 1) { zval_ptr_dtor(ret_array); ZVAL_ARR(ret_array, zend_array_dup(Z_ARR_P(ret_array))); } ret = php_exec(2, cmd, ret_array, return_value); } if (ret_code) { zval_ptr_dtor(ret_code); ZVAL_LONG(ret_code, ret); }}/* }}} */
阅读php_exec_ex()函数实现,会对cmd参数进行初始化处理,然后调用php_exec(mode, cmd, NULL, return_value)函数,mode为不同执行函数标识、cmd为指令参数。
跟踪php_exec()函数调用:extstandardexec.c:97
/* {{{ php_exec * If type==0, only last line of output is returned (exec) * If type==1, all lines will be printed and last lined returned (system) * If type==2, all lines will be saved to given array (exec with &$array) * If type==3, output will be printed binary, no lines will be saved or returned (passthru) * */PHPAPI int php_exec(int type, char *cmd, zval *array, zval *return_value){ FILE *fp; char *buf; size_t l = 0; int pclose_return; char *b, *d=NULL; php_stream *stream; size_t buflen, bufl = 0;#if PHP_SIGCHILD void (*sig_handler)() = NULL;#endif#if PHP_SIGCHILD sig_handler = signal (SIGCHLD, SIG_DFL);#endif#ifdef PHP_WIN32 fp = VCWD_POPEN(cmd, "rb");#else fp = VCWD_POPEN(cmd, "r");#endif if (!fp) { php_error_docref(NULL, E_WARNING, "Unable to fork [%s]", cmd); goto err; } stream = php_stream_fopen_from_pipe(fp, "rb"); buf = (char *) emalloc(EXEC_INPUT_BUF); buflen = EXEC_INPUT_BUF; if (type != 3) { b = buf; while (php_stream_get_line(stream, b, EXEC_INPUT_BUF, &bufl)) { /* no new line found, let's read some more */ if (b[bufl - 1] != '' && !php_stream_eof(stream)) { if (buflen < (bufl + (b - buf) + EXEC_INPUT_BUF)) { bufl += b - buf; buflen = bufl + EXEC_INPUT_BUF; buf = erealloc(buf, buflen); b = buf + bufl; } else { b += bufl; } continue; } else if (b != buf) { bufl += b - buf; } if (type == 1) { PHPWRITE(buf, bufl); if (php_output_get_level() < 1) { sapi_flush(); } } else if (type == 2) { /* strip trailing whitespaces */ l = bufl; while (l-- > 0 && isspace(((unsigned char *)buf)[l])); if (l != (bufl - 1)) { bufl = l + 1; buf[bufl] = '0'; } add_next_index_stringl(array, buf, bufl); } b = buf; } if (bufl) { /* strip trailing whitespaces if we have not done so already */ if ((type == 2 && buf != b) || type != 2) { l = bufl; while (l-- > 0 && isspace(((unsigned char *)buf)[l])); if (l != (bufl - 1)) { bufl = l + 1; buf[bufl] = '0'; } if (type == 2) { add_next_index_stringl(array, buf, bufl); } } /* Return last line from the shell command */ RETVAL_STRINGL(buf, bufl); } else { /* should return NULL, but for BC we return "" */ RETVAL_EMPTY_STRING(); } } else { while((bufl = php_stream_read(stream, buf, EXEC_INPUT_BUF)) > 0) { PHPWRITE(buf, bufl); } } pclose_return = php_stream_close(stream); efree(buf);done:#if PHP_SIGCHILD if (sig_handler) { signal(SIGCHLD, sig_handler); }#endif if (d) { efree(d); } return pclose_return;err: pclose_return = -1; goto done;}/* }}} */
审计int php_exec(int type, char *cmd, zval *array, zval *return_value)函数代码,发现函数内部会首先调用VCWD_POPEN()函数去处理cmd指令【在这里不难发现该部分函数VCWD_POPEN()调用同shell_exec()可执行函数实现原理相同,也就说明system、exec、passthru、shell_exec这类命令执行函数原理相同,底层都调用了相同函数VCWD_POPEN()去执行系统指令】。
这里的VCWD_POPEN()函数调用会通过相应的平台去执行:PHP_WIN32为Windows平台、另一个为Unix平台
#ifdef PHP_WIN32 fp = VCWD_POPEN(cmd, "rb");#else fp = VCWD_POPEN(cmd, "r");#endif
进入VCWD_POPEN(cmd, "rb")函数: Zendzend_virtual_cwd.h:269
#define VCWD_POPEN(command, type) virtual_popen(command, type)
由于VCWD_POPEN函数为virtual_popen实现,直接进入virtual_popen()函数实现:Zendzend_virtual_cwd.c:1831
#ifdef ZEND_WIN32CWD_API FILE *virtual_popen(const char *command, const char *type) /* {{{ */{ return popen_ex(command, type, CWDG(cwd).cwd, NULL);}/* }}} */#else /* Unix */CWD_API FILE *virtual_popen(const char *command, const char *type) /* {{{ */{ size_t command_length; int dir_length, extra = 0; char *command_line; char *ptr, *dir; FILE *retval; command_length = strlen(command); dir_length = CWDG(cwd).cwd_length; dir = CWDG(cwd).cwd; while (dir_length > 0) { if (*dir == ''') extra+=3; dir++; dir_length--; } dir_length = CWDG(cwd).cwd_length; dir = CWDG(cwd).cwd; ptr = command_line = (char *) emalloc(command_length + sizeof("cd '' ; ") + dir_length + extra+1+1); memcpy(ptr, "cd ", sizeof("cd ")-1); ptr += sizeof("cd ")-1; if (CWDG(cwd).cwd_length == 0) { *ptr++ = DEFAULT_SLASH; } else { *ptr++ = '''; while (dir_length > 0) { switch (*dir) { case ''': *ptr++ = '''; *ptr++ = ''; *ptr++ = '''; /* fall-through */ default: *ptr++ = *dir; } dir++; dir_length--; } *ptr++ = '''; } *ptr++ = ' '; *ptr++ = ';'; *ptr++ = ' '; memcpy(ptr, command, command_length+1); retval = popen(command_line, type); efree(command_line); return retval;}/* }}} */#endif
不难发现,针对virtual_popen()函数实现,也存在于不同平台,这里主要分析Windows平台,针对Unix平台在下面PHP for Linux部分会详细讲述。
针对Windows平台,virtual_popen()函数实现非常简单,直接调用popen_ex()函数进行返回。
进入popen_ex()函数实现:TSRMsrm_win32.c:473
TSRM_API FILE *popen_ex(const char *command, const char *type, const char *cwd, char *env){/*{{{*/ FILE *stream = NULL; int fno, type_len, read, mode; STARTUPINFOW startup; PROCESS_INFORMATION process; SECURITY_ATTRIBUTES security; HANDLE in, out; DWORD dwCreateFlags = 0; BOOL res; process_pair *proc; char *cmd = NULL; wchar_t *cmdw = NULL, *cwdw = NULL, *envw = NULL; int i; char *ptype = (char *)type; HANDLE thread_token = NULL; HANDLE token_user = NULL; BOOL asuser = TRUE; if (!type) { return NULL; } /*The following two checks can be removed once we drop XP support */ type_len = (int)strlen(type); if (type_len <1 || type_len > 2) { return NULL; } for (i=0; i < type_len; i++) { if (!(*ptype == 'r' || *ptype == 'w' || *ptype == 'b' || *ptype == 't')) { return NULL; } ptype++; } cmd = (char*)malloc(strlen(command)+strlen(TWG(comspec))+sizeof(" /c ")+2); if (!cmd) { return NULL; } sprintf(cmd, "%s /c "%s"", TWG(comspec), command); cmdw = php_win32_cp_any_to_w(cmd); if (!cmdw) { free(cmd); return NULL; } if (cwd) { cwdw = php_win32_ioutil_any_to_w(cwd); if (!cwdw) { free(cmd); free(cmdw); return NULL; } } security.nLength = sizeof(SECURITY_ATTRIBUTES); security.bInheritHandle = TRUE; security.lpSecurityDescriptor = NULL; if (!type_len || !CreatePipe(&in, &out, &security, 2048L)) { free(cmdw); free(cwdw); free(cmd); return NULL; } memset(&startup, 0, sizeof(STARTUPINFOW)); memset(&process, 0, sizeof(PROCESS_INFORMATION)); startup.cb = sizeof(STARTUPINFOW); startup.dwFlags = STARTF_USESTDHANDLES; startup.hStdError = GetStdHandle(STD_ERROR_HANDLE); read = (type[0] == 'r') ? TRUE : FALSE; mode = ((type_len == 2) && (type[1] == 'b')) ? O_BINARY : O_TEXT; if (read) { in = dupHandle(in, FALSE); startup.hStdInput = GetStdHandle(STD_INPUT_HANDLE); startup.hStdOutput = out; } else { out = dupHandle(out, FALSE); startup.hStdInput = in; startup.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE); } dwCreateFlags = NORMAL_PRIORITY_CLASS; if (strcmp(sapi_module.name, "cli") != 0) { dwCreateFlags |= CREATE_NO_WINDOW; } /* Get a token with the impersonated user. */ if(OpenThreadToken(GetCurrentThread(), TOKEN_ALL_ACCESS, TRUE, &thread_token)) { DuplicateTokenEx(thread_token, MAXIMUM_ALLOWED, &security, SecurityImpersonation, TokenPrimary, &token_user); } else { DWORD err = GetLastError(); if (err == ERROR_NO_TOKEN) { asuser = FALSE; } } envw = php_win32_cp_env_any_to_w(env); if (envw) { dwCreateFlags |= CREATE_UNICODE_ENVIRONMENT; } else { if (env) { free(cmd); free(cmdw); free(cwdw); return NULL; } } if (asuser) { res = CreateProcessAsUserW(token_user, NULL, cmdw, &security, &security, security.bInheritHandle, dwCreateFlags, envw, cwdw, &startup, &process); CloseHandle(token_user); } else { res = CreateProcessW(NULL, cmdw, &security, &security, security.bInheritHandle, dwCreateFlags, envw, cwdw, &startup, &process); } free(cmd); free(cmdw); free(cwdw); free(envw); if (!res) { return NULL; } CloseHandle(process.hThread); proc = process_get(NULL); if (read) { fno = _open_osfhandle((tsrm_intptr_t)in, _O_RDONLY | mode); CloseHandle(out); } else { fno = _open_osfhandle((tsrm_intptr_t)out, _O_WRONLY | mode); CloseHandle(in); } stream = _fdopen(fno, type); proc->prochnd = process.hProcess; proc->stream = stream; return stream;}/*}}}*/
从TSRMsrm_win32.c文件不难发现,由virtual_popen()函数不同平台到popen_ex()函数可知,virtual_popen()函数是作为不同平台的分割点,此时的调用链已经到了仅和windows平台有联系。
接着对*popen_ex()函数进行分析,参数:command为指令参数、cwd为当前工作目录、env为环境变量信息。
为cmd变量动态分配空间:这里不得不说把cmd变量的空间分配的刚刚好
cmd = (char*)malloc(strlen(command)+strlen(TWG(comspec))+sizeof(" /c ")+2);
分配空间后,为cmd变量赋值
sprintf(cmd, "%s /c "%s"", TWG(comspec), command);=> cmd = "cmd.exe /c whoami"
这部分其实在PHP官方手册的可执行函数中也有说明
![22a30e47c45f62fef95051ce7f01c178.png](https://i-blog.csdnimg.cn/blog_migrate/60e0d4f19e97b9de26d28d919a008661.jpeg)
到这里也就会发现system、exec、passthru、shell_exec这类命令执行函数底层都会调用系统终端cmd.exe来执行传入的指令参数。那么既然会调用系统cmd,就要将cmd进程启动起来。
继续向后分析*popen_ex()函数,会找到相关Windows系统API来启动cmd.exe进程,然后由cmd进程执行指令参数(内部|外部指令)。
if (asuser) { res = CreateProcessAsUserW(token_user, NULL, cmdw, &security, &security, security.bInheritHandle, dwCreateFlags, envw, cwdw, &startup, &process); CloseHandle(token_user); } else { res = CreateProcessW(NULL, cmdw, &security, &security, security.bInheritHandle, dwCreateFlags, envw, cwdw, &startup, &process); }
在 Windows 平台上,创建进程有 WinExec,system,_spawn/_wspawn,CreateProcess,ShellExecute 等多种途径,但上述函数基本上还是由 CreateProcess Family 封装的。在 Windows 使用 C/C++ 创建进程应当优先使用 CreateProcess,CreateProcess有三个变体,主要是为了支持以其他权限启动进程, CreateProcess 及其变体如下:
FunctionFeatureDetailsCreateProcessW/A创建常规进程,权限继承父进程权限CreateProcessAsUserW/A使用主 Token 创建进程,子进程权限与 Token 限定一致必须开启 SE_INCREASE_QUOTA_NAMECreateProcessWithTokenW使用主 Token 创建进程,子进程权限与 Token 限定一致必须开启 SE_IMPERSONATE_NAMECreateProcessWithLogonW/A使用指定用户凭据启动进程
PS:有关Windows系统API的调用情况,一般编程语言启动某个可执行程序的进程,都会调用CreateProcessW系统API,而不使用CreateProcessAsUserW系统API。同时在cmd终端进程下,启动外部指令程序所调用的系统API一般为CreateProcessInternalW。
接着将进程运行的结果信息以流的形式返回,最终完成PHP命令执行函数的整个调用过程。
if (read) { fno = _open_osfhandle((tsrm_intptr_t)in, _O_RDONLY | mode); CloseHandle(out); } else { fno = _open_osfhandle((tsrm_intptr_t)out, _O_WRONLY | mode); CloseHandle(in); } stream = _fdopen(fno, type); proc->prochnd = process.hProcess; proc->stream = stream; return stream;
同理,按照上述整个审计思路,可整理出PHP常见命令执行函数在Windows平台下的底层调用链
![9b75b4ae769f23ec65f132c4cd502c9c.png](https://i-blog.csdnimg.cn/blog_migrate/39cb616862467e58ba37a642680167cc.jpeg)
动态审计
有了上面静态审计部分的分析,后续进行动态调试会很方便。这里同样以system()函数执行whoami指令为例来进行动态调试,其它函数调试原理类似。
// test.php<?phpsystem ("whoami");?>
在ext/standard/exec.c:265中对system()函数实现入口处下断点,F5启动调试,运行至断点处
![088067a0c4becaab9dfa699d98107b9e.png](https://i-blog.csdnimg.cn/blog_migrate/fc2820a15209f0ca69584af8065115f0.jpeg)
F11步入函数php_exec_ex(INTERNAL_FUNCTION_PARAM_PASSTHRU, 1)内部:extstandardexec.c:209
php_exec_ex()对cmd参数初始化处理后调用php_exec(mode, cmd, NULL, return_value)函数
![7886f62a3f7625ff6616a5966d093cd9.png](https://i-blog.csdnimg.cn/blog_migrate/d0e2761418831827c75a90b7f6d8d565.jpeg)
F11步入php_exec()函数:extstandardexec.c:97,php_exec()函数会传入cmd指令调用VCWD_POPEN()函数
![348f60a134313cfd5847c590c79687da.png](https://i-blog.csdnimg.cn/blog_migrate/fda497d81bd99dbe85ba77c149f2404b.jpeg)
F11步入VCWD_POPEN()函数实现:
#define VCWD_POPEN(command, type) virtual_popen(command, type)Zendzend_virtual_cwd.h:269
由于VCWD_POPEN函数为virtual_popen实现,直接进入virtual_popen()函数实现:Zendzend_virtual_cwd.c:1831
![a81bd73b297d0679c04c4d1f16b8218b.png](https://i-blog.csdnimg.cn/blog_migrate/fcf6bf806c71b8795bfd0dc3a59b05fe.jpeg)
virtual_popen()函数将cmd指令、当前工作空间等参数传给popen_ex(command, type, CWDG(cwd).cwd, NULL)函数执行返回。
F11步入popen_ex()函数实现:TSRMsrm_win32.c:473
![3c8293f50e05d3fb696d13d6f85c8cfd.png](https://i-blog.csdnimg.cn/blog_migrate/02a20408815d71ab9ffa326429f68c2e.jpeg)
跟进popen_ex()函数,对cmd进行动态分配空间及赋值
![9349a4ff9518d3e77d6567c86c490be1.png](https://i-blog.csdnimg.cn/blog_migrate/8b97b42bb78768e622f2c96896fc2dc1.jpeg)
从cmd赋值的结果上来看,命令执行函数执行命令由底层调用cmd.exe来执行相应系统指令(内部|外部)。
后续调用CreateProcessW()系统API来启动cmd.exe进程,执行相应的指令即可。
![c75e0a3f3e51a156874f2377c68eeba7.png](https://i-blog.csdnimg.cn/blog_migrate/9dfd4a0eab5bb53c0052aa6494645f05.jpeg)
查看函数之间的调用栈
![f091a3385aa683e7489e59531637c987.png](https://i-blog.csdnimg.cn/blog_migrate/151bbcf985cfc5d64e452082f8237e66.jpeg)
如果单纯的是想知道某个命令执行函数是否调用cmd.exe终端去执行系统指令的话,可以在php脚本里面写一个循环,然后观察进程创建情况即可:简单、粗暴。
![fd5e88af63b9e349c948a49602e28987.png](https://i-blog.csdnimg.cn/blog_migrate/9e7691897f80d7b07432b227261df57b.jpeg)
参考链接
- Build your own PHP on Windows
- Visual Studio docs
- Visual Studio Code docs
- 《PHP 7底层设计与源码实现+PHP7内核剖析》
- 深入理解 PHP 内核
- WINDOWS下用VSCODE调试PHP7源代码
- 调式PHP源码
- 用vscode调试php源码
- GDB: The GNU Project Debugger
- CreateProcessW function
- 命令注入成因小谈
- 浅谈从PHP内核层面防范PHP WebShell
- Program execution Functions
- linux系统调用
- system calls