Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Run executable from php without spawning a shell

I need to call an executable from an imposed context of a PHP script. Both performance and security wise it's better not to call a shell at all between web server process and executable.

Of course I searched the web, without success (in such a PHP context). Many other languages allow that and document it clearly.

Alas, backticks, exec(), shell_exec(), passthru(), system(), proc_open(), popen() call a shell. And pcntl_fork() seems unavailable.

How to test if a function calls a shell or not.

This was tested on a Debian 6 64bit with PHP 5.3.3-7+squeeze15 . Test code on http://pastebin.com/y4C7MeJz

To get a meaningful test I used a trick which is to ask to execute a shell command not also available as an executable. A good example is umask . Any function returning something like 0022 definitely called a shell. exec(), shell_exec(), passthru(), system(), proc_open() all did. See detailed results on http://pastebin.com/RBcBz02F .

pcntl_fork fails

Now, back the the goal : how to execute arbitrary program without launching a shell ?

Php's exec takes as expected an array of string args instead of a unique string. But pcntl_fork just stops the requests without even a log.

Edit: pcntl_fork failure is because the server uses Apache's mod_php, see http://www.php.net/manual/en/function.pcntl-fork.php#49949 .

Edit: added popen() to the tests, following @hakre suggestion.

Any hint appreciated.

like image 865
Stéphane Gourichon Avatar asked May 16 '13 18:05

Stéphane Gourichon


People also ask

How do I execute a PHP command?

echo "<pre>$output</pre>" ; ?> The exec() function is an inbuilt function in PHP which is used to execute an external program and returns the last line of the output. It also returns NULL if no command run properly.

How do I run a PHP script in Linux?

You can execute linux commands within a php script - all you have to do is put the command line in brackits (`). And also concentrate on exec() , this and shell_exec() ..

Can PHP run a shell script?

The PHP functions to execute shell command are: shell_exec(), exec() or system(). These functions are remarkably similar but have slight differences.

Does PHP exec wait until finished?

PHP exec will wait until the execution of the called program is finished, before processing the next line, unless you use & at the end of the string to run the program in background.


2 Answers

To answer your sentence :

Both performance and security wise it's better not to call a shell at all between web server process and executable.

About performances, well, yes, php internals forks, and the shell itself forks too so that's a bit heavy. But you really need to execute a lot of processes to consider those performances issues.

About security, I do not see any issue here. PHP has the escapeshellarg function to sanitize arguments.

The only real problem I met with exec without pcntl is not a resource nor security issue : it is really difficult to create real deamons (without any attachment to its parent, particularily Apache). I solved this by using at, after double-escaping my command:

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

To get back to your question, the only way I know to execute programs in a standard (fork+exec) way is to use the PCNTL extension (as already mentionned). Anyway, good luck!


To complete my answer, you can create an exec function yourself that does the same thing as pcntl_fork+pcntl_exec.

I made a my_exec extension that does a classic exec+fork, but actually, I do not think it will solve your issues if you're running this function under apache, because the same behaviour as pcntl_fork will apply (apache2 will be forked and there may be unexpected behaviours with signal catching and so on when execv does not succeed).

config.m4 the phpize configuration file

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 the extension

#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 a usage sample

<?php

dl("my_exec.so");

$output = my_exec("/bin/ls", array("-l", "/"));
var_dump($output);

shell script run those commands, of course use your own module directory

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

Result

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"

I am not a C dev, so I think there are cleaner ways to achieve this. But you get the idea.

like image 108
Alain Tiemblo Avatar answered Sep 21 '22 05:09

Alain Tiemblo


In PHP 7.4+, proc_open open processes directly if cmd is passed as array.

As of PHP 7.4.0, cmd may be passed as array of command parameters. In this case the process will be opened directly (without going through a shell) and PHP will take care of any necessary argument escaping.

So this example:

<?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);

outputs:

cmd_string:
bash
php
sh
ps

cmd_array:
bash
php
ps
like image 4
Marcos Oliveira Avatar answered Sep 24 '22 05:09

Marcos Oliveira