这篇博客是作者总结b站 橙子科技工作室 的,并添加了一些自己的理解,已和up主说明
目录
序列化基础知识
序列化(serialization)是将对象的状态信息(属性)转换为可以存储或传输的形式的过程。
将对象或数组转化为可储存/传输的字符串
表达方式
<?php
$a = null;
echo serialize($a);
?>
数组序列化
<?php
$a = array('love','share','peace');
echo $a[0];
echo "<br>";
echo serialize($a);
?>
输出:
love
a:3:{i:0;s:4:"love";i:1;s:5:"share";i:2;s:5:"peace";}
对象序列化
<?php
class test{
public $pub='yasuo';
function jineng(){
echo $this->pub; //在类里面调用类的成员属性时,就要用 $this
}
}
$a = new test();
echo serialize($a);
?>
输出:
O:4:"test":1:{s:3:"pub";s:5:"yasuo";}
私有属性序列化
<?php
class test{
private $pub='yasuo';
function jineng(){
echo $this->pub;
}
}
$a = new test();
echo serialize($a);
echo "<br>";
echo urlencode(serialize($a));
?>
输出:
O:4:"test":1:{s:9:"testpub";s:5:"yasuo";}
O%3A4%3A%22test%22%3A1%3A%7Bs%3A9%3A%22%00test%00pub%22%3Bs%3A5%3A%22yasuo%22%3B%7D
private 私有属性序列化时,在变量名前加 "%00类名%00"(ascii码中 0 经过url编码后为%00)所以字符串长度变为 3+4(类名长度)+2。
protected 受保护属性序列化时在变量名前加 "%00*%00"
对象里面调用对象
<?php
class test{
var $pub='yasuo';
function jineng(){
echo $this->pub;
}
}
class test2{
var $yasuo;
}
$b = new test();
$a = new test2();
$a->yasuo = $b; //实例化后的对象$a的成员变量'yasuo'调用实例化后的对象$b
echo serialize($a);
?>
输出:
O:5:"test2":1:{s:5:"yasuo";O:4:"test":1:{s:3:"pub";s:5:"yasuo";}}
反序列化的特性
- 反序列化之后的内容为一个对象
- 反序列化生成的对象里的值,由反序列化里的值提供;与原有类预定义的值无关
- 反序列化不触发类的成员方法;需要调用方法后才能触发
魔术方法
魔术方法是一个预定义好的,在特定情况下自动触发的行为方法
__construct()
构造函数,在实例化一个对象的时候,首先会去自动执行的一个方法;
触发时机:实例化对象时
<?php
class user{
public $username;
public function __construct($username){
$this->username=$username;
echo "触发了构造函数1次";
}
}
$a = new user("put"); //实例化对象时触发构造函数__construct()
$b = serialize($a);
unserialize($b);
?>
__destruct()
析构函数,在对象的所有引用被删除或者当对象被显式销毁时执行的魔术方法
触发时机:对象引用完成,或对象被销毁
<?php
class user{
var $cmd="system('ls');";
public function __destruct(){
eval($this->cmd);
}
} //unserialize()触发__destruct()
$a = $_GET['b']; //destruct()执行eval()
unserialize($a); //eval()触发代码
?>
输入:
?b=O:4"user":1:{s:3:"cmd";s:13:"system('ls');";}
__sleep()
序列化 serialize()函数会检查类中是否存在一个魔术方法 __sleep()。
如果存在,该方法会先被调用,然后才执行序列化操作
触发时机:序列化serialize()之前
功能:对象被序列化之前触发,返回需要被序列化存储的成员属性,删除不必要的属性
参数:成员属性
返回值:需要被序列化存储的成员属性
<?php
class User{
const SITE = 'uusama';
public $username;
public $nickname;
private $password;
public function __construct($username,$nickname,$password){
$this->username = $username;
$this->nickname = $nickname;
$this->password = $password;
}
public function __sleep(){
return array('username','nickname');
}
}
$user = new User('a','b','c'); //这里触发__construct()函数
echo serialize($user); //执行这步之前触发__sleep()函数
?> //所以最后输出结果只有username和nickname
输出:
O:4:"User":2:{s:8:"username";s:8:"nickname";s:1:"b";}
__wakeup()
unserialize()会检查是否存在一个 __wakeup() 方法
如果存在,则会先调用 __wakeup() 方法,预先准备对象需要的资源
触发时机:反序列化unserialize() 之前
__wakeup()绕过
属性个数绕过
如果序列化字符串中表示对象属性个数的值大于真实属性个数时,会跳过 __wakeup() 的执行
或者直接增加在后面属性个数,例如:
O:1:"B":1:{s:1:"a";O:1:"A":1:{s:4:"code";s:10:"phpinfo();";}s:1:"z":Z;}
后面的Z就是加上的
环境限制:PHP5<5.6.25,PHP7<1.0.10
__toString()
表达方式错误导致魔术方法触发
触发时机:把对象当成字符串调用
<?php
class User{
var $benben = "this is a test";
public function __toString(){
return '格式错误,无法输出';
}
}
$test = new User();
print_r($test);
echo $test;
?>
输出结果会返回一个 格式错误,无法输出
把类User实体化并赋值给$test,此时$test是个对象,调用对象可以使用print_r或者var_dump
如果使用echo或者print只能调用字符串的方式去调用对象,即把对象当成字符串使用,此时自动触发toString()
__invoke()
格式表达错误导致魔术方法触发
触发时机:把对象当成函数调用
<?php
class User{
var $a = "this is a test";
public function __invoke(){
echo '它不是一个函数';
}
}
$test = new User();
echo $test->a;
echo $test()->a;
?>
输出结果会有:它不是一个函数
把类User实体化并赋值给$test为对象,正常输出对象的值为a,加上()是把test当初函数来调用,此时就会触发invoke()
__call()
触发时机:调用一个不存在的方法
<?php
class User{
public function __call($arg1,$arg2){
echo "$arg1,$arg2[0]";
}
}
$test = new User();
$test->callxxx('a');
?>
调用的方法callxxx()不存在,触发魔术方法call(),传参$arg1(callxxx),$arg2(a)
$arg1:调用的不存在的方法的名称;
$arg2:调用的不存在的方法的参数;
__callStatic()
触发时机:静态调用或调用成员常量时使用的方法不存在
__get()
触发时机:调用的成员属性不存在
返回值:不存在的成员属性的名称
<?php
class User{
public $var1
public function __get($arg1){
echo $arg1;
}
}
$test = new User();
$test->var2;
?>
调用的成员属性var2不存在,触发get(),把不存在的属性名称var2赋值给$arg1
__set()
触发时机:给不存在的成员属性赋值
返回值:不存在的成员属性的名称和赋值
<?php
class User{
public $var1
public function __set($arg1,$arg2){
echo $arg1.','.$arg2;
}
}
$test = new User();
$test->var2=1;
?>
__isset()
触发时机:对不可访问属性使用 isset() 或 empty() 时,__isset() 会被调用
参数:传参$arg1
返回值:不存在的成员属性的名称
<?php
class User{
private $var;
public function __isset($arg1){
echo $arg1;
}
}
$test = new User();
isset($test->var);
?>
isset() 调用的成员属性var不可访问或不存在触发isset(),返回$arg,这里不存在成员属性的名称
__unset()
触发时机:对不可访问属性使用 unset() 时
返回值:不存在的成员属性和名称
__clone()
触发时机:当使用 clone 关键字拷贝完成一个对象后,新对象会自动调用定义的魔术方法 __clone()
<?php
class User{
private $var;
public function __clone(){
echo "this is a test";
}
}
$test = new User();
$newclass = clone($test);
?>
字符串逃逸
在前面字符串没有问题的情况下,;} 是反序列化结束符,后面的字符串不影响反序列化结果(成员属性数量一致;成员属性名称长度,内容长度一致)
属性逃逸
一般在数据先经过一次serialize再经过unserialize,在这个中间反序列化的字符串变多或者变少的时候才有可能存在反序列化属性逃逸
字符串逃逸(字符减少)
<?php
class A{
public $v1 = "abcsystem()";
public $v2 = "123";
}
$data = serialize(new A()); //O:1:"A":2:{s:2:"v1";s:11:"abcsystem()";s:2:"v2";s:3:"123";}
$data = str_replace("system()","",$data); //str_replace把"system()"替换为"空"
echo $data; //O:1:"A":2:{s:2:"v1";s:11:"abc";s:2:"v2";s:3:"123";}
?>
这里因为 str_replace() 把 system() 被过滤,导致字符串缺失,此时内容长度不一致,导致该属性的内容后面的字符串被当作该属性的内容
此时修改内容长度和后面的字符串,从而在后面构造出想要的代码
这里就多逃逸出一个成员属性:v3,内容为空
字符串逃逸(字符增多)
<?php
class A{
public $v1 = 'ls';
public $v2 = '123';
}
$data = serialize(new A());
echo $data; //O:1:"A":2:{s:2:"v1";s:2:"ls";s:2:"v2";s:3:"123";}
$data = str_replace("ls","pwd",$data); //这里将 ls 替换为 pwd
echo $data; //O:1:"A":2:{s:2:"v1";s:2:"pwd";s:2:"v2";s:3:"123";}
?>
经过 str_replace() 替换后,字符增多,会把多出来的字符吐出来;所有可以在吐出来的字符中构造想要的代码
例如:
想要逃逸出一个新的属性 v3,那么要增加的字符串就是:";s:2:"v3";s:3:"666";} ,一共22位字符,一个 ls 转位 pwd 增加一位字符,所以需要22个ls转成pwd
总结
一般题目中出现会将字符串长度进行变化的时候,就可以考虑是否能用字符串逃逸来解题
引用
引用类似于 c 语言中的指针
这道题需要让 secret = enter ,就能得到 flag,secret 会赋值为 * ,enter 可以通过传参传进去,但是前面的 str_replace() 函数会把 * 转为 \*,所以这里用引用(&)来使 enter 的值为 *
$a->enter = &$a->secret;