序列化
序列化是将对象转换成字符串。但仅保留对象里的成员变量,不保留函数方法。将对象序列化有利于对象的保存和传输,也可以让多个文件共享对象。
反序列化
反序列化是将字符串恢复成对象。
PHP的序列化实现
源码:
<?php
class person{
public $name='zhangsan';
public $sex='boy';
public $age='18';
}
$zhangsan=new person();
$str=serialize($zhangsan);
echo $str;
?>
运行结果:
分析: 创建了一个person类,在这个类中定义了三个属性,然后实例化了一个$zhangsan对象。使用serializa()函数将$zhangsan这个对象进行了序列化,并将序列化之后的字符串进行输出。
序列化后的字符串详解:
O:6:"person":3:{s:4:"name";s:8:"zhangsan";s:3:"sex";s:3:"boy";s:3:"age";s:2:"18";}[Finished in 0.3s]
O代表对象 因为我们序列化的是一个对象 序列化数组则用A来表示
6 代表类名字person占6个字符
person 类名
3 代表三个属性
s 代表字符串
4 代表属性名长度
name 属性名
s:8:"zhangsan 字符串 属性值长度 属性值
访问控制修饰符
类当中属于的访问控制修饰符不通,序列化后的属性长度和属性值也会不同。
- public(公有)
- protected(受保护)
- private(私有的)
protected属性被序列化后属性名长度会增加3,因为属性名会变成%00*%00属性名。
private属性被序列化后属性名会变成%00类名%00属性名。
魔术方法 sleep()
serialize()函数在序列化对象的时候会检查类中是否存在魔术方法sleep()。如果存在,sleep()方法会先被调用,然后再执行序列化操作。__sleep()方法可以确定那些属性被序列化。如果没有__sleep()方法则默认序列化所有属性。
上述定义的sleep方法,返回了一个数组,致使sex和age属性被序列化name没有被序列化。
unserialize()后会导致 __wakeup()
或 __destruct()
的直接调用,中间无需其他过程.
因此最理想的情况就是一些漏洞/危害代码在 __wakeup()
或 __destruct()
中,从而当我们控制序列化字符串时可以去直接触发它们 .
魔术方法 __destruct()
脚本运行结束之前会调用对象的析构函数
<?php
class person{
public $name='zhangsan';
public $sex='boy';
public $age='16';
function __destruct(){
echo $this->name;
}
}
$id=$_GET['id'];
$un=unserialize($id);
?>
源码分析:get方法获取一个序列化之后的字符串,然后由unserialize()反序列化成对象,然后在脚本运行结束的时候该对象调用__destruct()方法,输出对象的$name属性的值。
运行结果:
漏洞利用:我们可以构建payload,将恶意代码插入序列化的字符串,然后将恶意代码转换成对象,调用__destruct()方法执行恶意代码。
payload:O:6:"person":3:{s:4:"name";s:25:"<script>alert(1)</script>";s:3:"sex";s:3:"boy";s:3:"age";s:2:"18";}
注意:属性的值改变,属性的值的长度也要改变成相应的长度。
魔术方法 __wakeup()
unserialize()函数在序列化对象的时候会检查类中是否存在魔术方法wakeup()。如果存在,wakeup()方法会先被调用,然后再执行反序列化操作。可以再__wakeup()方法中对属性进行初始化或者改变。
比如:在进行反序列化的时候对属性进行重新赋值。
<?php
class person{
public $name='zhangsan';
public $sex='boy';
public $age='18';
function __sleep(){
return array('name','sex','age');
}
function __wakeup(){
$this->name='renew name';
}
}
$zhangsan=new person();
echo(serialize($zhangsan))."<br>";
$id=$_GET['id'];
$un=unserialize($id);
var_dump($un);
?>
源码分析:实例化一个对象,然后将它序列化后输出。再进行然序列化,调用了wakeup()函数,导致序列化后$name变成了renew name。
这些看起来似乎是没有什么危害,但是如果wakeup()函数处定义的代码不是重新赋值,而是漏洞代码危害就大了。
<?php
class person{
public $name='zhangsan';
public $sex='boy';
public $age='18';
function __sleep(){
return array('name','sex','age');
}
function __wakeup(){
$fp=fopen("shell.php", "w");
fwrite($fp, $this->name);
fclose($fp);
}
}
$zhangsan=new person();
echo(serialize($zhangsan))."<br>";
$id=$_GET['id'];
$un=unserialize($id);
var_dump($un);
require "shell.php";
?>
源码分析:unserialize()反序列化之后会调用wakeup()函数,将对象属性name的值写入shell.php文件当中,然后去包含它。
如何利用unserialize()函数不会直接调用的魔术函数。
当wakeup()函数又调用了其他对象,层层溯源,还是可以调用的
构造函数__construct()
<?php
class test{
function __construct($name){
$fp=fopen("shell.php", "w");
fwrite($fp, $name);
fclose($fp);
echo "123456上山大老鼠";
}
}
class person{
public $name='zhangsan';
public $sex='boy';
public $age='18';
function __sleep(){
return array('name','sex','age');
}
function __wakeup(){
$obj=new test($this->name);
}
}
$zhangsan=new person();
echo(serialize($zhangsan))."<br>";
$id=$_GET['id'];
$un=unserialize($id);
var_dump($un);
require "shell.php";
?>
运行结果:
普通同名方法利用
<?php
class main{
var $test;
function __construct(){
$this->test=new test1();
}
function __destruct(){
$this->test->action();
}
}
class test1{
function action(){
echo "hello world";
}
}
class test2{
var $test2;
function action(){
eval($this->test2);
}
}
$a=new main();
$b=unserialize($_GET['test']);
print_r($b);
?>
源码分析:
$a=new main(); 从main类实例化对象$a;
$this->test=new test1();从test1类实例化对象$test
当脚本运行结束时,
$this->test->action();调用test1类的action()方法,输出“hello world”
我们看到test2类中也有action()方法,且该方法中存在代码执行漏洞。于是我们可以构建序列化字符串来调用test2类中的方法 而不是test1中的方法。
获取序列化字符串
$a=new main(); 中方法destruct()调用的action()方法是一样的,区别是$this->test=new test1();确定调用那个类中的方法,我们手工设置,让其对象调用的是test2中类的方法,并为其赋值危险代码。
直接访问php2.php
带入payload:
魔术方法__toString()
当对象被输出的时候,会调用__toString()方法
<?php
// 当他的对象被输出时候,调用__toString
class FileClass{
public $filename = 'shy.php';
// 返回读取一个文件的内容
public function __toString(){
return file_get_contents($this->filename);
}
}
$file=new FileClass();
echo(serialize($file));
echo $file;
实例化了一个对象$file,当我输出这个对象的时候,调用了__toString()方法,读取了文件shy.php的内容。
练习:
1,神盾局的秘密
访问http://web.jarvisoj.com:32768/
查看源码
继续接着访问:http://web.jarvisoj.com:32768/showimg.php?img=c2hpZWxkLmpwZw==
发现没有显示出图片,而且类似加载了文件的源码,可能存在任意文件读取漏洞。
读取index.php
isset() 函数用于检测变量是否已设置并且非 NULL。
empty() 函数用于检查一个变量是否为空。
readfile() 函数读取一个文件,并写入到输出缓冲。
如果成功,该函数返回从文件中读入的字节数。如果失败,该函数返回 FALSE 并附带错误信息。您可以通过在函数名前面添加一个 '@' 来隐藏错误输出。
读取showing
过滤了.. / \\ pctf了,如果存在,则输出File not found!,否则执行readfile($f);
查看shield.php文件代码:
shield.php中shield类中定义了readfile()方法,如果$file不为空,文件名中没有.. / \\ ,返回该文件的内容。
提示了//flag is in pctf.php,但是过滤了无法读取,直接访问也不行
因为index.php中包含了shield.php,所以放在一起进行分析:
<?php
//flag is in pctf.php
class Shield {
public $file;
function __construct($filename = '') {
$this -> file = $filename;
}
function readfile() {
if (!empty($this->file) && stripos($this->file,'..')===FALSE
&& stripos($this->file,'/')===FALSE && stripos($this->file,'\\')==FALSE) {
return @file_get_contents($this->file);
}
}
}
$x = new Shield();
isset($_GET['class']) && $g = $_GET['class'];
if (!empty($g)) {
$x = unserialize($g);
}
echo $x->readfile();
?>
我发现在这个类定义的方法中没有过滤pctf,可以通过这个读取pctf.php
第一步:构建序列化字符串
第二步,通过get方法传入序列化字符串
果然真的flag写在注释里面。
参考学习大佬文章:
https://mp.weixin.qq.com/s/FhwML5Jy6X8glJNOeMgV2g