【问题标题】:Run executable from php without spawning a shell从 php 运行可执行文件而不产生 shell
【发布时间】:2013-05-11 19:05:32
【问题描述】:

我需要从 PHP 脚本的强加上下文中调用可执行文件。在性能和安全方面,最好不要在 Web 服务器进程和可执行文件之间调用 shell。

当然,我搜索了网络,但没有成功(在这样的 PHP 环境中)。许多其他语言都允许这样做并清楚地记录它。

唉,反引号,exec()shell_exec()passthru()system()proc_open()popen() 调用一个 shell。 而pcntl_fork() 似乎不可用。

如何测试函数是否调用shell。

这是在 Debian 6 64 位和 PHP 5.3.3-7+squeeze15 上测试的。 http://pastebin.com/y4C7MeJz上的测试代码

为了获得有意义的测试,我使用了一个技巧,即要求执行一个不作为可执行文件可用的 shell 命令。一个很好的例子是 umask 。任何返回类似 0022 的函数都绝对称为 shell。 exec()shell_exec()passthru()system()proc_open() 都做到了。 在http://pastebin.com/RBcBz02F 上查看详细结果。

pcntl_fork 失败

现在,回到目标:如何在不启动 shell 的情况下执行任意程序?

Php 的 exec 按预期采用字符串 args 数组而不是唯一字符串。但是 pcntl_fork 只是在没有日志的情况下停止请求。

编辑:pcntl_fork失败是因为服务器使用了Apache的mod_php,见http://www.php.net/manual/en/function.pcntl-fork.php#49949

编辑:根据@hakre 的建议,将popen() 添加到测试中。

任何提示表示赞赏。

