前言
最近对某CMS进行了一次审计,发现该CMS在处理登陆认证时底层数据库查询语句存在设计缺陷导致admin用户在不校验密码的情况下直接登录oa系统,下面对该漏洞进行分析介绍。
漏洞分析
文件位置:CMSoa.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'
文件位置:CMSsourcecontroloaPublic.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(