最近的CTF比赛中,感觉PHP序列化与反序列化漏洞中的POP链的身影越越多了,特此,写一篇关于POP链构造的小文,内容不深,更多的是想提供给想要入坑的小伙伴们一点借鉴(厉害的大牛实在太多),所以,本文的自我定位并不高,跟多的谈谈自己的一些理解,如有不恰当的地方,恳请指点,但求共同进步。
我们知道,在PHP中可以定义“类”,“类”里面可以定义很多变量和类方法,当我们实例化一个类时,一些类方法可以手工调用,一些类方法可以自动调用(此时,需要满足某种触发条件),这种拥有自动调用能力的方法被称为魔术方法。序列化一个“类”的过程形象点讲就是将“类”变为字符串的过程,只不过这个字符串比较特殊,保存了类的变量的某些属性特征(类名,变量名,变量值,变量权限,类型,变量大小),但省略了类方法的保存。
简单提一下,序列化一个类的过程:
可以观察到:"类名,变量名,变量值,变量权限,类型,变量大小" 这些都被保存在了字符串中,但唯独没有将方法保存在这个字符串中。
而反序列化就是一个将字符串还原的过程,在本例中,这个特殊的字符串将被最终还原为原本的person 类
:
可以发现,反序列化形成的person类
的基本状态信息与原person类
基本一致(变个样子是,无所谓,描述信息不变就行)
承接上面的例子,可以有一个这样的猜测,如果我们将序列化的字符串按格式要求随意更改,是不是可以控制intro()方法的打印信息?答案是肯定的。事实上,序列化与反序列化漏洞也正是由此而来。(简单提一下)
假设有这样一个环境:
存在url:
127.0.0.1/ser.php
其对应的后台代码如下:
<?php class person { public $name="echo 'I am isee'"; public function __wakeup(){ eval ("$this->name;"); } } if (isset($_GET['mid'])){ $mid=$_GET['mid']; unserialize("$mid"); } else{ echo "<h1>hello!!!<h1/>"; }
这段代码定义了一个person类
,person类
里存在一个敏感函数eval()
可以执行任意代码,当反序列化这个**person类
形成的特殊字符串成功时会自动触发__wakeup()
魔术方法,从而执行eval()
函数(本例中,正常情况下,eval()
触发会将$name的值当PHP代码解析,即输出I am isee
** ),访问该页面,不传递参数时返回一个**hello!!!
信息
如果带上?mid
参数服务器后端对应的PHP代码将会反序列化这个用户传参,此时如果我们在本地正常序列化这个**pesong类
并拼接到?mid
的结果是这样的
本地正常序列化**person类
:
<?php class person { public $name = "echo 'I am isee'"; public function __wakeup() { eval ("$this->name;"); } } $person1=new person(); $mid=serialize($person1); var_dump($mid); //序列化后的字符串 //O:6:"person":1:{s:4:"name";s:16:"echo 'I am isee'";}
url拼接mid
参数后访问服务器:
可以清楚的看到,传递序列化后的字符串成功的被后台的unserialize()
还原成了原本的person类
,从而触发了__wakeup
魔术方法,最终eval()
成功的将$name
的值 echo 'I am isee'
将PHP代码解析,输出了I am isee
字样
如果我们自定义这个序列化后的特殊字符串,会怎样?
可以看到,ping命令被成功的执行,这也说明,其他的任意命令也可以执行了。
因此,简单小结一下PHP序列化与反序列化漏洞的成因:
序列化的字符串可以保存类的基本信息,反序列化的过程可以将这个特殊的字符串重新还原成类,虽然在整个序列化与反序列化的过程中我们无法控制类方法的改变(这个主要指后台的自定义函数),但是我们却可以通过复写变量并借用类中自定义好的方法(服务器上的)或魔术方法(服务器存在的或本地自定义的),并借用敏感函数来达到恶意效果。
关键点就在于PHP序列化与反序列化的过程用户可控。
个人认为,序列化与反序列化漏洞的精髓在与敏感函数利用与类重构。POP链也是序列化与反序列化漏洞利用方式的一种,两者都要利用到PHP中的魔发函数,自我感觉,区别就在于一个”短“点儿,一个”长“儿。
比如说,下面这个例子
<meta charset="utf-8"> <?php //hint is in hint.php error_reporting(1); class Start { public $name='guest'; public $flag='syst3m("cat 127.0.0.1/etc/hint");'; public function __construct(){ echo "I think you need /etc/hint . Before this you need to see the source code"; } public function _sayhello(){ echo $this->name; return 'ok'; } public function __wakeup(){ echo "hi"; $this->_sayhello(); } public function __get($cc){ echo "give you flag : ".$this->flag; return ; } } class Info { private $phonenumber=123123; public $promise='I do'; public function __construct(){ $this->promise='I will not !!!!'; return $this->promise; } public function __toString(){ return $this->file['filename']->ffiillee['ffiilleennaammee']; } } class Room { public $filename='/flag'; public $sth_to_set; public $a=''; public function __get($name){ $function = $this->a; return $function(); } public function Get_hint($file){ $hint=base64_encode(file_get_contents($file)); echo $hint; return ; } public function __invoke(){ $content = $this->Get_hint($this->filename); echo $content; } } if(isset($_GET['hello'])){ unserialize($_GET['hello']); }else{ $hi = new Start(); } ?>
我们直接看这里
敏感函数file_get_contents
已经有了,可以读取服务器上的任意文件(权限允许的话),echo()
将读取结果输出到用户界面,但是,虽然序列化的过程我们是可以控制的,但确实没有发现一下就能将Room类
利用起来的地方(Room类不像前面的Person类存在直接借用魔术方法调用eval()这样的利用点。
两个类的对比:
用户可控序列化的过程:
此时就需要借用其他类,一步一步调,直到最后调到Room类
。
这个过程需要大量用到PHP中的魔术方法,因此,在开始我们的pop链的过程之前,需要先讨论一下本例中要用到的魔术方法,其他的,如果读者有兴趣,可以自行了解,此处不再作过多赘述。
我们知道,再PHP中,要使用一个类的方法,首先要实例化这个类,之后再通过这个实例化的类对象去调用类方法(这也是面向对象语言的最大特别之处)
<?php //一个类,类里面定义了两个变量,和一个intro()方法用作输出两变量信息 class person { public $name="isee"; public $age="女"; public function intro(){ echo "打印结果如下:\n"; echo '$name:'."$this->name\n"; echo '$age:'."$this->age\n"; } } //实例化person类,并调用intro()方法 $person1=new person(); $person1->intro();
但有些类方法不需要我们手动调用,而是当一些事件或条件触发时自动调用。这类方法,我们称之为魔术方法。
再者,提一点,魔术方法里面的内容是可以自定义的。
本次讨论如下几个魔术方法:
__construct()
__destruct()
__sleep()
__wakeup()
__invoke()
__toString()
__get()
__construct()
当实例化一个类时,自动触发。
<?php class person { public $name="isee"; public $age="女"; public function __construct() { $this->name="eesi"; $this->age="女"; } public function intro(){ echo "打印结果如下:\n"; echo '$name:'."$this->name\n"; echo '$age:'."$this->age\n"; } } $person1=new person(); $person1->intro();
__destruct()
销毁一个类时自动触发。
<?php class person { public $name="isee"; public $age="女"; public function __construct() { echo '1、__construct()方法已执行!'."\n"; echo '$name,$sex已重置为新值.'."\n"; $this->name="eesi"; $this->age="女"; echo "\n"; } public function intro(){ echo '2、__intro方法已执行'."\n"; echo "打印结果如下:\n"; echo '$name:'."$this->name\n"; echo '$age:'."$this->age\n"; echo "\n"; } public function __destruct() { $this->name="isee"; $this->age="女"; echo '3、__destruct()方法已执行!'."\n"; echo '$name,$sex已重置为默认值.'."\n"; echo '$name:'."$this->name\n"; echo '$age:'."$this->age\n"; echo "\n"; } } $person1=new person(); $person1->intro();
__sleep()
执行serialize()
前如果存在__sleep()方法,触发
不序列化时:
<?php class person { public $name="isee"; public $age="女"; public function intro(){ echo '1、__intro方法已执行'."\n"; echo "打印结果如下:\n"; echo '$name:'."$this->name\n"; echo '$age:'."$this->age\n"; echo "\n"; } public function __sleep() { echo '2、__sleep()方法已执行!'."\n"; echo '$name,$sex已重置为新值.'."\n"; $this->name="eesi"; $this->age="女"; echo '$name:'."$this->name\n"; echo '$age:'."$this->age\n"; echo "\n"; return array('name','age'); } } $person1=new person(); $person1->intro();
序列化时:
<?php class person { public $name="isee"; public $age="女"; public function intro(){ echo '1、__intro方法已执行'."\n"; echo "打印结果如下:\n"; echo '$name:'."$this->name\n"; echo '$age:'."$this->age\n"; echo "\n"; } public function __sleep() { echo '2、__sleep()方法已执行!'."\n"; echo '$name,$sex已重置为新值.'."\n"; $this->name="eesi"; $this->age="女"; echo '$name:'."$this->name\n"; echo '$age:'."$this->age\n"; echo "\n"; return array('name','age'); } } $person1=new person(); $person1->intro(); serialize($person1);
__wakeup()
执行unserialize()
前,如果存在__wakeup()方法,触发,与__sleep()
相对
<?php class person { public $name="isee"; public $age="女"; public function intro(){ echo '1、__intro方法已执行'."\n"; echo "打印结果如下:\n"; echo '$name:'."$this->name\n"; echo '$age:'."$this->age\n"; echo "\n"; } public function __sleep() { echo '2、__sleep()方法已执行!'."\n"; echo '$name,$sex已重置为新值.'."\n"; $this->name="eesi"; $this->age="女"; echo '$name:'."$this->name\n"; echo '$age:'."$this->age\n"; echo "\n"; } public function __wakeup() { $this->name="isee"; $this->age="女"; echo '3、__wakeup()方法已执行!'."\n"; echo '$name,$sex已重置为默认值.'."\n"; echo '$name:'."$this->name\n"; echo '$age:'."$this->age\n"; echo "\n"; } } $person1=new person(); $person1->intro(); $mid=serialize($person1); unserialize($mid);
__invoke()
类被用作以函数的方式调用时,触发
__toString()
类被用作字符串输出时,触发
<?php class person { public $name="isee"; public $age="女"; public function intro(){ echo '__intro方法已执行'."\n"; echo "打印结果如下:\n"; echo '$name:'."$this->name\n"; echo '$age:'."$this->age\n"; echo "\n"; } public function __invoke() { echo '__invoke()方法已执行!'."\n"; echo '$name,$sex已重置为新值.'."\n"; $this->name="eesi"; $this->age="女"; echo '$name:'."$this->name\n"; echo '$age:'."$this->age\n"; echo "\n"; } public function __toString() { echo '__toString()方法已执行!'."\n"; echo '$name,$sex已重置为新值.'."\n"; $this->name="EESI"; $this->age="男"; echo '$name:'."$this->name\n"; echo '$age:'."$this->age\n"; return 'success!!!'."\n\n"; } } //常规类方法调用 $person1=new person(); $person1->intro(); //__invoke()触发 $person1(); //__toString触发 echo $person1;
__get()
访问私有类中的变量时,触发
<?php class alien{ private $Aname="TOTO"; private $Aage="1000"; public function __get($name) { echo '__get()方法已执行!'."\n"; echo '$name:'."$this->Aname\n"; echo '$age:'."$this->Aage\n"; echo "\n"; } } //__get()方法触发 $alien=new alien(); echo $alien->Aname;
以刚刚上面提到的长代码为例,接着,开始我们的POP链构造过程
首先,简单的将各个部分拆分,了解一下其大致的功能(这里,添加了注释)
class Start
class Start { //定义了两个已经初始化了值的变量,且其权限修饰符为"public" public $name='guest'; public $flag='syst3m("cat 127.0.0.1/etc/hint");'; //__construct()用echo()函数输出一句话 public function __construct(){ echo "I think you need /etc/hint . Before this you need to see the source code"; } //_sayhello用echo()函数输出$name变量的内容 public function _sayhello(){ echo $this->name; return 'ok'; } //__wakeup()用来调用_sayhello() public function __wakeup(){ echo "hi"; $this->_sayhello(); } //__get用echo()函数输出$flag的内容 public function __get($cc){ echo "give you flag : ".$this->flag; return ; } }
class Info
class Info { //定义了两个已经初始化值的变量,其权限修饰符分别为“private”和“public” private $phonenumber=123123; public $promise='I do'; //调用__construct()函数会返回一个字符串'I do',即$promise的值 public function __construct(){ $this->promise='I will not !!!!'; return $this->promise; } //__tostring()返回一个对象内部的变量,且该对象被存储在数组file['filename']里 public function __toString(){ return $this->file['filename']->ffiillee['ffiilleennaammee']; } }
class Room
class Room { //定义了三个变量,一个已经初始化值,两个未初始化值;且均为“public”修饰 public $filename='/flag'; public $sth_to_set; public $a=''; //__get()返回一个函数 public function __get($name){ $function = $this->a; return $function(); } //Get_hit()用echo()输出$hit的内容,且$hit的值为用file_get_contents()读取的文本内容 public function Get_hint($file){ $hint=base64_encode(file_get_contents($file)); echo $hint; return ; } //__invoke()会调用Get_hit()函数,且传递的参数为$filename public function __invoke(){ $content = $this->Get_hint($this->filename); echo $content; } }
入口
//如果传递$hello,则反序列化这个变量,如果未传递,则实例化一个类,即,class Start if(isset($_GET['hello'])){ unserialize($_GET['hello']); }else{ $hi = new Start(); } //和class Start
更多时候,会存在非常多的无法利用的类,因此,并不建议通读每条代码,更多情况下是POP链的起始点与末端敏感函数反推。
分析如下:
直接访问,后台对应的url返回如下结果:
客户端由于没有传递参数,服务器端实例化了一个Start 类
,而实例化Start类
触发__construct
魔术方法,该方法输出了一句话,至此,结束。
接着分析,很明显,
class Room
是调用链的末端file_get_contents()
可以读取服务器上的文件并通echo()
回显到服务器;__invoke()
魔术方法可以实现调用Get_hint()
,触发条件是实例化的 class Room
被当作函数调用,且最终会给file_get_contents($file)
,传递一个$filename='/flag'
参数;__get()
魔术方法可以实现"实例化的类以函数方式调用"这个条件,只要我们给$a重新赋值一个实例化的
Room对象**,其触发条件是访问实例化
Room`的私有变量目前来看,只需要解决如何调用__get()
即可
接着分析,要用到class info
实现对class Room
里__get()
的调用
__toString()
魔术方法可以实现实例化的类对象调用自身的变量,触发条件是实例化的class info被当作字符串输出,file['filename']
未定义,但存在__construct()
魔术方法,我们利用construct()
将file['filename']
赋值一个实例化后的Room对象
,这样以来,未定义的$ffiillee['ffiilleennaammee']
会被当作class Room
里的私有变量来访问,从而触发class Room
里的__get()
方法__construct()
魔术方法可以实现对$promise
的赋值和构造$ffiillee['ffiilleennaammee']
,其触发条件是class Info
被实例化,我们可以利用__construct()
创造一个$ffiillee['ffiilleennaammee']
,并初始一个class Room`实例化对象目前来看,只要实现实例化class Info
就行
接着分析
最终的payload如下:
这里,放一个比赛时,执行成功的结果:
最后base64解一下密就行
感觉,对于POP的链构造,魔术函数要玩熟练,且这种漏洞必须通过白盒审计发现,可能在实际中更多的是通过审计流行的框架去挖掘POP链(java里也有),然后,如果一些网站使用了这些开源的框架或软件且二次开发是没有修补这些漏洞,就有可能被攻击,并且,现在也存在很多自动化的工具可以直接利用序列化与反序列化漏洞。