声明:本公众号大部分文章来黑白之间安全团队成员的实战经验以及学习积累,文章内公布的漏洞或者脚本都来自互联网,未经授权,严禁转载。 请勿利用文章内的相关技术从事非法测试,如因此产生的一切不良后果与文章作者和本团队无关。
前言
友点 CMS V9.1 后台登录绕过 GetShell ,攻击者可无需任意权限即可登录后台并 getshell
漏洞影响
youdiancms <=9.1
漏洞细节
漏洞流程
- 验证码可设置session
- session(AdminGroupID==1) 超级管理员
- 后台模板修改代码执行
任意 session 获取
站在前人的肩膀上,看到 App/Lib/Action/BaseAction.class.php ,217 行 verifyCode 函数
function verifyCode(){
$length = $_GET['length'];
$mode = $_GET['mode'];
$type = $_GET['type'];
$width = $_GET['width'];
$height = $_GET['height'];
$verifyName = $_GET['verify'];
import("ORG.Util.Image");
Image::buildImageVerify($length, $mode, $type, $width, $height, $verifyName);
}
这是一个生成验证码的函数,可以看到,这些参数都是可控的,进入
Image::buildImageVerify($length, $mode, $type, $width, $height, $verifyName);
static function buildImageVerify($length=4, $mode=1, $type='png', $width=48, $height=22, $verifyName='verify') {
import('ORG.Util.StringEx');
$randval = StringEx::randString($length, $mode);
$_SESSION[$verifyName] = md5($randval);
...
看到这句
$_SESSION[$verifyName] = md5($randval);
$verifyName 是可控的,也就是说我们可以控制 $_SESSION 的键,后面的 md5 值暂时不考虑
接下来我们找可以利用的地方,我们看到管理员登陆的地方 App/Lib/Action/AdminBaseAction.class.php
function _initialize(){
$mName = strtolower(ACTION_NAME);
$NoCheckAction = array('login', 'verify','checklogin','showcode','logout');
if( !$this->isLogin() && !in_array($mName, $NoCheckAction)){
$this->redirect("Public/login");
}
if( !$this->checkPurview() ){
$this->redirect("Public/welcome");
}
这是初始化的位置,需要注意两个地方
$this->isLogin()
$this->checkPurview()
想要得到登陆的状态,就需要绕过这两个方法,先来看 $this->isLogin()
function isLogin(){
$b = session("?AdminID") && session("?AdminName");
return $b;
}
这里很简单,只要存在 AdminID 和 AdminName 这两个session就可以,我们之前找到的地方就可以赋值
再来看 $this->checkPurview()
function checkPurview(){
$gid = session('AdminGroupID');
if( $gid == 1 ) return true;
这里首先要有一个 session AdminGroupID,这个容易满足,也可以直接赋值,但是只能获得普通的登陆权限,想要获得超级管理员的权限,就需要 $gid == 1 ,看似没有办法完成,但是注意到这里是弱比较,而之前赋值的时候,是用的 md5 值,我们只需要得到一个 md5,满足第一个字符是 1 ,第二个字符不是数字,就可以完成
回到一开始的漏洞位置,我们看看如何生成 md5 值
import('ORG.Util.StringEx');
$randval = StringEx::randString($length, $mode);
$_SESSION[$verifyName] = md5($randval);
我们看到这里应该是生成的随机字符串,传入的 $length 和 $mode 是我们可控的,进入该函数
App/Core/Extend/Library/ORG/Util/StringEx.class.php 的 128 行
static public function randString($len=6,$type='',$addChars='') {
$str ='';
switch($type) {
case 0:
$chars='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.$addChars;
break;
case 1:
$chars= str_repeat('0123456789',3);
break;
case 2:
$chars='ABCDEFGHIJKLMNOPQRSTUVWXYZ'.$addChars;
break;
case 3:
$chars='abcdefghijklmnopqrstuvwxyz'.$addChars;
break;
case 4:
$chars = "们以我到...".$addChars;
break;
default :
$chars='ABCDEFGHIJKMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789'.$addChars;
break;
}
if($len>10 ) {
$chars= $type==1? str_repeat($chars,$len) : str_repeat($chars,5);
}
if($type!=4) {
$chars = str_shuffle($chars);
$str = substr($chars,0,$len);
}else{
for($i=0;$i<$len;$i++){
$str.= self::msubstr($chars, floor(mt_rand(0,mb_strlen($chars,'utf-8')-1)),1,'utf-8',false);
}
}
return $str;
}
可以看到,这里是利用 $type 选择相应的字符串,然后用 $len 控制长度,最后生成字符串
满足条件的 md5 值,1 - 100 以内就有 3个 ,为了减少爆破的次数,我们就直接选择数字以及长度设置成两位
后台getshell
后台 getshell 可以利用修改模板
App/Lib/Action/Admin/TemplateAction.class.php 的 68 行 saveModify 方法
function saveModify(){
header("Content-Type:text/html; charset=utf-8");
$ThemeName = C('HOME_DEFAULT_THEME');
$_POST['FileName'] = YdInput::checkFileName( $_POST['FileName'] );
$FullFileName = TMPL_PATH.'Home/'.$ThemeName.'/'.ltrim($_POST['FileName'],'/');
if( !$this->isValidTplFile($FullFileName)){
$this->ajaxReturn(null, '无效模板文件!' , 0);
}
$FileContent = htmlspecialchars_decode($_POST['FileContent']);
if (get_magic_quotes_gpc()) {
$FileContent = stripslashes($FileContent);
}
$b = file_put_contents($FullFileName, $FileContent);
首先获取文件名,然后将 post 上来的 content 写入模板文件,过程较为简单
Exp
https://github.com/N0puple/poc-set/tree/main/YouDian%20CMS%20Auth%20Bypass%20and%20RCE
总结
测试版本为 9.0,很多文章中写的利用短标签绕过过滤来getshell,这里没有必要,就没有去用,应该是 9.1 才有的过滤,这个前面的绕过漏洞是很有意思的,后面的后台 getshell 写的有点草率,尤其是exp中,直接覆盖了整个模板文件,真实的渗透测试中还是不要这么干,毕竟会影响到正常的业务了。
参考
- https://forum.butian.net/share/132
欢迎大家关注公众号
|