良精OA办公系统admin+任意密码登录的漏洞复现和代码分析

释放双眼,带上耳机,听听看~!

漏洞分析

文件位置:CMSoa.php
代码内容:

良精OA办公系统admin+任意密码登录的漏洞复现和代码分析-路人娱乐网

代码逻辑:该php文件为第一次访问OA子功能模块是的登陆认证页面,默认传递参数c(Public)、a(login),从下面的代码中可以看到此处会先获取参数c和a的值,之后判断参数是否为空,如果为空则赋予相对应的值,如果不为空则值不变,之后判断login是否在参数c中,如果在则导入配置文件,如果不再则继续下面的逻辑,在第一登陆时默认c的值为Public,不会去加载配置信息,在之后的if语句中定义了三个文件路径,其中$control_path为'source/control/oa/login.php',之后回去判断当前的control_path是否存在,如果不存在则提示处理文件未发现,如果存在则将上面的三个文件包含进来,这里简单说明一下下面三个文件的功能:

    • source/control/oabase.php:登陆认证与授权检查、查询系统配置信息、报存登陆日志、显示菜单界面等
    • source/control/apphook.php:加载广告、类型、标签等特征信息。
    • source/control/oa/login.php:登陆检测、登出、获取登陆ip地址。

之后再去new一个control类对象,然后检测action_login方法是否存在,如果存在且a参数值(login)的第一个字符不为'下划线',之后调用该方法,跟踪逻辑,进入到action_login,在这里会去直接调用$this->cptpl.'login.tpl'

文件位置:CMSsourcecontroloaPublic.php
代码内容:

良精OA办公系统admin+任意密码登录的漏洞复现和代码分析-路人娱乐网

代码逻辑:上面的'$this->cptpl'变量的定义位于文件oabase.php的32行代码:

良精OA办公系统admin+任意密码登录的漏洞复现和代码分析-路人娱乐网

之后,继续跟踪代码到tpl/oa/login.tpl下,具体代码如下(有点多,我就直接贴下面了):

    <!--{include file="<!--{$oapath}-->public/ins_base.tpl"}-->
    <block name="content">
        <div class="Public container">
            <!-- /container -->
            <div class="row">
                <div class="col-xs-12 hidden-xs" style="margin-top:120px;"></div>
            </div>
            <div class="row">
                <div class="col-sm-8 hidden-xs">
                    <div class="img"></div>
                </div>
                <div class="col-sm-4 well">
                    <div style="margin-bottom:44px;margin-top:20px;">
                        <h1 class="text-center"><!--{$config.system_name}--></h1>
                    </div>
                    <form method="post" id="form_login" class="form-horizontal">
                        <div class="form-group">
                            <label class="col-sm-3  control-label" for="emp_no">帐号:</label>
                            <div class="col-sm-9">
                                <input class="form-control" id="emp_no" name="emp_no" />
                            </div>
                        </div>
                        <div class="form-group">
                            <label class="col-sm-3  control-label" for="password">密码:</label>
                            <div class="col-sm-9">
                                <input class="form-control" id="password" type="password" name="password" />
                            </div>
                        </div>
                                            <!--{if $config.login_verify_code =='1'}-->
                                            <div class="form-group">
                                <label class="col-sm-3  control-label" for="verify">验证码:</label>
                                <div class="col-sm-9 row">
                                    <div class="col-xs-6">
                                        <input class="form-control" id="verify" name="verify" />
                                    </div>
                                    <div class="col-xs-6">
                                        <img src="<!--{$urlpath}-->source/include/imagecode.php?act=verifycode" style='cursor:pointer' title='刷新验证码' id='verifyImg' onclick='freshVerify()'>
                                    </div>
                                </div>
                            </div>
                                            <!--{/if}-->
     
                        <div class="form-group hidden">
                            <span class="col-sm-3  control-label"> </span>
                            <div class="col-sm-9">
                                <label class="inline pull-left col-3">
                                    <input class="ace" type="checkbox" name="remember" value="1" />
                                    <span class="lbl"> </span> </label>
                                <label for="remember-me">记住我的登录状态</label>
                            </div>
                        </div>
                        <div class="form-group">
                            <div class="col-sm-offset-3 col-sm-9">
                                <input type="button" value="登录" onclick="login();" class="btn btn-sm btn-primary col-10">
                            </div>
                        </div>
                    </form>
     
                </div>
            </div>
     
     
            <div class="row text-right">
     
            </div> -->
        </div>
    </block>
    <block name="js">
        <script type="text/javascript">
            function login() {
                sendForm("form_login", "oa.php?c=Public&a=check_login");
            }
     
        </script>
    </block>

