偶然挖到的

payload

当get请求 http://php.local/public/index.php/weixin/message/sendall_lists

会被程序跳转到 http://php.local/public/index.php/home/user/login/from/6/pbid/0 登录页面

20191210205508

但是随便提交post请求就会返回页面

20191210205611

再来一个

20191210205611

随意提交POST数据导致未授权访问,分析一下。

分析

http://php.local/public/index.php/weixin/message/sendall_lists 为例子

 1<?php
 2class Message extends WebBase
 3{
 4
 5    public function initialize()
 6    {
 7        parent::initialize();
 8        $param['mdm'] = I('mdm');
 9        $act = strtolower(ACTION_NAME);
10
11        $res['title'] = '高级群发';
12        $res['url'] = U('add', $param);
13        $res['class'] = $act == 'add' ? 'current' : '';
14        $nav[] = $res;
15
16        $res['title'] = '客服群发';
17        $res['url'] = U('custom_sendall', $param);
18        $res['class'] = $act == 'custom_sendall' ? 'current' : '';
19        $nav[] = $res;
20
21        $res['title'] = '消息管理';
22        $res['url'] = U('sendall_lists', $param);
23        $res['class'] = $act == 'sendall_lists' ? 'current' : '';
24        $nav[] = $res;
25        $this->assign('nav', $nav);
26    }
27    // 群发消息管理
28    public function sendall_lists()
29    {
30        $this->listNav();
31        $map = $this->dayMap();
32
33        $map['pbid'] = get_pbid();
34        $list = M('message')->where(wp_where($map))
35            ->order('id desc')
36            ->paginate();
37        $list = dealPage($list);
38
39        foreach ($list['list_data'] as &$v) {
40            $v = $this->makeContent($v);
41        }
42        $url = U('sendall_lists', $this->get_param);
43        $this->assign('searchUrl', $url);
44        $this->assign($list);
45        // $this->assign('normal_tips', '当用户发消息给认证公众号时,管理员可以在48小时内给用户回复信息');
46
47        return $this->fetch();
48    }
49}

可以看到在initialize()调用了父类parent::initialize(),继承的父类是WebBase,跟进

 1<?php
 2public function initialize()
 3{
 4    if (strtolower(MODULE_NAME) == 'install') {
 5        return false;
 6    }
 7    parent::initialize();
 8
 9    $not_need_wpid = [
10        'public_bind',
11        'home',
12        'admin'
13    ];
14
15    if (!in_array(MODULE_NAME, $not_need_wpid) && strtolower(CONTROLLER_NAME) != 'publics' && strtolower(CONTROLLER_NAME) != 'adminmaterial' && strtolower(MODULE_NAME) != 'scene' && strtolower(MODULE_NAME . '/' . CONTROLLER_NAME) != 'weixin/notice' && (!defined('WPID') || WPID <= 0)) {
16        $this->error('先增加公众号', U('weixin/publics/lists'));
17    }
18
19    $index_3 = strtolower(MODULE_NAME . '/' . CONTROLLER_NAME . '/' . ACTION_NAME);
20    if ($index_3 == 'weixin/index/index') {
21        return false;
22    }
23
24    // 微信客户端请求的用户初始化在weixin/index/index里实现,这里不作处理
25    $this->initUser();
26
27    $this->initWeb();
28
29    $this->_nav();
30}

在父类的initialize中首先判断是否安装了程序,然后判断是否添加了一个微信公众号,然后执行三个自身的方法

 1<?php
 2private function initUser()
 3{
 4    $uid = intval(session('mid_' . get_pbid()));
 5    $loginUid = is_login();
 6    if (empty($uid) && $loginUid > 0) {
 7        $uid = $loginUid;
 8        session('mid_' . get_pbid(), $loginUid);
 9    }
10
11    // 当前登录者
12    $GLOBALS['mid'] = $this->mid = $uid;
13    $myinfo = get_userinfo($this->mid);
14    $GLOBALS['myinfo'] = $myinfo;
15
16    // 当前访问对象的uid
17    $cuid = input('uid');
18    $GLOBALS['uid'] = $this->uid = $cuid > 0 ? $cuid : $this->mid;
19
20    $this->assign('mid', $this->mid); // 登录者
21    $this->assign('uid', $this->uid); // 访问对象
22    $this->assign('myinfo', $GLOBALS['myinfo']); // 访问对象
23}