【问题讨论】:

    标签: php performance security shell


    【解决方案1】:

    在 PHP 7.4+ 中,如果 cmd 作为数组传递,proc_open 直接打开进程。

    从 PHP 7.4.0 开始,cmd 可以作为命令参数数组传递。在这种情况下,进程将直接打开(无需通过 shell),PHP 将负责任何必要的参数转义。

    所以这个例子:

    <?php
    $file_descriptors = [
            0=>['pipe','r'],
            1=>['pipe','w'],
            2=>['pipe','w']
    ];
    $cmd_string = 'ps -o comm=';
    $cmd_array = [
            'ps',
            '-o',
            'comm='
    ];
    
    // This is executed by shell:
    $process = proc_open($cmd_string,$file_descriptors,$pipes);
    $output = stream_get_contents($pipes[1]);
    $return = proc_close($process);
    printf("cmd_string:\n%s\n",$output);
    
    // This is executed directly:
    $process = proc_open($cmd_array,$file_descriptors,$pipes);
    $output = stream_get_contents($pipes[1]);
    $return = proc_close($process);
    printf("cmd_array:\n%s\n",$output);
    

    输出:

    cmd_string:
    bash
    php
    sh
    ps
    
    cmd_array:
    bash
    php
    ps
    

    【讨论】:

      【解决方案2】:

      回答你的句子:

      在性能和安全方面,最好不要在 都在网络服务器进程和可执行文件之间。

      关于性能,嗯,是的,php 内部分叉,shell 本身也分叉,所以这有点重。但是你确实需要执行很多流程来考虑这些性能问题。

      关于安全性,我认为这里没有任何问题。 PHP 有 escapeshellarg 函数来清理参数。

      在没有 pcntl 的情况下,exec 遇到的唯一真正问题不是资源问题,也不是安全问题:创建 真正的 守护进程真的很困难(没有对其父级的任何附加,尤其是 阿帕奇)。在双重转义命令后,我使用at 解决了这个问题:

      $arg1 = escapeshellarg($arg1);
      $arg2 = escapeshellarg($arg2);
      $command = escapeshellarg("/some/bin $arg1 $arg2 > /dev/null 2>&1 &");
      exec("$command | at now -M");
      

      回到您的问题,我知道以标准(fork+exec)方式执行程序的唯一方法是使用PCNTL 扩展名(如前所述)。无论如何,祝你好运!


      要完成我的回答,您可以自己创建一个 exec 函数,它的作用与 pcntl_fork+pcntl_exec 相同。

      我做了一个 my_exec 扩展,它执行经典的 exec+fork,但实际上,如果您在 apache 下运行此功能,我认为它不会解决您的问题,因为同样pcntl_fork 的行为将适用(apache2 将被分叉,当execv 不成功时,可能会出现信号捕获等意外行为)。

      config.m4phpize配置文件

      PHP_ARG_ENABLE(my_exec_extension, whether to enable my extension,
      [ --enable-my-extension   Enable my extension])
      
      if test "$PHP_MY_EXEC_EXTENSION" = "yes"; then
        AC_DEFINE(HAVE_MY_EXEC_EXTENSION, 1, [Whether you have my extension])
        PHP_NEW_EXTENSION(my_exec_extension, my_exec_extension.c, $ext_shared)
      fi
      

      my_exec_extension.c 扩展

      #ifdef HAVE_CONFIG_H
      #include "config.h"
      #endif
      #include "php.h"
      
      #include <stdio.h>
      #include <stdlib.h>
      #include <unistd.h>
      #include <string.h>
      
      #define PHP_MY_EXEC_EXTENSION_VERSION "1.0"
      #define PHP_MY_EXEC_EXTENSION_EXTNAME "my_exec_extension"
      
      extern zend_module_entry my_exec_extension_module_entry;
      #define phpext_my_exec_extension_ptr &my_exec_extension_module_entry
      
      // declaration of a custom my_exec()
      PHP_FUNCTION(my_exec);
      
      // list of custom PHP functions provided by this extension
      // set {NULL, NULL, NULL} as the last record to mark the end of list
      static function_entry my_functions[] = {
          PHP_FE(my_exec, NULL)
          {NULL, NULL, NULL}
      };
      
      // the following code creates an entry for the module and registers it with Zend.
      zend_module_entry my_exec_extension_module_entry = {
      #if ZEND_MODULE_API_NO >= 20010901
          STANDARD_MODULE_HEADER,
      #endif
          PHP_MY_EXEC_EXTENSION_EXTNAME,
          my_functions,
          NULL, // name of the MINIT function or NULL if not applicable
          NULL, // name of the MSHUTDOWN function or NULL if not applicable
          NULL, // name of the RINIT function or NULL if not applicable
          NULL, // name of the RSHUTDOWN function or NULL if not applicable
          NULL, // name of the MINFO function or NULL if not applicable
      #if ZEND_MODULE_API_NO >= 20010901
          PHP_MY_EXEC_EXTENSION_VERSION,
      #endif
          STANDARD_MODULE_PROPERTIES
      };
      
      ZEND_GET_MODULE(my_exec_extension)
      
      char *concat(char *old, char *buf, int buf_len)
      {
          int str_size = strlen(old) + buf_len;
          char *str = malloc((str_size + 1) * sizeof(char));
          snprintf(str, str_size, "%s%s", old, buf);
          str[str_size] = '\0';
          free(old);
          return str;
      }
      
      char *exec_and_return(char *command, char **argv)
      {
          int link[2], readlen;
          pid_t pid;
          char buffer[4096];
          char *output;
      
          output = strdup("");
      
          if (pipe(link) < 0)
          {
              return strdup("Could not pipe!");
          }
      
          if ((pid = fork()) < 0)
          {
              return strdup("Could not fork!");
          }
      
          if (pid == 0)
          {
              dup2(link[1], STDOUT_FILENO);
              close(link[0]);
              if (execv(command, argv) < 0)
              {
                  printf("Command not found or access denied: %s\n", command);
                  exit(1);
              }
          }
          else
          {
              close(link[1]);
      
              while ((readlen = read(link[0], buffer, sizeof(buffer))) > 0)
              {
                  output = concat(output, buffer, readlen);
              }
      
              wait(NULL);
          }
          return output;
      }
      
      PHP_FUNCTION(my_exec)
      {
          char *command;
          int command_len, argc, i;
          zval *arguments, **data;
          HashTable *arr_hash;
          HashPosition pointer;
          char **argv;
      
          // recovers a string (s) and an array (a) from arguments
          if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "sa", &command, &command_len, &arguments) == FAILURE) {
              RETURN_NULL();
          }
      
          arr_hash = Z_ARRVAL_P(arguments);
      
          // creating argc and argv from our argument array
          argc = zend_hash_num_elements(arr_hash);
          argv = malloc((argc + 1) * sizeof(char *));
          argv[argc] = NULL;
      
          for (
                  i = 0, zend_hash_internal_pointer_reset_ex(arr_hash, &pointer);
                  zend_hash_get_current_data_ex(arr_hash, (void**) &data, &pointer) == SUCCESS;
                  zend_hash_move_forward_ex(arr_hash, &pointer)
              )
          {
              if (Z_TYPE_PP(data) == IS_STRING) {
                  argv[i] = malloc((Z_STRLEN_PP(data) + 1) * sizeof(char));
                  argv[i][Z_STRLEN_PP(data)] = '\0';
                  strncpy(argv[i], Z_STRVAL_PP(data), Z_STRLEN_PP(data));
                  i++;
              }
          }
      
          char *output = exec_and_return(command, argv);
      
          // freeing allocated memory
          for (i = 0; (i < argc); i++)
          {
              free(argv[i]);
          }
          free(argv);
      
          // WARNING! I guess there is a memory leak here.
          // Second arguemnt to 1 means to PHP: do not free memory
          // But if I put 0, I get a segmentation fault
          // So I think I do not malloc correctly for a PHP extension.
          RETURN_STRING(output, 1);
      }
      

      test.php使用示例

      <?php
      
      dl("my_exec.so");
      
      $output = my_exec("/bin/ls", array("-l", "/"));
      var_dump($output);
      

      shell script 运行那些命令,当然使用你自己的模块目录

      phpize
      ./configure
      make
      sudo cp modules/my_exec_extension.so /opt/local/lib/php/extensions/no-debug-non-zts-20090626/my_exec.so
      

      结果

      KolyMac:my_fork ninsuo$ php test.php
      string(329) ".DS_Store
      .Spotlight-V100
      .Trashes
      .file
      .fseventsd
      .hidden
      .hotfiles.btree
      .vol
      AppleScript
      Applications
      Developer
      Installer Log File
      Library
      Microsoft Excel Documents
      Microsoft Word Documents
      Network
      System
      Users
      Volumes
      bin
      cores
      dev
      etc
      home
      lost+found
      mach_kernel
      net
      opt
      private
      sbin
      tmp
      usr
      var
      vc_command.txt
      vidotask.txt"
      

      我不是 C 开发人员,所以我认为有更简洁的方法可以实现这一点。但你明白了。

      【讨论】:

      • 感谢您提供的信息丰富的回答。它涵盖了关于 PHP 的两个主题:首先,接受 fork 一个无用的 shell 并解决安全问题(escapeshellargs),其次是如何创建守护进程(这部分脱离了所提出的问题)。看看乔治康明斯的赏金。
      • 安全性是任何编程语言中任何 shell exec 的问题,因为大多数 shell(sh、ash、bash、ksh、csh、tcsh 等)略有不同,并且具有不同的保留字符和特殊字符意义。 Escapeshellarg() 是一种危险的安全错觉。更不用说从历史上看,escapeshellarg() 本身就有漏洞。如果您不需要外壳,那么不使用外壳要安全得多。这就是安全 101:减少攻击面。
      • 你提到了escapeshellarg。现在我们知道了 22 岁 (ref) 的 shellshock 漏洞,news.ycombinator.com/item?id=8371357 说:“清理是一个正交问题;再多的 escapeshellarg() 也无法将某人从这个问题中拯救出来(...)”不引入不必要的安全漏洞。在技​​术上不需要时调用 shell 就是这样做的。
      • escapeshellarg() 是愚蠢的。如果 shell 突然不是 PHP 所假设的,它会立即成为一种责任。此外,它在 Windows 下根本不起作用。
      【解决方案3】:

      我考虑试试pcntl_exec()

      【讨论】:

      • 阅读粘贴箱:PCNTL functions not available on this PHP installation.
      • 确实:php.net/manual/en/function.pcntl-fork.php#49949 "当 PHP 用作 Apache 模块时,不能使用函数 'pcntl_fork'。"
      • 不管doc里写的是什么,pcntl_fork()都可以在PHP是Apache模块的时候使用。但是它会分叉整个 Apache 服务器。
      猜你喜欢
      • 1970-01-01
      • 2012-09-25
      • 1970-01-01
      • 2013-05-08
      • 1970-01-01
      • 2015-12-19
      • 1970-01-01
      • 1970-01-01
      • 2016-04-11
      相关资源
      最近更新 更多