在这里提供了一个登陆认证的表单,之后当表单提交后会进入到最下面的处理逻辑中,重新赋予c和a的值,之后提交到oa.php中,其中c和a的值如下:

  • c:Public
  • a:check_login

之后我们再次回到oa.php文件中,代码如下:

良精OA办公系统admin+任意密码登录的漏洞复现和代码分析-路人娱乐网

此时,和之前分析的逻辑一致,唯一不同的是最后会调用control处理类中的action_check_login函数,因为此时的a已经成为了check_login,那么我们再跟踪到Public.php中的control类中的action_check_login函数中,具体代码如下(由于较多,直接贴进来了,可能有点不是那么好看,读者可以自我贴会sublime Text中查看):

    <?php
    class control extends oabase
    {
        public function action_login()
        {
            //    $is_verify_code = $this->get_system_config("login_verify_code");
            TPL::display($this->cptpl . 'login.tpl');
        }
         public function action_check_login()
        {
            $is_verify_code = $this->get_system_config();
            if (!empty($is_verify_code['login_verify_code'])) {
                parent::loadUtil('session');
                if ($_POST['verify'] != XSession::get('verifycode')) {
                    XHandle::halt('对不起,验证码不正确!', '', 1);
                }
            }
     
            if (empty($_POST['emp_no'])) {
                XHandle::halt('对不起,帐号必须!', '', 1);
                $this->error('!');
            } elseif (empty($_POST['password'])) {
                XHandle::halt('密码必须!', '', 1);
            }
     
            if ($_POST['emp_no'] == 'admin') {
                $_SESSION['ADMIN_AUTH_KEY'] = true;
            }
     
            // if(C("LDAP_LOGIN")&&!$is_admin){
            if (false) {
                $where['emp_no'] = array('eq', $_POST['emp_no']);
                $dept_name       = D('UserView')->where($where)->getField('dept_name');
     
                if (empty($dept_name)) {
                    XHandle::halt('帐号或密码错误!', '', 1);
                }
     
                $ldap_host = C("LDAP_SERVER");//LDAP 服务器地址
                $ldap_port = C("LDAP_PORT");//LDAP 服务器端口号
                $ldap_user = "CN=" . $_POST['emp_no'] . ",OU={$dept_name}," . C('LDAP_BASE_DN');
                $ldap_pwd  = $_POST['password']; //设定服务器密码
                $ldap_conn = ldap_connect($ldap_host, $ldap_port) or die("Can't connect to LDAP server");
     
                ldap_set_option($ldap_conn, LDAP_OPT_PROTOCOL_VERSION, 3);
                $r = ldap_bind($ldap_conn, $ldap_user, $ldap_pwd);//与服务器绑定
                if ($r) {
                    $map['emp_no'] = $_POST['emp_no'];
                    $map["is_del"] = array('eq', 0);
                    $model         = M("User");
                    $auth_info     = $model->where($map)->find();
                } else {
                    $this->error(ldap_error($ldap_conn));
                }
            } else {
     
                $model = parent::model('login', 'oa');
                $map   = array();
                // 支持使用绑定帐号登录
                $map['emp_no']   = $_POST['emp_no'];
                $map["is_del"]   = 0;
                $map['password'] = md5($_POST['password']);
                //print_r($map);die;
                $auth_info = $model->check_login($map);
            }
     
            //使用用户名、密码和状态的方式进行认证
            if (false == $auth_info) {
                XHandle::halt('帐号或密码错误!', '', 1);
            } else {
                $_SESSION['USER_AUTH_KEY'] = $auth_info['id'];
                $_SESSION['emp_no']        = $auth_info['emp_no'];
                $_SESSION['user_name']     = $auth_info['name'];
                $_SESSION['user_pic']      = $auth_info['user_pic'];
                $_SESSION['dept_id']       = $auth_info['dept_id'];
                //保存登录信息
                $ip                      = $this->get_client_ip();
                $time                    = time();
                $data                    = array();
                $data['last_login_time'] = $time;
                $data['login_count']     = $auth_info['login_count'] + 1;
                $data['last_login_ip']   = $ip;
                $model->save($auth_info['id'], $data);
                header('Location: oa.php');
            }
        }

在以上的代码中,会优先判断验证码是否存在,如果存在则交易验证码的正确性,知乎判断账号和密码是否填写,之后检查emp_no是否为'admin',也就是我们的账号名称,之后由于if(false)为假所以不会进入到if后面的语句中,直接进入到else处理逻辑,在这里会定义一个数组map,之后存储用户传递过来的认证信息,同时对密码进行md5加密存储,之后调用check_login函数进行检查,check_login函数位于:CMSsourcemodeloamodel.login.php
代码逻辑如下:

良精OA办公系统admin+任意密码登录的漏洞复现和代码分析-路人娱乐网

在这里会分别存刚才传递的数组map中再次取出用户提交的认证信息并将其分别赋值,之后拼接到SQL语句中去查询,在这里大家也许注意到了,这里的SQL语句是否有一些不正常呢?确实存在问题,我们将SQL语句复制下来看看:

$sql = "SELECT * FROM ".DB_PREFIXOA."user WHERE emp_no='".$emp_no."' OR name='".$emp_no."' AND is_del='".$is_del."' AND password='".$password."'";

很多人可能会说,这里直接拼接未做过滤处理,应该存在SQL注入漏洞,我们这里先不去管SQL注入问题,我们先来看看这里的SQL语句的设计是否有问题。上面的SQL查询语句格式可以简化为如下:

SELECT * FROM PREFIXOA_user WHERE 条件1=条件值1 or 条件2=条件值2 and 条件3=条件值3 and 条件4=条件值4

由于SQL语句中'And'的优先级会高于or的运行级别,所以最后的执行语句应该是这样的:

SELECT * FROM PREFIXOA_user WHERE (条件1=条件值1) or ((条件2=条件值2) and (条件3=条件值3) and (条件4=条件值4))

之后,我们回到原来的SQL语句中,并对语句进行一个划分:

良精OA办公系统admin+任意密码登录的漏洞复现和代码分析-路人娱乐网

从上面可以看到,该sql语句执行后会查询出账号名为admin的所有信息 或 账号名为admin且密码为正确且is_del值正确的所有数据信息,在正常登陆情况下(账号/密码全部正确)查询出的信息为admin用户的一行记录信息,在账号名为admin但是密码错误的情况下,查出来的依旧为admin用户的一行信息,所以账号的密码在这里根本没有任何校验的作用,而这里程序开发者真正想要的设计应该是这样的:

$sql = "SELECT * FROM ".DB_PREFIXOA."user WHERE ((emp_no='".$emp_no."')OR (name='".$emp_no."')) AND (is_del='".$is_del."') AND (password='".$password."'");