initUser()中判断了当前登录的用户,并没有判断路由权限,继续看initWeb()

 1<?php
 2private function initWeb()
 3{
 4    if (ACTION_NAME == 'logout') {
 5        return false;
 6    }
 7    ...省略...
 8
 9    $model_name = parse_name(MODULE_NAME);
10    $controller_name = parse_name(CONTROLLER_NAME);
11    $action_name = parse_name(ACTION_NAME);
12    $index_1 = $model_name . '/*/*';
13    $index_2 = $model_name . '/' . $controller_name . '/*';
14    $index_3 = $model_name . '/' . $controller_name . '/' . $action_name;
15
16    // 当前用户信息
17    $access = array_map('trim', explode("\n", config('ACCESS')));
18    $access = array_map('strtolower', $access);
19    $access = array_flip($access);
20
21    $guest_login = isset($access[$index_1]) || isset($access[$index_2]) || isset($access[$index_3]) || $index_1 == 'admin/*/*' || $index_3 == 'home/application/execute' || $index_2 == 'home/user/*' || $index_2 == 'home/product/*' || $index_2 == 'home/scan/*' || $index_2 == 'weixin/notice/*';
22
23    if (IS_GET && !is_login() && !$guest_login) {
24        $forward = cookie('__forward__');
25        empty($forward) && cookie('__forward__', $_SERVER['REQUEST_URI']);
26
27        return $this->redirect(U('home/user/login', array('from' => 6)));
28    }
29
30    /* 管理中心的导航 */
31    if (IS_GET) {
32        $menus = D('common/Menu')->getMenu();
33        $this->assign('top_menu', $menus);
34        $this->assign('now_top_menu_name', $menus['now_top_menu_name']);
35    }
36
37    ...省略
38}

当满足IS_GET && !is_login() && !$guest_login条件时,会return $this->redirect(U('home/user/login', array('from' => 6)))返回到登录页面,我们把条件拆开看

  • IS_GET 是否是get请求
  • !is_login()判断是否登录
  • !$guest_login => isset($access[$index_1]) || isset($access[$index_2]) || isset($access[$index_3]) || $index_1 == 'admin/*/*' || $index_3 == 'home/application/execute' || $index_2 == 'home/user/*' || $index_2 == 'home/product/*' || $index_2 == 'home/scan/*' || $index_2 == 'weixin/notice/*'

这是is_login()的定义,从session中取,0-未登录,大于0-当前登录用户ID。未登录时!is_login()为真

 1<?php
 2/**
 3 * 检测用户是否登录
 4 *
 5 * @return integer 0-未登录,大于0-当前登录用户ID
 6 */
 7function is_login()
 8{
 9    $user = session('user_auth');
10    if (empty($user)) {
11        $cookie_uid = cookie('user_id');
12        if (!empty($cookie_uid)) {
13            $uid = think_decrypt($cookie_uid);
14            $userinfo = getUserInfo($uid);
15            D('common/User')->autoLogin($userinfo);
16
17            $user = session('user_auth');
18        }
19    }
20    if (empty($user)) {
21        return 0;
22    } else {
23        return session('user_auth_sign') == data_auth_sign($user) ? $user['uid'] : 0;
24    }
25}

20191210211940

那么此时跳不跳转就取决于!$guest_loginIS_GET

!$guest_login取决于$index_1 $index_2 $index_3 打断点看下他们是什么

20191210211210

可以看到他们三个分别对应模块/控制器/操作,根据访问的路由来决定是否登录。

到这里实际上就明了了,如果我们提交POST请求,在未登录的情况下IS_GET && !is_login() && !$guest_login始终是false,那么就不会跳转到登录页面,如我们的payload所示。


此时我们第一时间想到的是通过未授权来访问管理页面获取更大权限,很遗憾的是并不行。我们继续分析下。后台页面在admin模块下,除了Publics控制器和Admin控制器继承了WebBase类,其他继承的都是Admin控制器

20191210212901

而在Admin控制器中

 1<?php
 2/**
 3 * 后台控制器初始化
 4 */
 5public function initialize()
 6{
 7    parent::initialize();
 8
 9    $this->assign('meta_title', '');
10
11    // 获取当前用户ID
12    if (defined('UID')) {
13        return;
14    }
15
16    define('UID', is_login());
17    if (! UID) {
18        // 还没登录 跳转到登录页面
19        $this->redirect('Publics/login');
20    }
21    if (config('user_administrator') != UID) {
22        $this->redirect('Publics/logout');
23    }
24
25    // 是否是超级管理员
26    define('IS_ROOT', is_administrator());
27    if (! IS_ROOT && config('ADMIN_ALLOW_IP')) {
28        // 检查IP地址访问
29        if (! in_array(get_client_ip(), explode(',', config('ADMIN_ALLOW_IP')))) {
30            $this->error('403:禁止访问');
31        }
32    }
33    // 检测系统权限
34    if (! IS_ROOT) {
35        $access = $this->accessControl();
36        if (false === $access) {
37            $this->error('403:禁止访问');
38        } elseif (null === $access) {
39            // 检测访问权限
40            $rule = strtolower(MODULE_NAME . '/' . CONTROLLER_NAME . '/' . ACTION_NAME);
41
42            // 检测分类及内容有关的各项动态权限
43            $dynamic = $this->checkDynamic();
44            if (false === $dynamic) {
45                $this->error('未授权访问!');
46            }
47        }
48    }
49}

通过UID严格校验了管理员的权限问题,导致admin模块下的统统不能未授权。

影响范围

2019/12/10 weiphp5.0官网最新版

未授权页面

所有继承webbase类的页面,几乎所有模块通杀。

20191210213504

文笔垃圾,措辞轻浮,内容浅显,操作生疏。不足之处欢迎大师傅们指点和纠正,感激不尽。