记对某CMS的一次代码审计

2020-04-01 约 504 字 预计阅读 3 分钟

声明:本文 【记对某CMS的一次代码审计】 由作者 Al1ex 于 2020-04-01 10:01:46 首发 先知社区 曾经 浏览数 96 次

感谢 Al1ex 的辛苦付出!

前言

最近对某CMS进行了一次审计,发现该CMS在处理登陆认证时底层数据库查询语句存在设计缺陷导致admin用户在不校验密码的情况下直接登录oa系统,下面对该漏洞进行分析介绍。

漏洞分析

文件位置:CMS\oa.php
代码内容:

代码逻辑:该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'

文件位置:CMS\source\control\oa\Public.php
代码内容:

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

之后,继续跟踪代码到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文件中,代码如下:

此时,和之前分析的逻辑一致,唯一不同的是最后会调用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函数位于:CMS\source\model\oa\model.login.php
代码逻辑如下:

在这里会分别存刚才传递的数组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语句中,并对语句进行一个划分:

从上面可以看到,该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 且 密码正确 且 账号依旧有效未被删除的记录信息!
下面我们使用数据库来比对一下二者的区别,可以看到一个有正确的数据(当前错误的设计),一个没有(真正想要的设计)

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

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

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

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

漏洞复现

访问一下URL,之后输入admin/sjdkgljsdkgjdkg:
http://192.168.174.160/oa.php?c=Public&a=login

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

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

小小结论

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

关键词:[‘安全技术’, ‘漏洞分析’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now