0x00 前言php
今天在Seebug的公衆號看到了Typecho的一個前臺getshell分析的文章,而後本身也想來學習一下。保持對行內的關注,瞭解最新的漏洞很重要。git
0x01 什麼是反序列化漏洞github
如題所說,這是一個反序列化致使的代碼執行。看過我以前文章的人應該不會陌生。PHP在反序列化一個字符串時,至關於激活了以前休眠的一個對象。當在激活的時候,會調用幾個魔法方法來進行初始化以及相關操做。而若是魔法方法中存在危險操做,好比數據庫操做,文件讀寫操做,系統命令調用等,那麼就有可能會形成相應的漏洞。用戶經過可控變量調整系統運行流程。進入到危險函數,從而致使漏洞。sql
0x02 分析shell
官方在前兩天已經作了修復。如今已經不存在這個漏洞了。咱們找到之前的版本進行安裝。連接以下:
數據庫
https://github.com/typecho/typecho/releases/tag/v1.1-15.5.12-beta
在根目錄的install.php中咱們發現:編程
咱們定位到了漏洞現場。而後問題來了,反序列化是將一個休眠對象(字符串)激活爲一個對象。那麼咱們接下來找到在激活的過程當中,有哪些方法能夠調用,它們纔是利用的關鍵。咱們來搜索這三個方法:數組
__toString() __destruct() __Wakeup()
我沒有找到wakeup方法,而後destruct方法也不能使用。而後搜索__toString()的時候發現有三處,分別以下:app
var\Typecho\Config.php public function __toString() { return serialize($this->_currentConfig); } var\Typecho\Db\Query.php public function __toString() { 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; } } \var\Typecho\Feed.php public function __toString() { 。。。後邊再貼代碼 }
可是必定能用嗎?函數式編程
咱們能夠看到隨後的$config進入了Typecho_Db類的構造方法,咱們來跟進:
在第120行的時候,發現傳入的參數用來作字符串拼接。這樣就會調用__toString()方法了.
而後咱們對以前搜索到的方法跟進,發現前兩個是無法用的。而後咱們重點關注最後一個。這個函數主要的目的是拼接xml文件。而後在整個過程當中,並無直接進入危險函數,因此此時陷入僵局。可是就這樣就無解了嗎?
0x03 __get()方法的逆襲
__get()方法在調用一個對象不存在的成員變量時候將會調用此方法。能夠參考我以前的文章。
那麼這樣的話咱們只要可以找到一個get方法中存在危險操做,這樣的話,這個漏洞仍是可能的。咱們來試試看。
全局來搜索__get()方法,而後在\var\Typecho\Request.php
public function __get($key) { return $this->get($key); }
咱們跟進:
public function get($key, $default = NULL) { switch (true) { case isset($this->_params[$key]): $value = $this->_params[$key]; break; case isset(self::$_httpParams[$key]): $value = self::$_httpParams[$key]; break; default: $value = $default; break; } $value = !is_array($value) && strlen($value) > 0 ? $value : $default; return $this->_applyFilter($value); }
在方法的最後調用了_applyFilter()方法,繼續跟進:
private function _applyFilter($value) { if ($this->_filter) { foreach ($this->_filter as $filter) { $value = is_array($value) ? array_map($filter, $value) : call_user_func($filter, $value); //漏洞現場 } $this->_filter = array(); } return $value; }
咱們來梳理一下整個邏輯,install.php (反序列化) ==> Db.php (將$config中的參數拿來拼接,調用toString方法) ==> Feed.php (調用一個不存在成員變量,將會調用__get()方法) ==> Request.php (調用get()方法,而後調用_applyFilter(), 而後調用call_user_func()致使了代碼執行)
0x03 利用方法
完整的利用過程,咱們能夠看
p0的文章 http://p0sec.net/index.php/archives/114/ 或者 Seebug的公衆號 http://mp.weixin.qq.com/s?__biz=MzAxNDY2MTQ2OQ==&mid=2650942666&idx=1&sn=5c84d6d69463a0a430e01dfa68c2d3ab&chksm=80796ef8b70ee7ee8ba5d88feb8d794bee19a55b6e17dff45fcee6ba6ee726fb4e2e029d50bd&scene=0#rd
0x04 思考
這個實際上是當時看到122行的時候:
我想這裏直接調用了call_user_func()函數,$adapterName我是可控的,咱們不直接能夠命令執行嗎?還要長長的執行鏈幹什麼啊?
而後我查了call_user_func()的使用方法:
若是傳入的是數組,那麼數組中的第一個值是一個類名,第二個是類中的一個方法。可是怎麼樣第一個值才能是類呢?這個時候我想到了匿名類。在函數式編程中常常會用到匿名函數,匿名類。因而我去查了一下發如今PHP7中加入了匿名類的定義,因此以下代碼是能夠的:
call_user_func(array( new class(){ public function isAvaliable() { echo "Hello, World"; } }, 'isAvaliable' ) ); //執行結果 Hello, World
這裏不能用的緣由在於被以前的$adapterName = 'Typecho_Db_Adapter_' . $adapterName;給打斷掉了。若是你用類和字符串作拼接。很明顯是不行的。