文章目录
SiteServerCMS是一款开源免费的企业级CMS系统,功能比较丰富,代码一多起来,难免会有些漏洞产生,之前应急响应碰到过几次这个系统,有些问题修复了,有些问题依然还在,趁着整理之前零散的资料,结合6.14.0版本写个总结。
SiteServerCMS有多种身份认证方式,这里以最常见的Cookie认证来展开分析:
从何说起呢?渗透,经常开局就只有一个登录框,有时还有验证码,那就从登录框开始吧,SiteServerCMS是后台管理+前台内容(含会员)的前后分离模式,各有独立的登录地址,先从后台登录开始,默认后台登录地址是:
http://IP:Port/SiteServer/pageLogin.cshtml
随便输入个用户名和密码登录查看数据包,通过JSON格式提交到了/api/v1/administrators/actions/login,进入脱发模式,打开源码跟进,位置:
源文件: ./SiteServer.Web/Controllers/V1/AdministratorsController.cs
登录失败次数+1,出局。
使用正确的用户名密码登录,登录成功后,会生成一个accessToken的字符串,这个accessToken是作为Cookie身份认证用的:
var accessToken = request.AdminLogin(adminInfo.UserName, isAutoLogin);
不信且看,走进AdminLogin(),跟进accessToken生成过程:
var accessToken = AdminApi.Instance.GetAccessToken(adminInfo.Id, adminInfo.UserName, expiresAt);
源文件: ./SiteServer.CMS/Core/AuthenticatedRequest.cs
SiteServerCMS有多种身份认证方式,这里的Constants.AuthKeyAdminCookie对应的是Cookie命名份格式: SS+名称,规则如下:
源文件: ./SiteServer.Utils/Constants.cs
public const string AuthKeyUserHeader = "X-SS-USER-TOKEN";
public const string AuthKeyUserCookie = "SS-USER-TOKEN";
public const string AuthKeyUserQuery = "userToken";
public const string AuthKeyAdminHeader = "X-SS-ADMIN-TOKEN";
public const string AuthKeyAdminCookie = "SS-ADMIN-TOKEN";
public const string AuthKeyAdminQuery = "adminToken";
public const string AuthKeyApiHeader = "X-SS-API-KEY";
public const string AuthKeyApiCookie = "SS-API-KEY";
public const string AuthKeyApiQuery = "apiKey";
public const int AccessTokenExpireDays = 7;
public static string GetSessionIdCacheKey(int userId)
{
return $"SESSION-ID-{userId}";
}
回来继续跟进GetAccessToken():
源文件: ./SiteServer.CMS/Plugin/Apis/AdminApi.cs
又回来了,继续回到上一个文件,找到那个GetAccessToken():
还记得第三个参数类型是什么吗? 突然冒出来的WebConfigUtils.SecretKey是什么?JwtHashAlgorithm.HS256又是什么鬼?为了避免篇幅太长:
WebConfigUtils.SecretKey: 加密密钥,圈起来,要考的;
JwtHashAlgorithm.HS256: Hash算法模式,知道就行了。
继续跟进JsonWebToken.Encode(),直接跳过中间的方法到最后一个Encode():
源文件: ./SiteServer.Utils/Auth/JWT.cs
这里的参数对应关系:
payload对应userToken;
key对应WebConfigUtils.SecretKey;
algorithm对应JwtHashAlgorithm.HS256。
然后整个accessToken生成格式为:
算法类型 + 认证信息 + 哈希摘要
Base64UrlEncode(headerBytes) + "." + Base64UrlEncode(payloadBytes) + "." + Base64UrlEncode(signature)
明文格式大致像这样:
{"typ":"JWT","alg":"HS256"}.{"UserId":1,"UserName":"admin","ExpiresAt":"\/Date(1583293343684)\/"}.哈希摘要
accessToken生成完了,看完头发掉了不少,有什么用?
暂时还派不上用场,现在我要讲另一件事:加密与解密。
且回到AdminLogin(),登录成功后会将accessToken通过Cookie返回客户端:
CookieUtils.SetCookie(Constants.AuthKeyAdminCookie, accessToken);
这里暂时不去理会是否isAutoLogin,捡简单的,跟进SetCookie():
源文件: ./SiteServer.Utils/CookieUtils.cs
注意这里有一个很关键的参数isEncrypt,缺省值是true,默认都是启用的:
加密: TranslateUtils.EncryptStringBySecretKey()
解密: TranslateUtils.DecryptStringBySecretKey()
且看EncryptStringBySecretKey():
源文件: ./SiteServer.Utils/TranslateUtils.cs
加密后将在字符串中的+、=、&、?、\特殊符号用0***0代替,解密前则反过来操作,然而那个SecretKey又出现了,它保存在根目录的Web.config中的appSettings节点下,是加解密的密钥,它的初始化是这样的:
源文件: ./SiteServer.Utils/WebConfigUtils.cs
SecretKey = StringUtils.GetShortGuid();,一个16位字符串的UID,类是:6f2bc5f951826267,注意一下150行被注释掉的SecretKey值。
回到正题,跟进encryptor.DesEncrypt()加密过程:
源文件: ./SiteServer.Utils/Auth/DesEncryptor.cs
使用DES加密,没有指定加密模式(.Net默认是CBC模式,是不是又想到了什么?),密钥从16位减到8位(是不是又有人想着爆破了?),加密解密iv都是固定值:
byte[] iv = { 0x12, 0x34, 0x56, 0x78, 0x90, 0xAB, 0xCD, 0xEF };
现在来梳理一下accessToken的加密过程:
accessToken -> EncryptStringBySecretKey() -> ToBase64String() -> Replace()
用正确密码登录成功Cookie则返回像下图这么一串东西,下面为未加密的accessToken:
冒着掉头发的风险又看了一大截,居然说登录还是要正确的密码? 骗子。。。
还没讲完,那后端是如何通过Cookie认证呢?一般都会在控制器看到这么写判断是否有权限:
var AuthRequest = new AuthenticatedRequest();
if (!AuthRequest.IsAdminLoggin) return;
以管理员登录为例,首先从Cookie中获取accessToken,获取流程如下:
源文件: ./SiteServer.CMS/Core/AuthenticatedRequest.cs
从GetCookie()取出后,同文件AuthenticatedRequest():
从AdminToken中获取信息做判断,还记得AdminLogin中也有个IsAdminLoggin = true;吗?
至此,通过Cookie身份认证部分讲得差不多了,普通用户的认证方式与管理员的类似,不重复了。
看起来好像没什么问题呀?一般,进入正题之前,都要先讲讲历史,如果网上搜索siteserver+漏洞关键词,你会看到模板远程GetShell、XSS/抓包绕过后台、挂马挖矿…等相关内容,而导致这些漏洞产生大多跟加密密钥泄露有关,这里分5.0版本前后,5.0版本之前可能没有源码,可以把.dll丢到dnSky里反编译。
在讲历史之前,我先讲一个和密钥(SecretKey)有关的故事,在以前的版本,有些管理接口可能是为方便,可以匿名访问,身份认证仅依赖于系统的加密字符串,还是以v6.14.0为例,看文件:
源文件: ./SiteServer.BackgroundPages/Ajax/AjaxOtherService.cs
这个AJAX请求地址就是不需要权限的,而远程文件下载地址要求是加密字符串,不然没法使用,好了,故事讲完了。
这里为什么要把5.0版本作为分界线呢? 因为5.0版本之前,密钥(Cipherkey)是存在数据库的,它存在一张bairong_Config表的SettingsXML字段里,生成算法如下:
一个8位随机字符串,IV也是写在源码里:
byte[] rgbIV = new byte[] { 18, 52, 86, 120, 144, 171, 205, 239 };
我们知道之前的某些版本是存在SQL注入的,利用SQL注入读取这个字段获取Cipherkey,然后就可以在加密下载链接,配合远程文件下载达到GetShell的目的。
1.x和2.x这种上古版本,年代久远就直接忽略了。
在 5.0 版本之后
5.0版本之后的secretKey是存在文件里的,其中5.x版本是存在:
源文件: ./SiteFiles/Configuration/Configuration.config
secretKey是 硬编码固定值: vEnfkn16t8aeaZKG3a4Gl9UUlzf4vgqU9xwh8ZV5
而6.0之后secretKey保存在Web根目录的Web.config里(随机生成),IV和5.x一样硬编码在源码里:
byte[] iv = { 0×12, 0×34, 0×56, 0×78, 0×90, 0xAB, 0xCD, 0xEF };
有了secretKey和IV就可以本地去加密数据,然后远程下载文件GetShell和计算管理员accessToken登录后台,加密算法python3实现:
def encrypt(msg, key, iv):
pad = 8 - len(msg) % 8
for i in range(pad):
msg = msg + chr(pad)
obj = DES.new(key, DES.MODE_CBC, iv)
buf = obj.encrypt(msg)
txt = base64.b64encode(buf).decode()
txt = txt.replace('+','0add0').replace('=','0equals0').replace('&','0and0')
txt = txt.replace('?','0question0').replace("'",'0quote0').replace('/','0slash0')
txt = txt + '0secret0' # v6.x
注意: 这里讲的版本划分只是大概版本,具体是哪个小版本开始是随机生成和改变存储位置,有兴趣的自个查一下。
前面讲到5.x版本密钥是固定的,可以用密钥构造Cookie直接登录后台,比如:CNVD-2018-00712,这里不展开说了,那有没有不用密钥的呢?
开始是从登录框说起,那么就以登录框结束吧,我再讲二分钟。。。一个不用获取密钥登录后台的栗子。
还记得前面的accessToken生成过程和Cookie身份认证中所用到的参数么?是不是都没有口令参数,都只用到了UserId和UserName?
还记得前面提到的前台和后台是分离的么?也就是管理员和会员各用一张数据表。
然而数据是加密的,有啥用?
注意到前面登录成功返回那数据包没有,UserId是整型递增的。
那么,在前台注册一个用户名与后台管理员用户名一样的用户,只要使其UserId和Username相等,是不是Cookie的关键信息是一样的。
我们来打开前台会员中心试一下:
注册一个名为adzroolsmin用户,然后登录,查看Cookie:
SS-USER-TOKEN-CLIENT是没加密的,SS-USER-TOKEN是加密的,还记得前面发送Cookie时管理员的名称是什么了吗?SS-ADMIN-TOKEN,那么,我们直接修改一下,然后访问后台管理员页面(为什么不选择直接跳转控制台主页/SiteServer/main.cshtml?那是另一个故事了):
直接跳进了后台管理页面,管理员ID往往是1,再多几个管理员也还是个位数,前台注册低位ID也是个迷,利用条件是不是很鸡肋,其实5.x版本里的accessToken是没有Userid这个字段的,然俄。。。
如今在身份鉴别模块能利用万能密码去登录的已不多见,更何况有着各种WAF,而今出现的身份鉴别模块的漏洞更倾向于逻辑类型,有时还需通过多种漏洞组合去利用。在平时做代码审计的时候往往需要耐心,也需要细心,很多时候两个看起来没什么问题的功能,遇到一起就擦出了火花,就像上面的Cookie构造里的栗子。
故事讲完了,下课,咱有缘再见。。。 -_-#
完整版脚本传送门: https://github.com/zrools/tools/tree/master/python
*本文原创作者:zrools,本文属于FreeBuf原创奖励计划,未经许可禁止转载