<span id="7ztzv"></span>
<sub id="7ztzv"></sub>

<span id="7ztzv"></span><form id="7ztzv"></form>

<span id="7ztzv"></span>

        <address id="7ztzv"></address>

            原文地址:http://drops.wooyun.org/tips/135

            譯自:《Pro PHP Security》

            驗證過濾用戶的輸入


            即使是最普通的字母數字輸入也可能是危險的,列舉幾個容易引起安全問題的字符:

            ! $ ^ & * ( ) ~ [ ] \ | { } ' " ; < > ? - `
            

            在數據庫中可能有特殊意義的字符:

            ' " ; \
            

            還有一些非打印字符:

            字符\x00或者說ASCII 0,NULL或FALSE

            字符\x10和\x13,或者說ASCII 10和13,\n \r

            字符\x1a或者說ASCII 26,表示文件的結束

            輸入錯誤的參數類型,也可能導致程序出現意想不到的錯誤。

            輸入過多的參數值,可能導致溢出等錯誤。

            PHP中驗證用戶的輸入

            這里特別要注意php.ini中的register_globals的設定,在早期的php版本中是默認開啟的,這會導致很嚴重的安全問題:

            #!php
            <?php
            // set admin flag
            if ($auth->isAdmin()) {
            $admin = TRUE;
            }
            // ...
            if ($admin) {
            // do administrative tasks
            }
            ?>
            

            上面這段代碼看起來是安全的,但是如果register_globals開啟了的話,在訪問的url中加入?admin=1即可繞過前半部分的邏輯判斷。

            更安全的代碼應該給$admin賦默認FALSE值:

            #!php
            <?php
            // create then set admin flag
            $admin = FALSE;
            if ($auth->isAdmin()) {
                $admin = TRUE;
            }
            // ...
            if ($admin) {
                // do administrative tasks
            }
            ?>
            

            早期人們開發調試的時候發現使用register_globals有極大的便利,所以早期的php版本中默認開啟。

            但是隨著越來越多的安全問題,從php 4.2.0開始,register_globals變為了默認關閉。

            當你發現register_globals是on的時候,你可能會在腳本當中加入如下代碼使其關閉:

            #!php
            ini_set('register_globals', 0);
            

            但實際上,當所有的全局變量已經創建了之后,以上代碼并不會起到作用。

            但是你可以在文檔的根目錄下的.htaccess的文件中加上下面這一行:

            php_flag register_globals 0
            

            變量聲明:強烈建議總是事先聲明變量。

            檢查輸入的類型,長度和格式:

            字符串檢查:在PHP中,字符串幾乎可以是任何事情,但有些值并不是嚴格的字符串類型,可以用is_string()函數來確定。

            有些時候你不介意數字作為字符串,可以用empty()函數。

            數字類型檢查:使用is_int()函數或者is_integer()或is_long(),例如:

            #!php
            $year = $_POST['year'];
            if (!is_int($year))
            exit("$year is an invalid value for year!");
            

            也可以使用gettype()函數判斷類型后做處理:

            #!php
            if (gettype($year) != 'integer') {
            exit("$year is an invalid value for year!");
            }
            

            至少還有三種方式可以吧$year變量轉變為整數:

            #!php
            $year = intval($_POST['year']);
            $year = ( int ) $_POST['year'];
            if (!settype($year, 'integer')) {exit("$year is an invalid value for year!");}
            

            如果允許浮點型與零的值,可以使用is_numeric()函數來做判斷。 判斷一個值是否為布爾型的時候使用is_bool()函數。

            下表是對各種類型變量使用各函數判斷的結果:

            http://static.wooyun.org/20141017/2014101711111019667.jpg

            檢查字符串的長度使用strlen()變量:

            #!php
            if (strlen($year) != 4)
                exit("$year is an invalid value for year!");
            

            概括總結一下PHP中類型,長度,格式等驗證:

            #!php
            <?php
            // set up array of expected values and types
            $expected = array(
                'carModel' => 'string',
                'year' => 'int',
                'imageLocation' => 'filename'
            );
            // check each input value for type and length
            foreach ($expected AS $key => $type) {
                if (empty($_GET[$key])) {
                    ${$key} = NULL;
                    continue;
                }
                switch ($type) {
                    case 'string':
                        if (is_string($_GET[$key]) && strlen($_GET[$key]) < 256) {
                            ${$key} = $_GET[$key];
                        }
                        break;
                    case 'int':
                        if (is_int($_GET[$key])) {
                            ${$key} = $_GET[$key];
                        }
                        break;
                    case 'filename':
                        // limit filenames to 64 characters
                        if (is_string($_GET[$key]) && strlen($_GET[$key]) < 64) {
                            // escape any non-ASCII
                            ${$key} = str_replace('%', '_', rawurlencode($_GET[$key]));
                            // disallow double dots
                            if (strpos(${$key}, '..') === TRUE) {
                                ${$key} = NULL;
                            }
                        }
                        break;
                }
                if (!isset(${$key})) {
                    ${$key} = NULL;
                }
            }
            // use the now-validated input in your application
            

            對于一些可能有害的字符,可以用如下的幾種方式進行保護:

            可以嘗試在php.ini中開啟magic_quotes_gpc,這樣對于所有由用戶GET、POST、COOKIE中傳入的特殊字符都會轉義。

            也可是使用addslashes()函數,但是開啟magic_quotes_gpc所造成的影響可能遠超過益處。

            addslashes()也只對最常見的四個字符做了轉義:單引號、雙引號、反斜線、空字符。

            同時為了使數據還原,需要使用stripslashes()函數,也可能破壞一些多字節的轉義。

            推薦使用mysql_real_escape_string()函數,雖然只是用來設計轉義插入數據庫的數據,但是他能轉義更多的字符。

            如:NULL、\x00、\n、\r、\、'、"和\x1a。使用用例:

            #!php
            <?php
            $expected = array(
                'carModel',
                'year',
                'bodyStyle'
            );
            foreach ($expected AS $key) {
                if (!empty($_GET[$key])) {
                    ${$key} = mysql_real_escape_string($_GET[$key]);
                }
            }
            ?>
            

            使用mysql_real_escape_string()函數也不是萬能的,轉義一些并非是要寫入mysql的數據庫的數據可能不會產生作用或者出現錯誤。

            可以根據自己的實際需要,自己使用str_replace()函數寫一個針對特殊字符的轉義。

            對于文件的路徑與名稱的過濾

            文件名中不能包含二進制數據,否則可能引起問題。

            一些系統允許Unicode多字節編碼的文件名,但是盡量避免,應當使用ASCII的字符。

            雖然Unix系統幾乎可以在文件名設定中使用任何符號,但是應當盡量使用 - 和 _ 避免使用其他字符。

            同時需要限定文件名的長度。

            php中的文件操作通常使用fopen()函數與file_get_contents()函數。

            #!php
            <?php
            $applicationPath = '/home/www/myphp/code/';
            $scriptname      = $_POST['scriptname'];
            highlight_file($applicationPath . $scriptname);
            ?>
            

            上面代碼的問題在于用戶POST輸入的scriptname沒有做任何過濾,如果用戶輸入../../../../etc/passwd,則有可能讀取到系統的passwd文件。

            #!php
            <?php
            $uri = $_POST['uri'];
            if (strpos($uri, '..'))
                exit('That is not a valid URI.');
            $importedData = file_get_contents($uri);
            

            如果發現 .. 字符串就不執行會不會出現問題呢?如果前面并沒有路徑限制的話,仍然會出現問題:

            使用file協議,當用戶輸入file:///etc/passwd的時候,會把passwd的內容帶入$importedData變量中。

            防止SQL注入


            SQL注入是如何產生的:

            1、接收一個由用戶提交的變量,假設變量為$variety:

            #!php
            $variety = $_POST['variety'];
            

            2、接收的變量帶入構造一個數據庫查詢語句:

            #!php
            $query = "SELECT * FROM wines WHERE variety='$variety'";
            

            3、把構造好的語句提交給MySQL服務器查詢,MySQL返回查詢結果。

            當由用戶輸入lagrein' or 1=1#時,產生的結果將會完全不同。

            防止SQL注入的幾種方式:

            檢查用戶輸入的類型,當用戶輸入的為數字時可以使用如下方式:

            使用is_int()函數(或is_integer()或is_long()函數)

            使用gettype()函數

            使用intval()函數

            使用settype()函數

            檢查用戶輸入字符串的長度使用strlen()函數。

            檢查日期或時間是否是有效的,可以使用strtotime()函數

            對于一個已經存在的程序來說,可以寫一個通用函數來過濾:

            #!php
            function safe($string)
            {
                return "'" . mysql_real_escape_string($string) . "'";
            }
            

            調用方式:

            #!php
            $variety = safe($_POST['variety']);
            $query   = "SELECT * FROM wines WHERE variety=" . $variety;
            

            對于一個剛開始寫的程序,應當設計的更安全一些,PHP5中,增加了MySQL支持,提供了mysqli擴展:

            PHP手冊地址:http://php.net/mysqli

            #!php
            <?php
            // retrieve the user's input
            $animalName = $_POST['animalName'];
            // connect to the database
            $connect    = mysqli_connect('localhost', 'username', 'password', 'database');
            if (!$connect)
                exit('connection failed:  ' . mysqli_connect_error());
            // create a query statement resource
            $stmt = mysqli_prepare($connect, "SELECT intelligence FROM animals WHERE name = ?");
            if ($stmt) {
                // bind the substitution to the statement
                mysqli_stmt_bind_param($stmt, "s", $animalName);
                // execute the statement
                mysqli_stmt_execute($stmt);
                // retrieve the result...
                mysqli_stmt_bind_result($stmt, $intelligence);
                // ...and display it
                if (mysqli_stmt_fetch($stmt)) {
                    print "A $animalName has $intelligence intelligence.\n";
                } else {
                    print 'Sorry, no records found.';
                }
                // clean up statement resource
                mysqli_stmt_close($stmt);
            }
            mysqli_close($connect);
            ?>
            

            mysqli擴展提供了所有的查詢功能。

            mysqli擴展也提供了面向對象的版本:

            #!php
            <?php
            $animalName = $_POST['animalName'];
            $mysqli     = new mysqli('localhost', 'username', 'password', 'database');
            if (!$mysqli)
                exit('connection failed:  ' . mysqli_connect_error());
            $stmt = $mysqli->prepare("SELECT intelligence FROM animals WHERE name = ?");
            if ($stmt) {
                $stmt->bind_param("s", $animalName);
                $stmt->execute();
                $stmt->bind_result($intelligence);
                if ($stmt->fetch()) {
                    print "A $animalName has $intelligence intelligence.\n";
                } else {
                    print 'Sorry, no records found.';
                }
                $stmt->close();
            }
            $mysqli->close();
            ?>
            

            防止XSS攻擊


            xss攻擊一個常用的方法就是注入HTML元素執行js腳本,php中已經內置了一些防御的函數(如htmlentities或者htmlspecialchars):

            #!php
            <?php
            function safe($value)
            {
                htmlentities($value, ENT_QUOTES, 'utf-8');
                // other processing
                return $value;
            }
            // retrieve $title and $message from user input
            $title   = $_POST['title'];
            $message = $_POST['message'];
            // and display them safely
            print '<h1>' . safe($title) . '</h1>
                   <p>' . safe($message) . '</p>';
            ?>
            

            過濾用戶提交的URL

            如果允許用戶輸入一個URL用來調用一個圖片或者鏈接,你需要保證他不傳入javascript:或者vbscript:或data:等非http協議。

            可以使用php的內置函數parse_url()函數來分割URL,然后做判斷。

            設置允許信任的域:

            #!php
            <?php
            $trustedHosts      = array(
                'example.com',
                'another.example.com'
            );
            $trustedHostsCount = count($trustedHosts);
            function safeURI($value)
            {
                $uriParts = parse_url($value);
                for ($i = 0; $i < $trustedHostsCount; $i++) {
                    if ($uriParts['host'] === $trustedHosts[$i]) {
                        return $value;
                    }
                }
                $value .= ' [' . $uriParts['host'] . ']';
                return $value;
            }
            // retrieve $uri from user input
            $uri = $_POST['uri'];
            // and display it safely
            echo safeURI($uri);
            ?>
            

            防止遠程執行


            遠程執行通常是使用了php代碼執行如eval()函數,或者是調用了命令執行如exec(),passthru(),proc_open(),shell_exec(),system()或popen()。

            注入php代碼:

            php為開發者提供了非常多的方法可以來調用允許php腳本,我們就需要注意對用戶可控的數據進行過濾。

            調用的幾種方式:

            include()和require()函數,eval()函數,preg_replace()采用e模式調用,編寫腳本模板。

            #!php
            <?php
            print Hello . world;
            ?>
            

            上面代碼將會輸出Helloworld,php在解析的時候會檢查是否存在一個名為Hello的函數。

            如果沒有找到的話,他會自己創建一個并把它的名字作為它的值,world也是一樣。

            上傳文件中嵌入php代碼:

            攻擊者可以上傳一個看似很普通的圖片,PDF等,但是實際上呢?

            linux下可以使用如下命令插入php代碼進入圖片中:

            $ echo '<?php phpinfo();?>' >> locked.gif
            

            代碼插入到了locked.gif圖片中。

            并且此時用file命令查看文件格式仍為圖片:

            $ file -i locked.giflocked.gif: image/gif
            

            任何的圖像編輯或圖像處理的程序包括PHP的getimagesize()函數,都會認為它是一個GIF圖像。

            但是當你使用cat命令查看圖片時,可以看到圖片末尾的

            當把圖片的后綴改為php或者已php的方式解析時,插入的phpinfo()函數便會執行。

            Shell命令執行

            PHP提供了一些可以直接執行系統命令的函數,如exec()函數或者 `(反引號)。

            PHP的安全模式會提供一些保護,但是也有一些方式可以繞過安全模式:

            1、上傳一個Perl腳本,或者Python或Ruby等,服務器支持的環境,來執行其他語言的腳本可繞過PHP的安全模式。

            2、利用系統的緩沖溢出漏洞,繞過安全模式。

            下表列出了跟Shell相關的一些字符:

            名稱 字符 ASCII 16進制 URL編碼 HTML編碼
            換行 10 \x0a %0a &#10
            感嘆號 ! 33 \x21 %21 &#33
            雙引號 " 34 \x22 %22 &#34或&quot
            美元符號 $ 36 \x24 %24 &#36
            連接符 & 38 \x26 %26 &#38或&#amp
            單引號 ' 39 \x27 %27 &#39
            左括號 ( 40 \x28 %28 &#40
            右括號 ) 41 \x29 %29 &#41
            星號 * 42 \x2a %2a &#42
            連字符號 - 45 \x2d %2d &#45
            分號 ; 59 \x3b %3b &#59
            左尖括號 < 60 \x3c %3c &#60
            右尖括號 > 62 \x3e %3e &#62
            問號 ? 63 \x3f %3f &#63
            左方括號 [ 91 \x5b %5b &#91
            反斜線 \ 92 \x5c %5c &#92
            右方括號 ] 93 \x5d %5d &#93
            插入符 ^ 94 \x5e %5e &#94
            反引號 ` 96 \x60 %60 &#96
            左花括號 { 123 \x7b %7b &#123
            管道符 | 124 \x7c %7c &#124
            右花括號 } 125 \x7d %7d &#125
            波浪號 ~ 126 \x7e %7e &#126

            如下PHP腳本:

            #!php
            <?php
            // get the word count of the requested file
            $filename = $_GET['filename'];
            $command  = "/usr/bin/wc $filename";
            $words    = shell_exec($command);
            print "$filename contains $words words.";
            ?>
            

            用戶可以輸入如下的URL來攻擊讀取passwd文件:

            wordcount.php?filename=%2Fdev%2Fnull%20%7C%20cat%20-%20%2Fetc%2Fpasswd
            

            字符串拼接之后,將會執行 /usr/bin/wc /dev/null | cat - /etc/passwd這條命令

            如果能夠不適用命令執行函數與eval()函數,可以在php.ini中禁止:disable_functions = "eval,phpinfo"

            PHP中還有一個preg_replace()函數,可能引起代碼執行漏洞。

            mixed preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit ] )
            

            在 subject 中搜索 pattern 模式的匹配項并替換為 replacement 。如果指定了 limit ,則僅替換 limit 個匹配。

            如果省略 limit 或者其值為 -1,則所有的匹配項都會被替換。

            特別注意:

            /e 修正符使 preg_replace() 將 replacement 參數當作 PHP 代碼(在適當的逆向引用替換完之后)。

            提示:要確保 replacement 構成一個合法的 PHP 代碼字符串,否則 PHP 會在報告在包含 preg_replace() 的行中出現語法解析錯誤。

            #!php
            <?php
            function test($str)
            {
                //......
                //......
                return $str;
            }
            echo preg_replace("/\s*\[p hp language=""](.+?)\[\/php\]\s*/ies", 'test("\1")', $_GET["h"]);
            ?>
            

            當用戶輸入

            ?h=[p hp]phpinfo()[/php]
            

            經過正則匹配后, replacement 參數變為'test("phpinfo()")',

            此時phpinfo僅是被當做一個字符串參數了。

            但是當我們提交

            ?h=[p hp]{${phpinfo()}}[/php]
            

            時,phpinfo()就會被執行。

            在php中,雙引號里面如果包含有變量,php解釋器會將其替換為變量解釋后的結果;單引號中的變量不會被處理。

            注意:雙引號中的函數不會被執行和替換。

            在這里我們需要通過{${}}構造出了一個特殊的變量,'test("{${phpinfo()}}")',達到讓函數被執行的效果 ${phpinfo()} 會被解釋執行。

            <span id="7ztzv"></span>
            <sub id="7ztzv"></sub>

            <span id="7ztzv"></span><form id="7ztzv"></form>

            <span id="7ztzv"></span>

                  <address id="7ztzv"></address>

                      亚洲欧美在线