EBCMSv1.1.1 后台Getshell

背景

EBCMS(易贝管理系统)是一款基于PHP+MYSQL开发的网站管理系统,系统核心极致精简,所有功能都是通过应用扩展来实现。

漏洞简述

攻击链很简单

  1. 以Administator身份登录后台后
  2. 在系统设置的会员中心的有三处修改提示信息的表单(本文只验证注册成功表单),通过在其中插入{php}phpinfo();{/php}或者<?php phpinfo();?>
  3. 前台话题插件注册用户,即可获得注册成功的系统消息
  4. Getshell

代码分析

首先查看修改配置的控制器,index方法提交修改

<?php 
namespace app\ebcms\controller;
class Config extends Common {

    function index(){
        if (\Ebcms::isPost()) {
            $configs = \mylib\Input::post('configs');
            if ($configs && is_array($configs)) {
                foreach ($configs as $id => $value) {
                    \Ebcms::db() -> update('config', [
                        'value'=>$value
                    ], [
                        'id'=>$id
                    ]);
                }
            }
            \mylib\Cache::delete('configs');
            return $this -> success('提交成功!', \mylib\Route::buildUrl('/ebcms/config/index'));
        }else{
            $tmp = \Ebcms::db() -> select('config', '*', [
                'ORDER' => [
                    'weight' => 'ASC',
                    'id' => 'ASC',
                ]
            ]);
            $configs = [];
            foreach ($tmp as $key => $value) {
                if (!isset($configs[$value['group']])) {
                    $configs[$value['group']] = [];
                }
                $configs[$value['group']][] = $value;
            }
            $this -> assign('configs', $configs);
            return $this -> html($this -> fetch());
        }
    }
}

跟进\mylib\Input::post()后,再跟进\mylib\Input::filter_var,发现对参数进行了htmlspecialchars过滤

<?php
...
public static function post($field, $default='', $filter=''){
        if (self::has('post.'.$field)) {
            return self::filter_var($_POST[$field], $filter);
        }else{
            return $default;
        }
    }
...
private static function filter_var($value, $filters=''){
        if (!is_null($filters)) {
            if (!$filters) {
                $filters = \mylib\Config::get('sys.default_filters', ['htmlspecialchars']);
            }
            if (is_array($value)) {
                foreach ($value as $key => $v) {
                    $value[$key] = self::filter_var($v, $filters);
                }
            }else{
                foreach ($filters as $filter) {
                    if (is_callable($filter)) {
                        $value = call_user_func($filter, $value);
                    }
                }
            }
        }
        return $value;  
    }

那么,最后出现在数据库中的一定是转义后的<>

接着看/vendor/hetangyuese/xlphp/mylib/View.php,这里存在可以将特定标签包围的php代码解析为原生php代码的功能。

public function parseTag($html){
        $tags = [
            '/{(foreach|if|for|switch)\s+(.*)}/U'=>function($matchs){
                return '<?php '.$matchs[1].'('.$matchs[2].'){ ?>';
            },
            '/{function\s+(.*)}/U'=>function($matchs){
                return '<?php function '.$matchs[1].'{ ?>';
            },
            '/{case\s+(.*)}/U'=>function($matchs){
                return '<?php case '.$matchs[1].': ?>';
            },
            '/{default\s*}/U'=>function($matchs){
                return '<?php default: ?>';
            },
            '/{php}/'=>function($matchs){
                return '<?php ';
            },
            '/{\/php}/'=>function($matchs){
                return ' ?>';
            },
            '/{\/(foreach|if|for|function|switch)}/'=>function($matchs){
                return '<?php } ?>';
            },
            '/{\/(case|default)}/'=>function($matchs){
                return '<?php break; ?>';
            },
            '/{(elseif)\s+(.*)}/U'=>function($matchs){
                return '<?php }'.$matchs[1].'('.$matchs[2].'){ ?>';
            },
            '/{else\/?}/'=>function($matchs){
                return '<?php }else{ ?>';
            },
            '/{include\s*([\w\.,\/]*)}/U'=>function($matchs){
                $html = '';
                $tpls = explode(',', $matchs[1]);
                foreach ($tpls as $tpl) {
                    $html .= file_get_contents($this -> getTpl($tpl));
                }
                return $this -> parseTag($this -> buildLiteral($html));
            },
            '/{(\$[\w\[\'"\]]*)\.([\w\.]*)([^{}]*)}/'=>function($matchs){
                return '{'.$matchs[1].'[\''.implode('\'][\'', explode('.', $matchs[2])).'\']'.$matchs[3].'}';
            },
            '/{(\$[\w\[\'"\]\-$]*)\?(.*):(.*)}/U'=>function($matchs){
                return '{:isset('.$matchs[1].') ? '.($matchs[2]?:$matchs[1]).' : '.$matchs[3].'}';
            },
            '/{(\$[\w\[\'"\]\-$]*)}/U'=>function($matchs){
                return '<?php echo '.$matchs[1].'; ?>';
            },
            '/{:(.*)\s*;?\s*}/U'=>function($matchs){
                return '<?php echo '.$matchs[1].'; ?>';
            },
            '/\?>[\s\n]*<\?php/U'=>function($matchs){
                return '';
            },
        ];
        foreach ($tags as $preg => $callback) {
            $html = preg_replace_callback($preg, $callback, $html);
        }
        return $html;
    }

那么不断向上回溯,可以发现
renderTpl函数,这个函数可以将传入的tpl模版渲染成html代码,那么接下来就找调用的控制器就可以了

public static function renderTpl($tpl, $data=[]){
        $view = new \mylib\View(\mylib\Config::get('view'));
        return $view -> render($tpl, $data);
    }

最后分别定位到三处方法
在/app/member/controller/Auth.php

<?php
...
if ($tpl = \Ebcms::config('tpl_reg_success')) {
                \Member::notice($member['id'], \Ebcms::renderTpl(htmlspecialchars_decode($tpl), [
                    'member'=>$member
                ]) ,'欢迎注册!');
            }
...
if ($tpl = \Ebcms::config('tpl_password_reset')) {
                \Member::notice($member['id'], \Ebcms::renderTpl(htmlspecialchars_decode($tpl),[
                    'member'=>$member
                ]) ,'密码重置提醒');
            }

和/app/member/controller/Index.php中

if ($tpl = \Ebcms::config('tpl_phone_verify')) {
                \Member::notice(\mylib\Session::get('member_id'), \Ebcms::renderTpl(htmlspecialchars_decode($tpl), [
                    'member'=>$member
                ]) ,'电话号码绑定成功!');
            }

发现这三处,都对从数据库中取出来的数据进行了htmlspecialchars_decode解码

那么原来实体编码导致的问题就迎刃而解了,因此也可以直接注入<?php phpinfo();?>

漏洞复现


修复方案

对后台代码进行必要的过滤

此处评论已关闭