即,查询name为admin或者emp_no为admin 且 密码正确 且 账号依旧有效未被删除的记录信息!
下面我们使用数据库来比对一下二者的区别,可以看到一个有正确的数据(当前错误的设计),一个没有(真正想要的设计)

良精OA办公系统admin+任意密码登录的漏洞复现和代码分析-路人娱乐网

基于以上简要分析,可以看到这里我们输入admin+任意密码登陆都可以成功查询到用户原有的数据库内正确的信息,下面我们继续跟踪后续流程:
在执行完SQL语句之后,会返回一个查询结果:

良精OA办公系统admin+任意密码登录的漏洞复现和代码分析-路人娱乐网

之后我们回到之前的Public.php中的action_check_login函数中:

良精OA办公系统admin+任意密码登录的漏洞复现和代码分析-路人娱乐网

可以看到在后门的代码中会将查询结果返回给auth_info,如果auth_info非false,则将数据库中的信息存储到session中,之后保存,同时最后重定向到oa.php中,通过之前的分析可以知晓,如果这里的用户名为admin,那么密码不管正确与否,返auth_info都不会为False,都会为true。
之后我们回过来再看oa.php中如果进行后续的操作:

良精OA办公系统admin+任意密码登录的漏洞复现和代码分析-路人娱乐网

此时,oa中的参数c与a为空值,那么在L5~6将会将其赋值为Index与run,之后成功进入到L18函加载配置类信息,这里就不再继续跟踪后续的配置加载了,分析到目前用户已经完成了登陆认证,并且成功进入oa了~
下面进行漏洞复现~

漏洞复现

隐藏内容,您需要满足以下条件方可查看
End

良精OA办公系统admin+任意密码登录的漏洞复现和代码分析-路人娱乐网

之后成功进入后台管理界面:

良精OA办公系统admin+任意密码登录的漏洞复现和代码分析-路人娱乐网

在这里我们使用admin+任意密码即可登录~

小小结论

从上面的实例可以看到有时候sql语句的设计如果不合理也会导致某些强硬的判断条件被绕过,尤其是在使用and、OR连接SQL语句时,应该先分析当前要实现的功能,如果有点乱可以使用括号进行区分使得代码逻辑更加规范~

人已赞赏
建站技巧

如何自建Mattermost聊天服务器

2020-11-13 7:02:40

漏洞简析

【路人原创】对某DDOS页端源码的一次简单审计

2020-11-1 22:44:41

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索