44 votes

Exécuter un exécutable à partir de php sans faire apparaître un shell

J'ai besoin d'appeler un exécutable depuis un contexte imposé d'un script PHP. En termes de performances et de sécurité, il est préférable de ne pas appeler de shell du tout entre le processus du serveur web et l'exécutable.

J'ai bien sûr cherché sur le web, sans succès (dans un tel contexte de PHP). De nombreux autres langages le permettent et le documentent clairement.

Hélas, des backticks, exec() , shell_exec() , passthru() , system() , proc_open() , popen() appeler un shell. Et pcntl_fork() semble indisponible.

Comment tester si une fonction appelle un shell ou non.

Ceci a été testé sur une Debian 6 64bit avec PHP 5.3.3-7+squeeze15 . Code de test sur http://pastebin.com/y4C7MeJz

Pour obtenir un test significatif, j'ai utilisé une astuce qui consiste à demander l'exécution d'une commande shell qui n'est pas disponible en tant qu'exécutable. Un bon exemple est umask . Toute fonction retournant quelque chose comme 0022 est définitivement appelée un shell. exec() , shell_exec() , passthru() , system() , proc_open() tous l'ont fait. Voir les résultats détaillés sur http://pastebin.com/RBcBz02F .

pcntl_fork échoue

Maintenant, revenons au but : comment exécuter un programme arbitraire sans lancer un shell ?

L'exécution de Php prend comme prévu un tableau de chaînes d'arguments au lieu d'une chaîne unique. Mais pcntl_fork arrête juste les requêtes sans même un journal.

Edit : l'échec de pcntl_fork est dû au fait que le serveur utilise le mod_php d'Apache, voir http://www.php.net/manual/en/function.pcntl-fork.php#49949 .

Edit : ajouté popen() aux tests, en suivant la suggestion de @hakre.

Tout conseil est le bienvenu.

4 votes

La première fois que j'ai découvert que PHP ne semblait offrir aucun mécanisme pour appeler un programme externe sans passer par une ligne de commande via un shell, j'ai été alarmé et étonné. Un langage de programmation polyvalent et populaire, qui dispose de commandes telles que exec() y system() a ce défaut et qu'il n'a jamais été corrigé ? VRAIMENT ? Mais... Je suppose que c'est vrai.

8 votes

@celada : vous devez réaliser que PHP n'est qu'une enveloppe autour d'un grand nombre de fonctions standard glib/libc. PHP lui-même ne peut rien faire que glib/libc ne fassent eux-mêmes, et l'une de ces fonctions est l'exécution d'un autre processus via un shell.

0 votes

L'umask de PHP ne fonctionnerait-il pas ? us2.php.net/umask

12voto

Alain Tiemblo Points 8884

Pour répondre à votre phrase :

En termes de performance et de sécurité, il est préférable de ne pas appeler de shell du tout entre le processus du serveur web et l'exécutable. entre le processus du serveur web et l'exécutable.

En ce qui concerne les performances, eh bien, oui, les internes de php fourchent, et la coque elle-même fourche aussi, donc c'est un peu lourd. Mais il faut vraiment exécuter un grand nombre de processus pour envisager ces problèmes de performances.

En ce qui concerne la sécurité, je ne vois pas de problème ici. PHP a la escapeshellarg pour assainir les arguments.

Le seul vrai problème que j'ai rencontré exec sans pcntl n'est pas un problème de ressource ni de sécurité : il est vraiment difficile de créer réel deamons (sans aucun attachement à son parent, en particulier Apache ). J'ai résolu ce problème en utilisant at après avoir doublement échappé à mon commandement :

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

Pour en revenir à votre question, la seule façon que je connaisse d'exécuter des programmes dans une standard (fork+exec) est d'utiliser la fonction PCNTL extension (comme déjà mentionné). En tout cas, bonne chance !


Pour compléter ma réponse, vous pouvez créer un exec vous-même qui fait la même chose que pcntl_fork + pcntl_exec .

J'ai fait un my_exec extension qui fait un classique exec+fork, mais en fait, Je ne pense pas que cela résoudra vos problèmes si vous exécutez cette fonction sous apache. car le même comportement que pcntl_fork s'appliquera (apache2 sera bifurqué et il peut y avoir des comportements inattendus avec la capture de signaux et ainsi de suite quand execv ne réussit pas).

config.m4 le site phpize fichier de configuration

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 l'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 un échantillon d'utilisation

<?php

dl("my_exec.so");

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

shell script exécuter ces commandes, en utilisant bien sûr votre propre répertoire de modules

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"

Je ne suis pas un développeur C, je pense donc qu'il existe des moyens plus propres d'y parvenir. Mais vous comprenez l'idée.

0 votes

Merci pour cette réponse informative. Elle couvre deux sujets sur PHP : premièrement, accepter de forker un shell inutile et contourner la sécurité (escapeshellargs), et deuxièmement, comment créer des démons (cette partie s'éloigne de la question posée). Voir la prime de George Cummins.

4 votes

La sécurité est un problème avec tout shell exec dans n'importe quel langage de programmation, parce que la plupart des shells (sh, ash, bash, ksh, csh, tcsh, etc.) sont légèrement différents et ont des caractères réservés différents avec une signification spéciale. Escapeshellarg() est une dangereuse illusion de sécurité. Sans compter qu'historiquement, escapeshellarg() présente lui-même des vulnérabilités. Si vous n'avez pas besoin d'un shell, il est bien plus sûr de ne pas en utiliser. C'est la sécurité 101 : réduire la surface d'attaque.

2 votes

Vous avez mentionné escapeshellarg . Nous savons maintenant que le jeune homme de 22 ans ( réf. ) bogue shellshock, news.ycombinator.com/item?id=8371357 dit : "L'assainissement est un problème orthogonal ; aucune quantité de escapeshellarg() ne sauvera quelqu'un de ce problème (...)". La sécurité exige de ne pas introduire de failles de sécurité inutiles. Appeler un shell quand ce n'est pas techniquement nécessaire fait exactement cela.

4voto

Marcos Oliveira Points 131

En PHP 7.4+, proc_open ouvrir les processus directement si cmd est passé comme un tableau.

Depuis PHP 7.4.0, cmd peut être passé comme un tableau de paramètres de commande. Dans ce cas, le processus sera ouvert directement (sans passer par un shell) et PHP se chargera de l'échappement des arguments.

Donc cet exemple :

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

sorties :

cmd_string:
bash
php
sh
ps

cmd_array:
bash
php
ps

0voto

Felype Points 346

J'envisage d'essayer pcntl_exec()

0 votes

Lisez la fiche d'information : PCNTL functions not available on this PHP installation.

2 votes

En effet : php.net/manual/fr/function.pcntl-fork.php#49949 "Il n'est pas possible d'utiliser la fonction 'pcntl_fork' lorsque PHP est utilisé comme module Apache".

0 votes

Indépendamment de ce qui est écrit dans la doc, pcntl_fork() peut être utilisé lorsque PHP est un module Apache. Cependant, elle va forker tout le serveur Apache.

-2voto

Stampy Points 227

Vous ne pouvez pas lancer un programme arbitraire sans un shell - le shell est l'enveloppe autour du programme en cours d'exécution qui fournit ses paramètres d'environnement, comme PWD et PATH.

Si vous voulez avoir la possibilité d'exécuter des commandes système sans lancer un shell, réécrivez les commandes système en tant que modules PHP et installez-les dans votre serveur PHP, et exécutez-les en tant que fonctions.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X